AgentKthx is a stdlib-only agentic framework —
dependencies = []inpyproject.toml, and that's the load-bearing design decision behind almost every convention in this file. If your contribution would require adding a runtime dependency, please open a Discussion before opening a PR; the answer is almost always "find a stdlib way" or "make it a plugin."
This document is the short version. The long version is audit/brief.md — read the Critical Files Index and Known Landmines sections there before touching anything non-trivial.
Be excellent to each other. Concretely:
- Assume good faith; ask before accusing.
- Disagree about code, not about people.
- Zero tolerance for harassment, discrimination, or doxxing.
- Repo owner (VTSTech) has final say on enforcement; bans are reversible on request.
We don't have a separate CODE_OF_CONDUCT.md because the above is the whole policy. If the project grows beyond a single maintainer, we'll adopt Contributor Covenant 2.1.
- Zero runtime dependencies is the feature. Every "just use
requests" suggestion is wrong by default.urllib,json,sqlite3,ast,subprocess,socket,ipaddress,threading,weakref— that's the toolbox. - Hackable over clever. A 50-line straightforward implementation beats a 10-line metaprogramming trick. The codebase is meant to be read end-to-end by someone learning how agents work.
- Defense-in-depth, not sandbox. The security primitives (
validate_path,sanitize_command,is_safe_url,safe_eval,sanitize_tool_output) are best-effort guards, not isolation boundaries. Document new tools accordingly. - The audit register is canonical. Findings live in
audit/audit.md(OPEN) andaudit/deltas.md(CLOSED/WONTFIX). Cite finding IDs in commit messages and PR descriptions.
- Python 3.11+ (CI matrix covers 3.11, 3.12, 3.13, 3.14)
git- An editor that won't fight you on line endings (
.gitattributesenforces LF)
git clone https://github.com/VTSTech/AgentKthx.git
cd AgentKthx
pip install -e . # editable install; zero runtime deps so this is fast
pip install -e ".[dev]" # if you want pytest/black/ruff (see below)There is no [dev] extra in pyproject.toml — dev tools are listed in .github/workflows/ci.yml:
pip install pytest black ruffpython -m pytest tests/ -q # ~13s, 2608 tests, 16 skipped
ruff check agentkthx/ tests/ # REQUIRED CI check
black --check agentkthx/ tests/ # REQUIRED CI checkIf any of these fail locally, they will fail in CI. Fix before pushing.
| Aspect | Convention |
|---|---|
| Python version | 3.11+ — match/case and | unions OK; no PEP 695 generics/type aliases, no backslash/multi-line f-string expressions (PEP 701, 3.12-only) |
| Formatter | black (defaults; line length 88) |
| Linter | ruff (config in pyproject.toml) |
| Imports | stdlib only at runtime; pytest / black / ruff are dev-only |
| File naming | snake_case.py modules, PascalCase classes, SCREAMING_SNAKE constants |
| Test files | tests/test_*.py; per-release regression files use tests/test_r07_XX_<topic>.py |
| Type hints | Encouraged for new public APIs; not enforced (no mypy yet — see TEST-04 audit finding) |
| Docstrings | Required on public classes and on mixin host-contract methods (the "host declares X" pattern) |
Before adding an import statement, ask:
import foo— isfooin the standard library for Python 3.11+? If yes, fine.from foo import barwherefoois a third-party package — no. Either find a stdlib equivalent, or move the code into a plugin (plugins may declare their own dependencies in their manifest, but the core package stays pure).__import__("foo")orimportlib.import_module("foo")for an optional dep — acceptable if wrapped intry/except ImportErrorand the failure path degrades gracefully (seeagentkthx/__init__.pyfor the pattern:PersistentMemory,ACPPlugin,Soulare all optional-import).
If you genuinely cannot solve the problem with the stdlib, open a Discussion describing the use case. We've found workarounds for HTTP, SSE, SQLite, AST eval, SSRF guards, plugin loading, and process lifecycle — chances are good we can find one for you.
cli/commands/* → cli/agent_factory → Agent (agent.py)
│
┌───────────────────────────────┤
▼ ▼
AgentSetupMixin AgenticLoopMixin ── ToolExecutionMixin ── CompactionMixin ── StreamingMixin
(agent_setup.py) (agentic_loop.py) (tool_execution.py) (compaction.py) (streaming.py)
│ │ │
▼ ▼ ▼
soul/loader.py core/error_recovery.py core/tool_parse.py
│ │ │
▼ ▼ ▼
core/models.py ◄──── core/helpers.py ◄────────────────────────────── core/api_resilience.py
▲
│
┌─────────────────┴┴─────────────────┐
│ │
backends/base.py ◄── backends/cloud_base.py ◄── plugins/{zai,openrouter,gemini,openai,huggingface,mistral,pollinations,orcarouter}
│ ▲
▼ │
backends/openai_compat.py ◄───────────────────────────┘
│
▼
backends/ollama.py ◄── backends/llama_server.py ◄── plugins/turboquant (lifecycle: TurboState, _build_command)
The full version with file-by-file commentary is in audit/brief.md. The one thing to internalize: the Agent is a 5-mixin composition, not a god-class. Mixins access the host via self.X with a docstring-declared "host contract." If you add a new mixin or change what the host must provide, update every affected docstring.
These files have outsize blast radius. A bad change here will break many things.
| File | Why it's hot |
|---|---|
agentkthx/core/helpers.py (~1,384 LOC) |
Imported by 18+ modules. Any change to validate_path, sanitize_command, is_safe_url, sanitize_tool_output, or normalize_args affects every tool, every backend, every plugin. |
agentkthx/backends/cloud_base.py |
Shared base for 8 cloud plugins. A bug here is a bug × 8. |
agentkthx/cli/agent_factory.py (~820 LOC) |
Touched by every chat startup path (catalog ladder + tool detection + quant detection) AND the model-switch callback. Changes here affect every backend launch. |
agentkthx/shared_args.py (~563 LOC) |
CLI flag definitions must be wired in 3 surfaces (add_agent_args, add_shared_args, test parser) + parse_shared_args + SharedConfig. Miss one and the flag is silently ignored somewhere. |
agentkthx/plugins/_loader.py (~1,530 LOC) |
PluginManager singleton loads at startup. A failure cascades to all backends. |
agentkthx/backends/llama_server.py |
The _agent_internal exclusion tuple is duplicated in _generate_completion and _stream_completion. Add a new agent-internal kwarg to only one → the other leaks it into /completion request bodies. |
agentkthx/core/agentic_loop.py |
The unified loop body. Both streaming and non-streaming paths funnel through _generate_with_retry (line ~264). New instrumentation should hook there. |
os.kill(pid, 0)kills on Windows (ROB-33,plugins/turboquant/turbo.py:159-183). On Windows, any signal other than CTRL_*_EVENT isTerminateProcess. Do not probe process liveness this way on Windows paths.--force-reactisstore_true(MAINT-25).--force-react=Falseis an argparse error. If you need an opt-out for the local-backend default-to-ReAct behavior, add a new flag — don't try to extend the existing one.Agent.add_toolis deprecated but silent (MAINT-16). Useregister_toolfor mid-session tool addition.add_toolclears conversation memory for backward compat and emits noDeprecationWarning._agent_internaltuple is duplicated across twollama_server.pymethods. If you add an agent-internal kwarg, add it to BOTH.- New CLI param → 3 surfaces +
parse_shared_args+SharedConfig, useis not None(notor— see ROB-35), mirror the_explicitpin pattern. AGENTKTHX_NO_UPDATE_CHECK=1skips the 3-request startup check. Don't add new network calls to the startup path without an env-var escape hatch.MemoryConfig.max_tokens=0default — token tier is opt-in. Don't change this default casually.- BitNet
repeat_penalty=1.3is a default, not a hardcode. The kwargs loop inllama_server.pydeliberately does NOT skiprepeat_penalty. Don't "restore" the hardcode; explicit kwargs and/param repeat_penaltymust override it. BUILTIN_REGISTRYtodo store is a module-level singleton. TwoAgentinstances in one process share todos unlessset_todo_session()is called. Document this if you add new session-scoped tool state.normalize_argsstrategy 5 matches substrings (MAINT-03).{"e": ...}will bind toexpression. Prefer exact keys in tool args.Memory.sanitize_historymutates_messagesin place (PERF-01). Don't call it inside nested iteration.
The full list with severity is in audit/audit.md (37 OPEN findings) and audit/brief.md (top 22 in the "Known Landmines" section).
Subclass CloudBackend (~100 LOC). Override:
- Class attributes:
provider_name,base_url,is_cloud = True,auth_header_name,auth_header_prefix _get_models_url(),_get_chat_url(),_parse_models_response(),_parse_chat_response()test_tool_support(model)— return aToolSupportLevel(capabilities-first probe preferred over sampled inference)test_thinking_support(model)— return aThinkingSupport
Register in backends/__init__.py. Add a plugins/<provider>/<provider>.py shim if you want it auto-discovered as a plugin.
Beware the retry-loop family (ROB-29 / MAINT-23, open). The skeleton has not yet been lifted to the base class. If your backend needs retry-on-transient, you'll end up copy-pasting the loop from mistral/ or pollinations/. Please contribute a refactor to lift it into cloud_base.py rather than adding a third copy.
Subclass BaseBackend directly. Implement generate(), _generate_stream(), list_models(), test_tool_support(), test_thinking_support(). Use urllib.request for HTTP (see llama_server.py for the SSE buffering pattern). Do not add a network library as a dependency.
This is the most error-prone common contribution. The checklist:
- Add the flag to
add_agent_args()inshared_args.py(coverschat,run,agent). - Add the flag to
add_shared_args()(covers example scripts). - Add the flag to the
testsubcommand parser (incli/parser.py). - Add the field to
SharedConfig. - Read it in
parse_shared_args()usingis not None— neveror(ROB-35). - If the param has an env fallback, use
_env_int/_env_floatand document the env var name in the help text. - If the param should survive
/modelswitches, set an_explicitpin in_build_agentand clear it on/param reset. - Add a test in
tests/test_<topic>.py— for parameter parsing, the convention is one test per flag in atest_r07_XX_<topic>.pyregression file.
Use @agent.tool() decorator or call agent.register_tool(...) mid-session. Do not use agent.add_tool() — it's deprecated, clears memory, and emits no warning (MAINT-16).
Tool args go through helpers.py:normalize_args (5 strategies, last is permissive — MAINT-03). Prefer exact param names; the fuzzy matcher is a footgun.
Tool output is automatically wrapped by sanitize_tool_output — do not pre-sanitize.
If your tool has side effects (shell, file write, network), mark it sequential via the tool registry so it's excluded from parallel batches. See tools/builtins.py for the pattern.
Read docs/PLUGIN_SPEC.md and schemas/v0.2/plugin.schema.json (note: the schema is declared but not validated — SEC-13 is open — so dict-shape checks in _parse_manifest are the actual contract).
Minimum viable plugin:
my_plugin/
├── manifest.json # name, version, hooks, optional sha256
└── __init__.py # register_<your_thing>() function called by the loader
The loader does Kahn topological sort on declared dependencies, then exec_modules each plugin. If a sha256 pin is present, it's verified fail-closed BEFORE exec_module. If absent, the plugin loads silently (SEC-13).
- Co-located in
tests/, namedtest_<topic>.py - All current tests are mocked unit tests — there is no integration tier (TEST-01, open). If you add a test that hits a real network endpoint or real local backend, mark it
@pytest.mark.integrationand add a fixture that's skipped when the endpoint isn't reachable. Don't break the "13-second test suite" invariant. - For per-release regressions, use
tests/test_r07_XX_<topic>.py(e.g.,test_r07_19_zai_probe_transient.py). The naming convention is enforced by convention, not by code. - Per-release exemplars from R07.19 are the house style: small, focused, one behavior per test, mocked backend.
- Souls (
agentkthx/soul/+agentkthx/souls/): YAML/markdown persona packages. Mostly safe. Follow the spec indocs/(Soul Spec v0.5). If your soul uses ReAct format, ship theAction:/Action Input:block explicitly — MAINT-24 means custom souls without their own block produce prompts with zero format instructions. - Skills (
agentkthx/skills/): Python code that runs with full process privileges. Be careful. The bundled skills (codebase-audit, crypto-signals, skill-creator, test-harness) are the house style.
The audit is part of the project, not bolted on. Before adding work:
- Read
audit/audit.md(37 OPEN findings, ID-indexed, priority matrix). If your work touches an open finding, reference its ID in your PR. - Read
audit/deltas.md(77 archived: 70 CLOSED + 7 WONTFIX). Don't reintroduce a closed design decision without discussing it. - The audit tooling is in
agentkthx/skills/codebase-audit/— read itsSKILL.mdfor the re-audit workflow and the dashboard parser contract.
When you close a finding with your PR, add a delta entry to audit/deltas.md with the format used by existing entries (ID, status, closure prose, commit SHA).
<short summary, imperative mood, ≤72 chars>
<optional body, wrapped at 72, explaining why not what>
<optional footer>
Closes <finding ID or issue #>
Examples from the existing history:
Fix #13: cloud thinking name heuristics match model segment only
The R07.19 heuristics matched against the full model slug including
vendor prefixes, so `thinkingmachines/*` cached `YES` under
`thinking:<model>` keys incorrectly. Scope the regex to the model
segment after the last `/`.
Closes ROB-13 (audit/deltas.md)
- Tests pass:
python -m pytest tests/ -q - Lint passes:
ruff check agentkthx/ tests/ - Format passes:
black --check agentkthx/ tests/ - No new runtime dependencies added (or Discussion approved)
- If touching a critical file, the change is minimal and explained
- If closing an audit finding,
audit/deltas.mdupdated - If adding a CLI flag, wired in all 3 surfaces +
SharedConfig+parse_shared_args - If adding a backend,
test_tool_supportandtest_thinking_supportimplemented - If adding a tool, registered via
register_tool(notadd_tool) - New tests added; if integration, marked and skip-by-default
-
audit/brief.mdupdated if the architecture map or critical files index changed materially
Prefer small, focused PRs. If a PR touches more than ~10 files or adds more than ~500 LOC, expect review to take longer and consider splitting. The exception is a per-release PR that closes a cluster of related findings — those are expected to be larger.
- Bugs → GitHub Issues, with a reproducer
- Feature requests → GitHub Discussions → "Ideas" category
- Questions → GitHub Discussions → "Q&A" category
- Security reports → see
SECURITY.md(do NOT use public issues)
Issues that lack a reproducer or a clear ask will be closed with a "needs more info" label after 14 days.
- All tests pass on
main - Bump version in
pyproject.tomlandagentkthx/__init__.py(scripts/bump-version.shautomates this) - Update
docs/CHANGELOG.mdwith R07.XX section - Tag:
git tag -s v0.7.XX(signed; if you don't have a signing key, annotatedgit tag -ais acceptable) - Push tag → triggers release workflow → PyPI publish
- Add delta entries for any findings closed in the release
The release workflow lives in .github/workflows/. There is no auto-release on every push; releases are deliberate.
Contributors are credited in docs/CHANGELOG.md per release. If you'd prefer not to be named, say so in your PR.
- The codebase is small enough to read end-to-end. If you're stuck on where something lives,
audit/brief.mdhas the map;grepis your friend; don't be afraid to ask in Discussions. - The owner (VTSTech) is a single maintainer with limited bandwidth. PRs that match the conventions in this document will move faster than ones that don't.
- The audit register is the source of truth for known issues. If you find a bug, check the register first — it may already be tracked.
Thanks for contributing.