Skip to content

Development

Repository layout and a map of where each subsystem lives, for anyone working on Jenny’s Python side.

Android is the only runtime target for the finished product, but nearly all of the logic — the agent loop, tools, providers, sessions, config — is plain Python that also runs under CPython on your desktop for testing. See Build from source for getting a real APK on a device, including the desktop-only run_gateway() shortcut for iterating on the Python side without one.

Path What’s there
jenny/ The Python package: agent core, providers, channels, tools, config, WebUI backend. This is what gets bundled into the APK via Chaquopy.
jenny/templates/ui/ The mobile-first WebUI’s static assets (HTML/CSS/JS), served by the gateway and rendered inside the Android WebView.
jenny/templates/ (top-level .md files) Prompt templates (HEARTBEAT.md, dream.md, etc.) copied into a fresh workspace on first run.
jenny/skills/ Built-in skill definitions (SKILL.md folders) loaded into agent context.
android/ The native Android project: Kotlin MainActivity/GatewayService, the Gradle build, the Chaquopy configuration that embeds the jenny package and its pinned dependencies.
tests/ The pytest suite. Mirrors the jenny/ package structure directory-for-directory (tests/agent/ for jenny/agent/, tests/webui/ for jenny/webui/, and so on) — a new module usually gets a matching test directory in the same relative location.
docs/ This documentation.
scripts/ Standalone helper scripts (check_dco.sh, vendorize_ui.py, capture_screenshots.sh), kept outside CI YAML so they’re runnable and testable on a developer machine too.
AGENTS.md The canonical architecture reference for AI coding agents (and a good orientation doc for humans too) — this page summarizes it, but AGENTS.md is the source of truth if the two ever disagree.
Terminal window
git clone https://github.com/flagdizero/jenny-android-ai-agent.git jenny
cd jenny
pip install -e ".[dev]"

That is what CI installs. The dev extra in pyproject.toml pins, to exact versions, pytest, pytest-asyncio and ruff plus three test-only packages. Two of them are not optional if you intend to run the suite:

  • cryptography — the encrypted-backup tests. On Android backup crypto uses javax.crypto instead, so this must never end up in requirements-android.txt / requirements-android.lock.txt.
  • asyncssh — the desktop SSH backend raises an in-process server for the suite. Without it the SSH suites do not run: test_ssh.py, test_ssh_jobs.py and test_ssh_backend_dev.py (and one test in test_ssh_api.py) call pytest.importorskip("asyncssh") and skip, so nothing fails and the suite stays green while the SSH coverage quietly disappears. On Android the client is jsch through a native bridge, so the same rule applies: never in the Android requirements.

The third is Pillow, for tests/webui/test_mascot_layer_sources.py, which skips whole without it. android/image_source/gen_icons.py and gen_pose_webp.py use it too, to regenerate the shipped icon and mascot assets; they are not part of a normal build, which is why Pillow is in no Android requirement file.

Two more things the suite can want, outside Python:

  • node — about twenty WebUI suites execute the real JS. They skip without it, so the suite still goes green while ~200 behaviour tests quietly do not run.
  • jsdom — the suites that mount the whole home in a DOM (tests/support/home_dom.py) and the graph contract need it, and skip without it. It is not a dependency of the repo: npm install --no-save [email protected], then run the suite with NODE_PATH=$PWD/node_modules, as CI does. node_modules/ is not in .gitignore, so delete it afterwards.

In CI, tests/webui/test_node_is_available.py fails instead of letting node, jsdom or Pillow go missing in silence. It guards only those three: a broken asyncssh or cryptography install would still skip its suites silently in CI. That is a known gap, not a design choice.

The high-level data flow: an async MessageBus (jenny/bus/queue.py) decouples channels from the agent core. The WebSocket channel (jenny/channels/websocket.py) receives messages from the WebUI and publishes them as InboundMessage events onto the bus; AgentLoop (jenny/agent/loop.py) consumes them, builds context, and coordinates the turn; AgentRunner (jenny/agent/runner.py) drives the actual multi-turn LLM conversation — sending messages, receiving tool calls, executing tools, streaming responses; results come back out as OutboundMessage events. See Architecture and The agent turn for the full picture.

Subsystem Lives in Notes
Agent loop / turn coordination jenny/agent/loop.py, runner.py AgentLoop manages session keys, hooks, context building; AgentRunner executes the tool-calling conversation loop.
LLM providers jenny/providers/ base.py is the common provider interface; factory.py builds the runtime provider from config. See Add a provider for how to add a new one — that’s a separate page, not duplicated here.
Channels jenny/channels/ Four channels: WebSocket (websocket.py), Telegram (telegram.py), and on Android Notification (notification.py) and Floating (floating.py); dispatcher.py routes outbound bus messages to whichever are active, with retry, delta coalescing, and progress filtering.
Tools jenny/agent/tools/ Filesystem, python_exec, Android web search/fetch, cron, subagent spawning, long-running tasks, self-modification. Tools are registered explicitly: loader.py imports a fixed module list and each module declares its own TOOLS = [...]; name collisions raise at startup — there is no automatic module scanning. See Write a tool.
Memory jenny/agent/memory.py Session history persistence plus Dream two-phase memory consolidation. Atomic writes with fsync for durability.
Sessions jenny/session/ History persistence, context compaction, sustained-goal state tracking (goal_state.py); TTL-based auto-compaction lives next door in jenny/agent/autocompact.py (AutoCompact). The user conversation is a single unified session (unified:default, see keys.py), except that a project chat on the WebSocket channel gets its own project:<name> session; internal work (cron, Dream, heartbeat) uses separate internal keys via session_key_override.
Config jenny/config/schema.py, loader.py Pydantic-style config (jenny/pydantic_compat/ — a stdlib-only reimplementation, see FORK_BOUNDARY.md) loaded from workspace/config.json. Supports camelCase aliases for JSON compatibility.
WebUI (frontend) jenny/templates/ui/ The mobile-first SPA. Talks to the gateway over the same WebSocket used for chat, plus HTTP routes under /api/.
WebUI (backend) jenny/webui/ The /api/ route handlers backing the SPA — apps, settings, media, skills, transcript, token usage, workspaces, file preview — plus gateway service/token wiring.
Jenny Apps jenny/apps/ Runtime for user-authored mini-apps: manifest.py, executor.py, storage.py, summary.py, http.py, proxy.py, token.py. See Write a mini-app.
Command router jenny/command/ Slash command routing and built-in command handlers.
Skills jenny/skills/ Built-in skill definitions loaded into agent context.
Security jenny/security/ Workspace policy/access control plus SSRF network protections.
Gateway entry point jenny/android_entry.py → gateway_runtime._run_gateway() → jenny/runtime/container.py::GatewayContainer GatewayContainer is the explicit composition root: builds the whole object graph and owns runtime state (onboarding-deferred agent creation, ordered shutdown drain).
Runtime state jenny/runtime/context.py::RuntimeContext The single source of truth for the workspace directory, Android context, and config-path override. Accessors like get_workspace_path() / get_android_context() / get_config_path() delegate here rather than reading scattered module globals.
Cron / delivery jenny/runtime/cron_dispatch.py::CronDispatcher, jenny/runtime/delivery.py::ChannelDeliverer

Several large classes are decomposed into focused mixins/leaf modules composed via MRO, with behavior kept identical to a monolithic version — this is a structural pattern worth knowing before you go looking for a method and can’t find it on the class you expected:

  • AgentLoop ← turn_states / turn_persistence / loop_provider / loop_tasks (+ turn_types)
  • AgentRunner ← request_execution / tool_execution (+ context_governor, usage_accounting, history_repair, tool_error_policy)
  • OpenAICompatProvider ← openai_compat_parsing (+ openai_compat_helpers)
  • AnthropicProvider ← anthropic_conversion
  • WebSocketChannel ← ws_sender (+ ws_parsing)
  • transcript → transcript_store / recorder / replay / markdown / tool_events
  • ws_http → *_routes families (e.g. settings_routes.py, wiki_routes.py, workspace_routes.py, apps_routes.py, backup_routes.py)

jenny/config/schema.py::SecurityConfig (config.security) is the canonical home for restrict_to_workspace / ssrf_whitelist. Only restrict_to_workspace is mirrored onto ToolsConfig for the tool layer; ssrf_whitelist lives on SecurityConfig alone, so writing tools.ssrf_whitelist is silently dropped. A validator migrates the legacy tools.* locations into security only for a config that has no security block at all — and every config Jenny has saved has one. jenny/config/runtime_env.py is the single layer for operational JENNY_* environment knobs — see Environment variables. See Security model for what these boundaries actually enforce (and don’t).

Python 3.11+, asyncio throughout, 100-character line length. See Code style for the full lint/type-check/language conventions — the short version: ruff (rules E, F, I, N, W; E501 ignored) and Italian docstrings/comments for new code (inherited upstream code stays in English; identifiers, log messages, and commit-facing strings are always English). Run the full check before opening a PR — see Testing.

Contribution flow, the Developer Certificate of Origin sign-off requirement, and licensing are covered in CONTRIBUTING.md at the repository root — read that before your first PR, since CI’s dco job blocks merges on unsigned commits.

Jenny · code underAGPL-3.0 · name and mascot undertrademark

No cookies, no trackers, no analytics. This page loads nothing from a third party.