otto-cli-agent 0.1.1__tar.gz → 0.1.2__tar.gz
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.
- otto_cli_agent-0.1.2/.gitattributes +3 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/CONTRIBUTING.md +1 -1
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/PKG-INFO +19 -10
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/README.md +46 -7
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/README.md +7 -1
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/lessons.py +11 -3
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/main.py +2 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/output.py +4 -2
- otto_cli_agent-0.1.2/agent/cli/serve.py +127 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/sessions.py +3 -5
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/setup_screen.py +4 -17
- otto_cli_agent-0.1.2/agent/config/README.md +15 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/config/envfile.py +27 -8
- otto_cli_agent-0.1.2/agent/config/home.py +74 -0
- otto_cli_agent-0.1.2/agent/embed.py +1030 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/memory/embeddings.py +13 -1
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/memory/lessons.py +222 -31
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/memory/sessions.py +152 -28
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/memory/store.py +18 -3
- otto_cli_agent-0.1.2/agent/phone/README.md +86 -0
- otto_cli_agent-0.1.2/agent/phone/__init__.py +29 -0
- otto_cli_agent-0.1.2/agent/phone/assets/app_notes/com.android.settings.md +2 -0
- otto_cli_agent-0.1.2/agent/phone/assets/app_notes/in.amazon.mShop.android.shopping.md +7 -0
- otto_cli_agent-0.1.2/agent/phone/assets/guard_pages.json +862 -0
- otto_cli_agent-0.1.2/agent/phone/assets/guard_rules.json +121 -0
- otto_cli_agent-0.1.2/agent/phone/backend.py +175 -0
- otto_cli_agent-0.1.2/agent/phone/digest.py +367 -0
- otto_cli_agent-0.1.2/agent/phone/guard.py +753 -0
- otto_cli_agent-0.1.2/agent/phone/notes.py +220 -0
- otto_cli_agent-0.1.2/agent/phone/tools.py +817 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/README.md +18 -2
- otto_cli_agent-0.1.2/agent/pipeline/_python_session_shim.py +298 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/nodes.py +396 -26
- otto_cli_agent-0.1.2/agent/pipeline/profile.py +53 -0
- otto_cli_agent-0.1.2/agent/pipeline/python_session.py +657 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/research.py +11 -2
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/run.py +15 -3
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/toolkit.py +70 -8
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/tools.py +66 -3
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/health.py +32 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/llm_provider/__init__.py +6 -1
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/llm_provider/anthropic_provider.py +2 -1
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/llm_provider/base.py +31 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/llm_provider/inception_provider.py +2 -1
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/llm_provider/openai_provider.py +2 -1
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/outcomes.py +4 -1
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/overrides.py +93 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/router.py +48 -5
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/setup.py +24 -0
- otto_cli_agent-0.1.2/agent/server/README.md +40 -0
- otto_cli_agent-0.1.2/agent/server/__init__.py +9 -0
- otto_cli_agent-0.1.2/agent/server/app.py +743 -0
- otto_cli_agent-0.1.2/agent/server/protocol.py +131 -0
- otto_cli_agent-0.1.2/agent/server/proxy.py +111 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/docs/HISTORY.md +7 -1
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/docs/index.md +1 -1
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/docs/pypi.md +8 -3
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/pyproject.toml +26 -8
- otto_cli_agent-0.1.2/scripts/phone_page_fixture.py +153 -0
- otto_cli_agent-0.1.2/scripts/phone_page_manifest.py +58 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/README.md +2 -2
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/conftest.py +24 -0
- otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_address_form_pincode.json +180 -0
- otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_address_step.json +155 -0
- otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_buy_now_sheet.json +101 -0
- otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_cart.json +1164 -0
- otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_cart_pens.json +1280 -0
- otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_checkout_payment.json +807 -0
- otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_checkout_payment_scrolled.json +1037 -0
- otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_checkout_upsell.json +1736 -0
- otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_filters.json +743 -0
- otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_home.json +1427 -0
- otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_order_review.json +137 -0
- otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_order_review_scrolled.json +116 -0
- otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_product.json +1116 -0
- otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_product_buybox.json +1183 -0
- otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_product_ids.json +157 -0
- otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_results_after_review.json +97 -0
- otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_results_offers.json +1522 -0
- otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_results_search.json +1199 -0
- otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_results_sorted.json +1522 -0
- otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_search_suggestions.json +896 -0
- otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_sign_in.json +99 -0
- otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_sort_price.json +800 -0
- otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_sort_sheet.json +667 -0
- otto_cli_agent-0.1.2/tests/fixtures/phone_pages/amazon_upi_pin_screen.json +40 -0
- otto_cli_agent-0.1.2/tests/fixtures/phone_pages/bank_pin_numpw.json +61 -0
- otto_cli_agent-0.1.2/tests/fixtures/phone_pages/blinkit_cart_pay.json +97 -0
- otto_cli_agent-0.1.2/tests/fixtures/phone_pages/blinkit_search.json +116 -0
- otto_cli_agent-0.1.2/tests/fixtures/phone_pages/chat_with_otp.json +78 -0
- otto_cli_agent-0.1.2/tests/fixtures/phone_pages/phonepe_upi_pin.json +59 -0
- otto_cli_agent-0.1.2/tests/fixtures/phone_pages/settings_display.json +79 -0
- otto_cli_agent-0.1.2/tests/fixtures/phone_pages/shop_onboarding_continue.json +78 -0
- otto_cli_agent-0.1.2/tests/fixtures/phone_pages/shop_order_summary_continue.json +97 -0
- otto_cli_agent-0.1.2/tests/fixtures/phone_pages/shop_otp_boxes.json +147 -0
- otto_cli_agent-0.1.2/tests/fixtures/phone_pages/shop_otp_caption.json +59 -0
- otto_cli_agent-0.1.2/tests/fixtures/phone_pages/shop_qty_total_pay.json +98 -0
- otto_cli_agent-0.1.2/tests/phone_fakes.py +151 -0
- otto_cli_agent-0.1.2/tests/test_background_distil.py +149 -0
- otto_cli_agent-0.1.2/tests/test_embed.py +381 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_embedding_backends.py +18 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_health.py +33 -0
- otto_cli_agent-0.1.2/tests/test_home.py +132 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_lessons.py +92 -1
- otto_cli_agent-0.1.2/tests/test_lessons_cli.py +54 -0
- otto_cli_agent-0.1.2/tests/test_optional_vendor.py +51 -0
- otto_cli_agent-0.1.2/tests/test_phone_app_notes.py +137 -0
- otto_cli_agent-0.1.2/tests/test_phone_backend.py +101 -0
- otto_cli_agent-0.1.2/tests/test_phone_call_budget.py +131 -0
- otto_cli_agent-0.1.2/tests/test_phone_decision.py +395 -0
- otto_cli_agent-0.1.2/tests/test_phone_digest.py +69 -0
- otto_cli_agent-0.1.2/tests/test_phone_do.py +228 -0
- otto_cli_agent-0.1.2/tests/test_phone_element_ids.py +183 -0
- otto_cli_agent-0.1.2/tests/test_phone_fold.py +205 -0
- otto_cli_agent-0.1.2/tests/test_phone_guard.py +278 -0
- otto_cli_agent-0.1.2/tests/test_phone_learned_notes.py +177 -0
- otto_cli_agent-0.1.2/tests/test_phone_page_corpus.py +110 -0
- otto_cli_agent-0.1.2/tests/test_phone_prompt.py +141 -0
- otto_cli_agent-0.1.2/tests/test_phone_seats.py +211 -0
- otto_cli_agent-0.1.2/tests/test_phone_shopping_digest.py +85 -0
- otto_cli_agent-0.1.2/tests/test_phone_tools.py +549 -0
- otto_cli_agent-0.1.2/tests/test_profile.py +84 -0
- otto_cli_agent-0.1.2/tests/test_provider_connection_detail.py +58 -0
- otto_cli_agent-0.1.2/tests/test_python_session.py +837 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_research_workflow.py +13 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_router.py +34 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_run_scoped_toolkit.py +63 -0
- otto_cli_agent-0.1.2/tests/test_seed_lessons.py +105 -0
- otto_cli_agent-0.1.2/tests/test_serve.py +1018 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_sessions.py +83 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/uv.lock +32 -13
- otto_cli_agent-0.1.1/agent/config/README.md +0 -7
- otto_cli_agent-0.1.1/tests/test_lessons_cli.py +0 -20
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/.github/workflows/publish.yml +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/.github/workflows/tests.yml +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/.gitignore +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/.python-version +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/CODE_OF_CONDUCT.md +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/LICENSE +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/SECURITY.md +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/__init__.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/README.md +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/__init__.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/art.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/chat.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/clipboard.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/context.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/doctor.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/errors.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/eval.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/eval_claw.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/eval_compaction.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/eval_hle.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/eval_memory.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/eval_swe.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/modals.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/models.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/route.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/shell.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/tui.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/ui.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/cli/usage_panel.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/config/__init__.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/README.md +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/__init__.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/claw_bench.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/compaction_bench.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/data/README.md +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/data/claw/README.md +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/data/claw/llm_judge-gemini.patch +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/data/claw/otto.yaml +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/failures.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/README.md +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/code_01.json +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/code_02.json +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/code_03.json +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/code_04.json +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/code_05.json +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/code_06.json +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/math_01.json +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/math_02.json +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/math_03.json +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/math_04.json +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/math_05.json +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/math_06.json +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/nphard_gcp_01.json +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/nphard_ksp_01.json +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/nphard_math_binpacking_01.json +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/nphard_math_clique_01.json +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/nphard_math_setcover_01.json +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/nphard_math_subsetsum_01.json +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/nphard_tsp_01.json +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/golden/nphard_tsp_02.json +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/hle_bench.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/langfuse_sync.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/memory_bench.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/runner.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/single_agent.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/swe_bench.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/eval/terminal_bench.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/memory/README.md +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/memory/__init__.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/memory/hashing.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/memory/queue.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/memory/retrieval.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/memory/session.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/memory/tokens.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/memory/wiring.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/__init__.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/browsing.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/budget.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/codemap.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/evidence.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/execution.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/modes.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/native.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/pricing.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/progress.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/rag.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/screen.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/state.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/tracing.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/usage.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/vision.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/walkthrough.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/pipeline/workspace.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/README.md +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/__init__.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/automap.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/llm_provider/README.md +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/llm_provider/custom.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/llm_provider/gemini_provider.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/llm_provider/retired.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/llm_provider/temperature.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/mapping.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/agent/router/reload.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/containers/README.md +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/containers/otto-desktop/Dockerfile +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/containers/otto-desktop/README.md +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/containers/otto-desktop/start-desktop.sh +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/debug_pipeline.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/docs/README.md +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/docs/RESEARCH.md +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/docs/_config.yml +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/docs/design/README.md +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/docs/design/tiered-memory.md +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/docs/media/codebase-code-map.png +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/docs/media/exercise-todo-cli.png +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/docs/media/otto-demo.gif +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/docs/media/palette-doctor-route.png +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/docs/media/run-trace-and-judge.png +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/docs/media/session-resumed-thinking.png +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/docs/media/sessions-picker.png +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/docs/media/setup-screen.png +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/docs/media/social-preview.png +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_agent_loop.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_art.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_ask_user_node.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_automap.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_browsing.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_budget.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_call_budget.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_chat_fast_path.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_claw_bench.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_clipboard.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_codemap.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_compaction_bench.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_custom_endpoints.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_diffusion_retry.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_envfile.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_eval_honesty.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_eval_runner.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_evaluator_node.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_evicted_context.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_evidence.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_failures.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_hle_bench.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_inception_provider.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_inception_stream.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_langfuse_sync.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_mapping.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_memory_bench.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_memory_embeddings.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_memory_hashing.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_memory_queue.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_memory_retrieval.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_memory_session.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_memory_store.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_memory_tokens.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_memory_wiring.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_model_policies.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_modes.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_native.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_no_import_shadowing.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_output.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_overrides.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_pipeline_nodes.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_pipeline_run.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_progress.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_prompt_tool_sync.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_reload.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_research_router.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_retrieval_method.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_screen.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_seat_outcomes.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_setup.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_swe_bench.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_temperature_learning.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_terminal_bench_adapter.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_tool_loop.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_tools_stubs.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_tracing.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_tui.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_tui_progress.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_tui_setup.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_usage.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_vision_tool.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_walkthrough.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_workspace_session.py +0 -0
- {otto_cli_agent-0.1.1 → otto_cli_agent-0.1.2}/tests/test_workspace_tools.py +0 -0
|
@@ -7,7 +7,7 @@ built to: say what you measured, or what you would need to measure.
|
|
|
7
7
|
|
|
8
8
|
```bash
|
|
9
9
|
uv sync
|
|
10
|
-
uv run pytest -q # 1,
|
|
10
|
+
uv run pytest -q # 1,593 tests, no API key, no network, about two minutes
|
|
11
11
|
uv run otto doctor # once you have keys in .env
|
|
12
12
|
```
|
|
13
13
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: otto-cli-agent
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Otto: a terminal AI agent built around measured results -- one agent loop, a rubric-first evaluator, tiered memory, multi-vendor routing, and six benchmark harnesses.
|
|
5
5
|
Project-URL: Repository, https://github.com/siddharth23P/otto_agent
|
|
6
6
|
Project-URL: Issues, https://github.com/siddharth23P/otto_agent/issues
|
|
@@ -38,24 +38,28 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
|
38
38
|
Classifier: Topic :: Software Development
|
|
39
39
|
Requires-Python: >=3.12
|
|
40
40
|
Requires-Dist: emoji>=2.15.0
|
|
41
|
-
Requires-Dist: fastembed>=0.8.0
|
|
42
41
|
Requires-Dist: inceptionai>=0.1.3
|
|
43
42
|
Requires-Dist: langchain-anthropic>=1.7.1
|
|
44
43
|
Requires-Dist: langchain-google-genai>=4.4.0
|
|
45
44
|
Requires-Dist: langchain-openai>=1.6.0
|
|
46
45
|
Requires-Dist: langchain>=1.4.0
|
|
47
46
|
Requires-Dist: langfuse>=4.15.1
|
|
48
|
-
Requires-Dist: langgraph-cli[inmem]>=0.4.31
|
|
49
47
|
Requires-Dist: langgraph>=1.2.11
|
|
50
|
-
Requires-Dist: numpy>=
|
|
51
|
-
Requires-Dist: openpyxl>=3.1.5
|
|
48
|
+
Requires-Dist: numpy>=1.26.2
|
|
49
|
+
Requires-Dist: openpyxl>=3.1.5; sys_platform != 'android'
|
|
52
50
|
Requires-Dist: prompt-toolkit>=3.0.53
|
|
53
|
-
Requires-Dist: python-docx>=1.1.2
|
|
51
|
+
Requires-Dist: python-docx>=1.1.2; sys_platform != 'android'
|
|
54
52
|
Requires-Dist: python-dotenv>=1.2.3
|
|
55
|
-
Requires-Dist: reportlab>=4.2
|
|
53
|
+
Requires-Dist: reportlab>=4.2; sys_platform != 'android'
|
|
56
54
|
Requires-Dist: textual>=8.2.8
|
|
57
55
|
Requires-Dist: tiktoken>=0.14.0
|
|
58
56
|
Requires-Dist: typer>=0.27.2
|
|
57
|
+
Provides-Extra: dev-server
|
|
58
|
+
Requires-Dist: langgraph-cli[inmem]>=0.4.31; extra == 'dev-server'
|
|
59
|
+
Provides-Extra: local-embeddings
|
|
60
|
+
Requires-Dist: fastembed>=0.8.0; extra == 'local-embeddings'
|
|
61
|
+
Provides-Extra: serve
|
|
62
|
+
Requires-Dist: websockets>=13; extra == 'serve'
|
|
59
63
|
Description-Content-Type: text/markdown
|
|
60
64
|
|
|
61
65
|
# Otto
|
|
@@ -63,19 +67,24 @@ Description-Content-Type: text/markdown
|
|
|
63
67
|
A terminal AI agent that works on a codebase, a container, a browser or a
|
|
64
68
|
desktop, uses what it built, and judges its own work against criteria it
|
|
65
69
|
wrote before it started. One agent loop with modes over four model vendors,
|
|
66
|
-
a rubric-first evaluator, tiered memory, six benchmark harnesses, and 1,
|
|
67
|
-
tests that need no API key.
|
|
70
|
+
a rubric-first evaluator, tiered memory, six benchmark harnesses, and 1,685
|
|
71
|
+
tests that need no API key. It embeds: `agent.embed` is the surface an app
|
|
72
|
+
with Otto's Python inside it depends on.
|
|
68
73
|
|
|
69
74
|

|
|
70
75
|
|
|
71
76
|
## Install
|
|
72
77
|
|
|
73
78
|
```bash
|
|
74
|
-
pip install otto-cli-agent
|
|
79
|
+
pip install "otto-cli-agent[local-embeddings]"
|
|
75
80
|
otto doctor
|
|
76
81
|
otto tui
|
|
77
82
|
```
|
|
78
83
|
|
|
84
|
+
The `local-embeddings` extra is the on-device embedding model; without it
|
|
85
|
+
recall uses a Gemini key when there is one, and recency otherwise. `[serve]`
|
|
86
|
+
adds `otto serve`, the agent behind a WebSocket for the phone app.
|
|
87
|
+
|
|
79
88
|
Put keys in a `.env` in the directory you run it from. `INCEPTION_API_KEY`
|
|
80
89
|
is required (it alone serves the fill-in-the-middle and edit endpoints);
|
|
81
90
|
`OPENAI_API_KEY`, `ANTHROPIC_API_KEY` and `GEMINI_API_KEY` are optional and
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|

|
|
5
5
|

|
|
6
6
|
[](https://pypi.org/project/otto-cli-agent/)
|
|
7
|
-

|
|
8
8
|
|
|
9
9
|
Otto is a terminal AI agent that works on a codebase, a container, a browser
|
|
10
10
|
or a desktop, uses what it built, and judges its own work against criteria it
|
|
@@ -88,10 +88,12 @@ transcript without spending a turn.
|
|
|
88
88
|
## Install
|
|
89
89
|
|
|
90
90
|
```bash
|
|
91
|
-
pip install otto-cli-agent
|
|
91
|
+
pip install "otto-cli-agent[local-embeddings]"
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
-
Python 3.12 or newer. The
|
|
94
|
+
Python 3.12 or newer. The `local-embeddings` extra is the on-device
|
|
95
|
+
embedding model (fastembed); without it, memory recall uses a Gemini key when
|
|
96
|
+
there is one and plain recency otherwise. `[serve]` adds `otto serve`. The wheel and sdist are also attached to every [release](https://github.com/siddharth23P/otto_agent/releases).
|
|
95
97
|
|
|
96
98
|
## Quick start
|
|
97
99
|
|
|
@@ -117,6 +119,7 @@ uv run otto chat # the same pipeline at a prompt
|
|
|
117
119
|
| --- | --- |
|
|
118
120
|
| `otto tui` / `otto chat` | interactive sessions; `--workspace PATH`, `--no-workspace`, `--resume <id\|prefix\|last>` |
|
|
119
121
|
| `otto sessions` | list, `--delete`, `--rename`, `--export`, `--import`, `--prune` |
|
|
122
|
+
| `otto serve` | the agent behind a WebSocket for the phone app; `--host`, `--port`, `--token`, `--qr`, `--allow-origin`, `--no-exec` |
|
|
120
123
|
| `otto doctor` | provider and route health, exit 2 on a missing required key |
|
|
121
124
|
| `otto models` | every model each configured vendor lists, with detected capabilities |
|
|
122
125
|
| `otto route <task>` | the fallback chain for a seat, pins starred, observed outcomes shown |
|
|
@@ -130,11 +133,17 @@ uv run otto chat # the same pipeline at a prompt
|
|
|
130
133
|
|
|
131
134
|
Environment variables: `OTTO_MAX_MODEL_CALLS` (per-turn ceiling, default 120),
|
|
132
135
|
`OTTO_COMMAND_TIMEOUT` (120 s with a workspace, 10 s without),
|
|
136
|
+
`OTTO_PYTHON_SESSION=0` (a fresh process per `execute_python` call instead of
|
|
137
|
+
one interpreter per run), `OTTO_PYTHON_SESSION_MEMORY_MB` (that interpreter's
|
|
138
|
+
address-space ceiling on Linux, default 8192, 0 for none),
|
|
133
139
|
`OTTO_EMBEDDING_MODEL` (`provider:model`; local BGE is the floor),
|
|
134
140
|
`OTTO_MODEL_PRICES` (a JSON file that overrides the price table),
|
|
135
141
|
`OTTO_IGNORE_ROUTES=1` (use the shipped routing table untouched; evals do),
|
|
136
142
|
`OTTO_BROWSER_PYTHON` (an interpreter with Playwright and `pyte`, which
|
|
137
|
-
enables the browser and terminal tools), `
|
|
143
|
+
enables the browser and terminal tools), `OTTO_HOME` (where state lives,
|
|
144
|
+
default `~/.otto`), `OTTO_ENV_FILE` (the `.env` to load and write),
|
|
145
|
+
`OTTO_OUTPUT_DIR`, `OTTO_SERVE_TOKEN` (the `otto serve` pairing secret),
|
|
146
|
+
`OTTO_NO_ANIMATION=1`, `OTTO_THEME`.
|
|
138
147
|
|
|
139
148
|
Otto ships no browser. To let it load the pages and terminal programs it
|
|
140
149
|
writes, install Playwright and `pyte` into any Python once and point
|
|
@@ -202,11 +211,41 @@ The pieces, each documented in its own folder:
|
|
|
202
211
|
| [agent/router](agent/router/README.md) | task seats, the routing table, provider adapters, learned ordering, health, pins, temperature policy |
|
|
203
212
|
| [agent/cli](agent/cli/README.md) | the TUI, the REPL, sessions, setup, and every `otto` command |
|
|
204
213
|
| [agent/eval](agent/eval/README.md) | the six benchmark harnesses, the failure taxonomy, the single-agent control |
|
|
205
|
-
| [agent/config](agent/config/README.md) | the one `.env` file Otto reads and writes |
|
|
214
|
+
| [agent/config](agent/config/README.md) | the one `.env` file Otto reads and writes, and `OTTO_HOME` |
|
|
215
|
+
| [agent/phone](agent/phone/README.md) | the phone tools, the screen digest and the money guard behind the Android app |
|
|
216
|
+
| [agent/server](agent/server/README.md) | `otto serve`, the agent over a WebSocket |
|
|
206
217
|
| [containers](containers/README.md) | the throwaway desktop image the screen tools drive |
|
|
207
|
-
| [tests](tests/README.md) | 1,
|
|
218
|
+
| [tests](tests/README.md) | 1,685 tests that need no key and no network |
|
|
208
219
|
| [docs](docs/README.md) | the development log, the research sources, the memory design |
|
|
209
220
|
|
|
221
|
+
## Embedding Otto
|
|
222
|
+
|
|
223
|
+
Otto is also a library. `agent/embed.py` is the surface a host depends on --
|
|
224
|
+
an Android app with Otto's Python inside it is the case it was built for --
|
|
225
|
+
and it is deliberately small:
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
from agent import embed
|
|
229
|
+
|
|
230
|
+
embed.configure("/data/data/dev.otto.phone/files/otto") # before anything else is imported
|
|
231
|
+
embed.set_key("INCEPTION_API_KEY", "...") # written masked; or pass environ={...}
|
|
232
|
+
runtime = embed.Runtime() # imports the pipeline, once
|
|
233
|
+
session = runtime.open_session()
|
|
234
|
+
session.run("make my font bigger", events=print,
|
|
235
|
+
tools=phone_tools(backend), guidance=PHONE_GUIDANCE,
|
|
236
|
+
disabled_tools=PHONE_DISABLED_STANDING_TOOLS) # blocks; ask/answer from another thread
|
|
237
|
+
# Keys given through configure(environ=...) switch execute_bash/execute_python off unless
|
|
238
|
+
# disabled_tools says otherwise: a subprocess inherits the environment, keys included.
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Events are plain dicts (`progress`, `board`, `ask`, `final`, `error`), a run
|
|
242
|
+
pauses inside `run()` until `answer()` arrives, and `cancel()` stops it within
|
|
243
|
+
one model call. Keys live in the process environment, so one process is one
|
|
244
|
+
person's Otto; a host serving several people runs several processes. `API_VERSION` says which contract you have. The phone tools,
|
|
245
|
+
the screen digest and the money guard are [agent/phone](agent/phone/README.md);
|
|
246
|
+
`otto serve` puts the same runtime behind a WebSocket for a client that has
|
|
247
|
+
the hands ([agent/server](agent/server/README.md)).
|
|
248
|
+
|
|
210
249
|
## Evaluation
|
|
211
250
|
|
|
212
251
|
Six harnesses, each grading by something outside the model: a checker, a
|
|
@@ -219,7 +258,7 @@ enforce: [agent/eval/README.md](agent/eval/README.md).
|
|
|
219
258
|
|
|
220
259
|
## Testing
|
|
221
260
|
|
|
222
|
-
1,
|
|
261
|
+
1,685 tests pass and 12 skip on macOS, Linux and Windows in a few minutes,
|
|
223
262
|
with no API keys and no network. Tests assert on the messages handed to the
|
|
224
263
|
model, on the exact inputs that broke real runs, on call counts against the
|
|
225
264
|
real compiled graph, and directly on the library behaviours the code relies
|
|
@@ -10,7 +10,13 @@ its own README:
|
|
|
10
10
|
| [router/](router/README.md) | which model serves which kind of work, across four vendors |
|
|
11
11
|
| [cli/](cli/README.md) | the TUI, the REPL and every `otto` sub-command |
|
|
12
12
|
| [eval/](eval/README.md) | the benchmark harnesses |
|
|
13
|
-
| [config/](config/README.md) | the `.env` file |
|
|
13
|
+
| [config/](config/README.md) | the `.env` file, and where Otto's state lives |
|
|
14
|
+
| [phone/](phone/README.md) | the phone as a place Otto works: the backend Protocol, the screen digest, the money guard, the phone tools |
|
|
15
|
+
| [server/](server/README.md) | `otto serve`: the agent behind a WebSocket for a client that has the hands |
|
|
16
|
+
|
|
17
|
+
Beside them, one module: `embed.py`, the surface an embedding host (the
|
|
18
|
+
Android app) depends on -- `configure()`, keys, `Runtime`, `SessionHandle` --
|
|
19
|
+
versioned by `API_VERSION` and deliberately narrow.
|
|
14
20
|
|
|
15
21
|
Dependency direction, enforced by the modules' own docstrings and a few
|
|
16
22
|
tests: `memory/` imports nothing from `pipeline/` or `router/`, so it never
|
|
@@ -14,6 +14,9 @@ rubbish" is how that gets noticed, and `--clear` is how it gets undone.
|
|
|
14
14
|
through the same duplicate adjudication a run's own lessons face. The TUI's
|
|
15
15
|
"Export lessons…" / "Import lessons…" palette entries call the same two
|
|
16
16
|
functions (agent/memory/lessons.py).
|
|
17
|
+
|
|
18
|
+
`--phone` (2026-09-15) does any of the above to the lessons phone runs keep
|
|
19
|
+
apart from the workspace's (agent/memory/lessons.py PHONE_KIND).
|
|
17
20
|
"""
|
|
18
21
|
from __future__ import annotations
|
|
19
22
|
|
|
@@ -28,7 +31,8 @@ from typing_extensions import Annotated
|
|
|
28
31
|
|
|
29
32
|
from agent.cli.ui import err, out
|
|
30
33
|
from agent.memory.lessons import (
|
|
31
|
-
Lesson, all_lessons, bank_path, bind_bank, clear_bank, export_lessons,
|
|
34
|
+
KIND, PHONE_KIND, Lesson, all_lessons, bank_path, bind_bank, bind_kind, clear_bank, export_lessons,
|
|
35
|
+
import_lessons,
|
|
32
36
|
)
|
|
33
37
|
from agent.memory.store import MemoryStore
|
|
34
38
|
|
|
@@ -69,6 +73,10 @@ def lessons_cmd(
|
|
|
69
73
|
bool,
|
|
70
74
|
typer.Option("--replace", help="With --import: empty the bank first."),
|
|
71
75
|
] = False,
|
|
76
|
+
phone: Annotated[
|
|
77
|
+
bool,
|
|
78
|
+
typer.Option("--phone", help="The lessons phone runs learn and read, instead of the workspace's."),
|
|
79
|
+
] = False,
|
|
72
80
|
) -> None:
|
|
73
81
|
path = bank or bank_path()
|
|
74
82
|
if import_from is None and not path.exists():
|
|
@@ -76,7 +84,7 @@ def lessons_cmd(
|
|
|
76
84
|
return
|
|
77
85
|
|
|
78
86
|
store = MemoryStore(path)
|
|
79
|
-
with bind_bank(store):
|
|
87
|
+
with bind_bank(store), bind_kind(PHONE_KIND if phone else KIND):
|
|
80
88
|
if clear:
|
|
81
89
|
removed = clear_bank()
|
|
82
90
|
err.print(f"cleared {removed} lesson(s) from {path}")
|
|
@@ -92,6 +100,6 @@ def lessons_cmd(
|
|
|
92
100
|
|
|
93
101
|
learned = all_lessons()
|
|
94
102
|
if not learned:
|
|
95
|
-
out.print(f"{path} is empty")
|
|
103
|
+
out.print(f"{path} has no phone lessons yet" if phone else f"{path} is empty")
|
|
96
104
|
return
|
|
97
105
|
out.print(lessons_table(learned, path))
|
|
@@ -34,6 +34,7 @@ from agent.cli import lessons as lessons_cmd
|
|
|
34
34
|
from agent.cli import models as model_cmd
|
|
35
35
|
from agent.cli import route as route_cmd
|
|
36
36
|
from agent.cli import sessions as sessions_cmd
|
|
37
|
+
from agent.cli import serve as serve_cmd
|
|
37
38
|
from agent.cli import chat as chat_cmd
|
|
38
39
|
from agent.cli import tui as tui_cmd
|
|
39
40
|
|
|
@@ -60,6 +61,7 @@ for _name, _fn in (
|
|
|
60
61
|
("eval-swe", eval_swe_cmd.eval_swe_cmd),
|
|
61
62
|
("lessons", lessons_cmd.lessons_cmd),
|
|
62
63
|
("sessions", sessions_cmd.sessions_cmd),
|
|
64
|
+
("serve", serve_cmd.serve),
|
|
63
65
|
):
|
|
64
66
|
app.command(_name)(friendly(_fn))
|
|
65
67
|
|
|
@@ -10,13 +10,15 @@ CodeTask, right after rendering the panel.
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
+
import os
|
|
13
14
|
from pathlib import Path
|
|
14
15
|
|
|
15
16
|
#: Relative to wherever `otto chat`/`otto tui` is launched from -- next to
|
|
16
17
|
#: the project it's working on, not buried in ~/.otto with the ledger/profile
|
|
17
18
|
#: (10.1/10.2), since this is meant to be opened by a person, not read back
|
|
18
|
-
#: by Otto itself.
|
|
19
|
-
|
|
19
|
+
#: by Otto itself. OTTO_OUTPUT_DIR overrides it for a host with no useful
|
|
20
|
+
#: working directory (an embedded run; agent/config/home.py).
|
|
21
|
+
OUTPUT_DIR = Path(os.environ.get("OTTO_OUTPUT_DIR", "").strip() or Path.cwd() / "otto_output")
|
|
20
22
|
|
|
21
23
|
#: classify()'s LANGUAGE line is free text in plain English ("python",
|
|
22
24
|
#: "javascript"), not an enum -- mapped here to the extension a person would
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""`otto serve`: the agent behind a WebSocket for the phone app (agent/server/)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import ipaddress
|
|
6
|
+
import os
|
|
7
|
+
import secrets
|
|
8
|
+
from typing import Annotated, Optional
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from agent.cli.ui import err, out
|
|
13
|
+
|
|
14
|
+
TOKEN_ENV = "OTTO_SERVE_TOKEN"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def pairing_url(host: str, port: int) -> str:
|
|
18
|
+
shown = "127.0.0.1" if host in ("", "0.0.0.0", "::") else host
|
|
19
|
+
return f"ws://{shown}:{port}/"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def is_loopback(host: str) -> bool:
|
|
23
|
+
"""Whether `host` only accepts connections from this computer."""
|
|
24
|
+
host = (host or "").strip().strip("[]")
|
|
25
|
+
if host.lower() == "localhost":
|
|
26
|
+
return True
|
|
27
|
+
try:
|
|
28
|
+
return ipaddress.ip_address(host).is_loopback
|
|
29
|
+
except ValueError:
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def exposure_warning(host: str, *, no_exec: bool = False) -> str | None:
|
|
34
|
+
"""What to print when the server listens beyond loopback, or None.
|
|
35
|
+
|
|
36
|
+
A turn the phone is not needed for runs like `otto tui` -- shell and
|
|
37
|
+
Python on this computer included (2026-09-15, decided with the person who
|
|
38
|
+
runs it) -- so the pairing token is no longer only "talk to the agent".
|
|
39
|
+
Whoever holds it on a network this reaches can run commands here. The
|
|
40
|
+
phone does not need the network for this: `adb reverse` carries a
|
|
41
|
+
loopback server over USB."""
|
|
42
|
+
if is_loopback(host):
|
|
43
|
+
return None
|
|
44
|
+
grants = ("read and write files in otto's workspaces on this computer" if no_exec
|
|
45
|
+
else "run commands and read and write files on this computer")
|
|
46
|
+
return (f"otto serve is listening on {host or 'every interface'}, beyond this computer: anyone "
|
|
47
|
+
f"with the pairing token can {grants}. Prefer --host 127.0.0.1 with "
|
|
48
|
+
"`adb reverse tcp:8765 tcp:8765`" + ("" if no_exec else ", or add --no-exec") + ".")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def ensure_token(explicit: str | None) -> str:
|
|
52
|
+
"""The token from --token, else OTTO_SERVE_TOKEN, else a new one written
|
|
53
|
+
to the env file so the app stays paired across restarts."""
|
|
54
|
+
if explicit:
|
|
55
|
+
return explicit
|
|
56
|
+
existing = os.environ.get(TOKEN_ENV, "").strip()
|
|
57
|
+
if existing:
|
|
58
|
+
return existing
|
|
59
|
+
from agent.config import envfile
|
|
60
|
+
|
|
61
|
+
token = secrets.token_urlsafe(24)
|
|
62
|
+
envfile.set_value(TOKEN_ENV, token)
|
|
63
|
+
return token
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _log_phone_actions() -> None:
|
|
67
|
+
"""Each phone tool call and what came of it, on stderr (agent/phone/tools.py logs them): what a
|
|
68
|
+
person reads when a run went back and forth. Typed text is never logged."""
|
|
69
|
+
import logging
|
|
70
|
+
|
|
71
|
+
phone = logging.getLogger("agent.phone")
|
|
72
|
+
if not any(getattr(h, "_otto_serve", False) for h in phone.handlers):
|
|
73
|
+
handler = logging.StreamHandler()
|
|
74
|
+
handler.setFormatter(logging.Formatter("%(asctime)s phone %(message)s", "%H:%M:%S"))
|
|
75
|
+
handler._otto_serve = True # type: ignore[attr-defined]
|
|
76
|
+
phone.addHandler(handler)
|
|
77
|
+
phone.setLevel(logging.INFO)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def serve(
|
|
81
|
+
host: Annotated[str, typer.Option(help="Interface to listen on; loopback unless you mean otherwise.")] = "127.0.0.1",
|
|
82
|
+
port: Annotated[int, typer.Option(help="Port to listen on.")] = 8765,
|
|
83
|
+
token: Annotated[Optional[str], typer.Option(help=f"Shared secret the app presents; default {TOKEN_ENV} or a generated one.")] = None,
|
|
84
|
+
qr: Annotated[bool, typer.Option("--qr", help="Print a pairing QR code (needs the qrcode package).")] = False,
|
|
85
|
+
allow_origin: Annotated[Optional[list[str]], typer.Option(
|
|
86
|
+
"--allow-origin", help="A browser origin allowed to connect (repeatable). Pages are refused otherwise.")] = None,
|
|
87
|
+
no_exec: Annotated[bool, typer.Option(
|
|
88
|
+
"--no-exec", help="No shell or Python for any turn: the app's answers stay in the workspace.")] = False,
|
|
89
|
+
allow_remote_setup: Annotated[bool, typer.Option(
|
|
90
|
+
"--allow-remote-setup", help="Take key and routing changes from a client not on this computer.")] = False,
|
|
91
|
+
) -> None:
|
|
92
|
+
"""Serve the agent over a WebSocket for the phone app."""
|
|
93
|
+
try:
|
|
94
|
+
import websockets # noqa: F401
|
|
95
|
+
except ImportError:
|
|
96
|
+
err.print("[bad]otto serve needs the websockets package: pip install 'otto-cli-agent[serve]'[/]")
|
|
97
|
+
raise typer.Exit(2)
|
|
98
|
+
from agent import embed
|
|
99
|
+
from agent.config.envfile import ENV_PATH
|
|
100
|
+
from agent.config.home import otto_home
|
|
101
|
+
from agent.server.app import OttoServer
|
|
102
|
+
|
|
103
|
+
embed.configure(otto_home(), env_file=ENV_PATH)
|
|
104
|
+
_log_phone_actions()
|
|
105
|
+
secret = ensure_token(token)
|
|
106
|
+
url = pairing_url(host, port)
|
|
107
|
+
payload = f"{url}#{secret}"
|
|
108
|
+
err.print(f"[muted]otto serve[/] listening on [bold]{url}[/]")
|
|
109
|
+
err.print(f"[muted]pair the app with[/] {payload}")
|
|
110
|
+
if (warning := exposure_warning(host, no_exec=no_exec)) is not None:
|
|
111
|
+
err.print(f"[warn]{warning}[/]", soft_wrap=True)
|
|
112
|
+
if qr:
|
|
113
|
+
try:
|
|
114
|
+
import qrcode
|
|
115
|
+
|
|
116
|
+
code = qrcode.QRCode(border=1)
|
|
117
|
+
code.add_data(payload)
|
|
118
|
+
code.print_ascii(invert=True)
|
|
119
|
+
except ImportError:
|
|
120
|
+
err.print("[warn]--qr needs `pip install qrcode`; the URL above pairs the same way[/]")
|
|
121
|
+
if not embed.ready():
|
|
122
|
+
err.print("[warn]INCEPTION_API_KEY is not set; turns will fail until it is (otto tui -> Setup)[/]")
|
|
123
|
+
try:
|
|
124
|
+
asyncio.run(OttoServer(secret, allowed_origins=tuple(allow_origin or ()),
|
|
125
|
+
no_exec=no_exec, allow_remote_setup=allow_remote_setup).run(host, port))
|
|
126
|
+
except KeyboardInterrupt:
|
|
127
|
+
out.print("[muted]bye[/]")
|
|
@@ -7,7 +7,6 @@ index from outside a session. Resuming is `otto chat --resume` / `otto tui
|
|
|
7
7
|
"""
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
from datetime import date
|
|
11
10
|
from pathlib import Path
|
|
12
11
|
from typing import Optional
|
|
13
12
|
|
|
@@ -39,10 +38,9 @@ def sessions_table(rows: list[SessionInfo], current: str | None = None) -> Table
|
|
|
39
38
|
|
|
40
39
|
|
|
41
40
|
def default_export_path(info: SessionInfo, directory: Path | None = None) -> Path:
|
|
42
|
-
"""`
|
|
43
|
-
default; the TUI proposes the home directory)
|
|
44
|
-
|
|
45
|
-
return (directory or Path.cwd()) / f"otto-session-{info.short_id}-{date.today():%Y-%m-%d}.json"
|
|
41
|
+
"""agent/memory/sessions.py's `export_filename`, in `directory` (the
|
|
42
|
+
current one by default; the TUI proposes the home directory)."""
|
|
43
|
+
return (directory or Path.cwd()) / session_index.export_filename(info)
|
|
46
44
|
|
|
47
45
|
|
|
48
46
|
def sessions_cmd(
|
|
@@ -48,11 +48,13 @@ from agent.cli import art
|
|
|
48
48
|
from agent.cli.art import animations_enabled
|
|
49
49
|
from agent.router.llm_provider.base import ModelInfo, ProviderStatus
|
|
50
50
|
from agent.router.mapping import Candidate, Task
|
|
51
|
-
from agent.router.overrides import PROVIDER_ONLY
|
|
52
51
|
|
|
53
52
|
__all__ = ["SetupBackend", "SetupScreen", "MappingRow", "pin_options", "NO_PIN"]
|
|
54
53
|
|
|
55
|
-
|
|
54
|
+
# pin_options and NO_PIN live in agent/router/setup.py, beside the rest of the
|
|
55
|
+
# setup data layer, so `otto serve` offers the same choices; re-exported here
|
|
56
|
+
# for the screen and everything that imported them from it.
|
|
57
|
+
from agent.router.setup import NO_PIN, pin_options # noqa: E402
|
|
56
58
|
CUSTOM = "__custom__"
|
|
57
59
|
|
|
58
60
|
#: Status word -> primitive Rich style. agent/cli/doctor.py's map, without
|
|
@@ -86,21 +88,6 @@ class SetupBackend:
|
|
|
86
88
|
reload: Callable[[], Any]
|
|
87
89
|
|
|
88
90
|
|
|
89
|
-
def pin_options(task: Task, models: list[ModelInfo],
|
|
90
|
-
routes: Mapping[Task, tuple[Candidate, ...]]) -> list[tuple[str, str]]:
|
|
91
|
-
"""The Select options for one task: no pin, then every model the seat
|
|
92
|
-
could use -- the head candidate's `requires`, narrowed to the provider a
|
|
93
|
-
task is bound to (agent/router/overrides.py PROVIDER_ONLY)."""
|
|
94
|
-
head = routes[task][0]
|
|
95
|
-
bound = PROVIDER_ONLY.get(task)
|
|
96
|
-
eligible = [
|
|
97
|
-
m for m in models
|
|
98
|
-
if head.requires <= m.capabilities and (bound is None or m.provider == bound[0])
|
|
99
|
-
]
|
|
100
|
-
eligible.sort(key=lambda m: (m.provider, m.id))
|
|
101
|
-
return [NO_PIN] + [(m.spec, m.spec) for m in eligible]
|
|
102
|
-
|
|
103
|
-
|
|
104
91
|
class MappingRow(Horizontal):
|
|
105
92
|
"""One task: its name, what resolves now, the proposal, and the pick."""
|
|
106
93
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# agent/config/
|
|
2
|
+
|
|
3
|
+
`envfile.py` reads and writes the one `.env` file Otto loads, at the
|
|
4
|
+
repository root. It is what the setup screen writes keys and named endpoints
|
|
5
|
+
into, and what `agent/cli/main.py` loads at start. Keys are only ever shown
|
|
6
|
+
masked; a named custom endpoint `local` becomes `LOCAL_API_KEY` and
|
|
7
|
+
`LOCAL_BASE_URL`. Nothing else in the package reads the file directly.
|
|
8
|
+
|
|
9
|
+
`home.py` says where that file is and where every per-installation file
|
|
10
|
+
goes: `OTTO_HOME` (default `~/.otto`) for the memory stores, the session
|
|
11
|
+
index, the outcome log, routes and lessons; `OTTO_ENV_FILE` for the `.env`
|
|
12
|
+
(default: the checkout's, or `<OTTO_HOME>/.env` for an installed wheel).
|
|
13
|
+
Both are read when the state modules are imported, so an embedding host sets
|
|
14
|
+
them first -- `agent/embed.py`'s `configure()` does. `OTTO_OUTPUT_DIR` moves
|
|
15
|
+
`otto_output/` the same way.
|
|
@@ -20,10 +20,13 @@ from pathlib import Path
|
|
|
20
20
|
|
|
21
21
|
from dotenv import set_key, unset_key
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
#:
|
|
26
|
-
|
|
23
|
+
from agent.config.home import env_file
|
|
24
|
+
|
|
25
|
+
#: The file agent/cli/main.py loads at import: the repository root's `.env`
|
|
26
|
+
#: from a checkout, `$OTTO_ENV_FILE` when set, `<OTTO_HOME>/.env` for an
|
|
27
|
+
#: installed wheel (agent/config/home.py). One definition, imported by
|
|
28
|
+
#: main.py, so the writer and the loader cannot drift apart.
|
|
29
|
+
ENV_PATH: Path = env_file()
|
|
27
30
|
|
|
28
31
|
KEY_RE = re.compile(r"^[A-Z][A-Z0-9_]*$")
|
|
29
32
|
|
|
@@ -42,7 +45,7 @@ def present(key: str) -> bool:
|
|
|
42
45
|
return bool(os.environ.get(key))
|
|
43
46
|
|
|
44
47
|
|
|
45
|
-
def set_value(key: str, value: str, *, path: Path =
|
|
48
|
+
def set_value(key: str, value: str, *, path: Path | None = None) -> str:
|
|
46
49
|
"""Write `KEY=value` to `path` and into `os.environ`. Returns the masked
|
|
47
50
|
value, which is all a caller should ever display.
|
|
48
51
|
|
|
@@ -55,22 +58,38 @@ def set_value(key: str, value: str, *, path: Path = ENV_PATH) -> str:
|
|
|
55
58
|
if not value:
|
|
56
59
|
unset_value(key, path=path)
|
|
57
60
|
return masked(None)
|
|
58
|
-
|
|
61
|
+
# Resolved at call time, not bound as a default: a default would freeze
|
|
62
|
+
# the path at import, and both the tests and an embedding host that
|
|
63
|
+
# reassigns ENV_PATH would silently write somewhere else.
|
|
64
|
+
path = Path(ENV_PATH if path is None else path)
|
|
59
65
|
if not path.exists():
|
|
60
66
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
61
67
|
path.touch(mode=0o600)
|
|
62
68
|
set_key(str(path), key, value, quote_mode="auto")
|
|
69
|
+
_private(path)
|
|
63
70
|
os.environ[key] = value
|
|
64
71
|
return masked(value)
|
|
65
72
|
|
|
66
73
|
|
|
67
|
-
def
|
|
74
|
+
def _private(path: Path) -> None:
|
|
75
|
+
"""Owner-only, on every write and not only on creation: the file may
|
|
76
|
+
have been made by a host app or by hand with a wider mode, and since
|
|
77
|
+
0.1.2 it also carries the `otto serve` pairing token. Windows has no
|
|
78
|
+
such mode; a failure to set it is not a failure to write."""
|
|
79
|
+
try:
|
|
80
|
+
os.chmod(path, 0o600)
|
|
81
|
+
except OSError:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def unset_value(key: str, *, path: Path | None = None) -> None:
|
|
68
86
|
"""Remove `key` from `path` (if present) and from `os.environ`."""
|
|
69
87
|
if not KEY_RE.match(key or ""):
|
|
70
88
|
raise ValueError(f"{key!r} is not a valid environment variable name")
|
|
71
|
-
path = Path(path)
|
|
89
|
+
path = Path(ENV_PATH if path is None else path)
|
|
72
90
|
if path.exists():
|
|
73
91
|
# unset_key logs a warning and returns (None, key) when the key is
|
|
74
92
|
# absent; neither is an error for a caller that wants it gone.
|
|
75
93
|
unset_key(str(path), key)
|
|
94
|
+
_private(path) # a removal is a write too
|
|
76
95
|
os.environ.pop(key, None)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Where Otto keeps its per-installation state, and where its `.env` is.
|
|
2
|
+
|
|
3
|
+
Two facts that used to be constants: `~/.otto` for the memory stores, the
|
|
4
|
+
session index, the outcome log, routes and lessons (agent/memory/store.py,
|
|
5
|
+
agent/memory/sessions.py, agent/router/outcomes.py), and the repository
|
|
6
|
+
root's `.env` for keys (agent/config/envfile.py). Both are wrong the moment
|
|
7
|
+
Otto runs anywhere but a developer's shell. Embedded in an Android app there
|
|
8
|
+
is no `HOME` at all -- `Path.home()` raises -- and no repository, so an
|
|
9
|
+
installed wheel has nowhere to write a key to.
|
|
10
|
+
|
|
11
|
+
So they are functions of two environment variables, read at import time by
|
|
12
|
+
the modules that own the constants:
|
|
13
|
+
|
|
14
|
+
OTTO_HOME the directory for every per-installation file
|
|
15
|
+
OTTO_ENV_FILE the .env Otto loads at start and writes keys into
|
|
16
|
+
|
|
17
|
+
Read at import rather than at every call, deliberately: the constants they
|
|
18
|
+
feed (`store.DB_DIR`, `sessions.DEFAULT_INDEX_PATH`, `outcomes.DB_DIR`,
|
|
19
|
+
`envfile.ENV_PATH`) are what tests monkeypatch and what `bind_index`,
|
|
20
|
+
`bind_log` and `bind_store` override per test, and a value that moved
|
|
21
|
+
underneath those seams would make every one of them lie. The contract is
|
|
22
|
+
therefore: set the variables BEFORE `import agent.<anything>`, which is what
|
|
23
|
+
agent/embed.py's `configure()` does for an embedding host.
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import os
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
HOME_ENV = "OTTO_HOME"
|
|
31
|
+
ENV_FILE_ENV = "OTTO_ENV_FILE"
|
|
32
|
+
|
|
33
|
+
#: The repository root when Otto runs from a checkout, else None. An
|
|
34
|
+
#: installed wheel's `agent/` sits in site-packages, which has no
|
|
35
|
+
#: pyproject.toml two levels up.
|
|
36
|
+
_CHECKOUT_ROOT = Path(__file__).resolve().parents[2]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def otto_home() -> Path:
|
|
40
|
+
"""`$OTTO_HOME`, else `~/.otto`.
|
|
41
|
+
|
|
42
|
+
Raises RuntimeError naming the variable when neither can be resolved,
|
|
43
|
+
which is the honest answer on a host with no home directory rather than a
|
|
44
|
+
`Path.home()` traceback from inside an unrelated import.
|
|
45
|
+
"""
|
|
46
|
+
raw = os.environ.get(HOME_ENV, "").strip()
|
|
47
|
+
if raw:
|
|
48
|
+
return Path(raw).expanduser()
|
|
49
|
+
try:
|
|
50
|
+
return Path.home() / ".otto"
|
|
51
|
+
except (RuntimeError, KeyError) as exc:
|
|
52
|
+
raise RuntimeError(
|
|
53
|
+
f"no home directory to put Otto's state in -- set {HOME_ENV} to a "
|
|
54
|
+
"writable directory before importing agent"
|
|
55
|
+
) from exc
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def running_from_checkout() -> bool:
|
|
59
|
+
return (_CHECKOUT_ROOT / "pyproject.toml").is_file()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def env_file() -> Path:
|
|
63
|
+
"""`$OTTO_ENV_FILE`, else the checkout's `.env`, else `<otto_home>/.env`.
|
|
64
|
+
|
|
65
|
+
The checkout case keeps `otto tui`'s setup screen writing the file the
|
|
66
|
+
README tells a developer to create; an installed wheel has no such file
|
|
67
|
+
and keeps keys beside its other state instead.
|
|
68
|
+
"""
|
|
69
|
+
raw = os.environ.get(ENV_FILE_ENV, "").strip()
|
|
70
|
+
if raw:
|
|
71
|
+
return Path(raw).expanduser()
|
|
72
|
+
if running_from_checkout():
|
|
73
|
+
return _CHECKOUT_ROOT / ".env"
|
|
74
|
+
return otto_home() / ".env"
|