sliceagent 0.1.1__tar.gz → 0.1.3__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.
- {sliceagent-0.1.1 → sliceagent-0.1.3}/CHANGELOG.md +33 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/PKG-INFO +3 -4
- {sliceagent-0.1.1 → sliceagent-0.1.3}/QUICKSTART.md +6 -4
- {sliceagent-0.1.1 → sliceagent-0.1.3}/README.md +2 -3
- {sliceagent-0.1.1 → sliceagent-0.1.3}/install.sh +2 -3
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/__init__.py +1 -1
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/cli.py +9 -2
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/llm.py +18 -1
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/onboarding.py +91 -11
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/sensory_cortex.py +24 -3
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_onboarding.py +7 -7
- sliceagent-0.1.3/tests/test_provider_lineup.py +95 -0
- sliceagent-0.1.3/tests/test_repo_map_bounded.py +91 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/.dockerignore +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/.env.example +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/.github/dependabot.yml +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/.github/workflows/ci.yml +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/.github/workflows/publish.yml +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/.gitignore +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/CODE_OF_CONDUCT.md +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/CONTRIBUTING.md +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/Dockerfile +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/LICENSE +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/NOTICE +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/SECURITY.md +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/examples/plugins/hello/__init__.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/examples/plugins/hello/plugin.toml +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/pyproject.toml +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/scripts/gen_config_reference.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/scripts/run_tests.sh +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/sliceagent.toml.example +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/__main__.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/access.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/agents.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/background_review.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/binsniff.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/clock.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/code_grep.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/code_index.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/config.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/context_overflow.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/envspec.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/errors.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/events.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/finding_types.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/flags.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/fuzzy.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/guardrails.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/guidance.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/hippocampus.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/hooks.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/interfaces.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/loop.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/mcp_client.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/mcp_security.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/memory.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/metrics.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/model_catalog.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/monitor.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/neocortex.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/oracle.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/pagetable.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/pfc.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/plugins.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/policy.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/procman.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/prompt.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/records.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/recovery.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/regions.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/registry.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/retriever.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/safety.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/sandbox.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/scheduler.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/search_index.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/seed.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/session.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/skill_provenance.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/skill_usage.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/skills.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/subagent.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/subdir_hints.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/swap.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/taskstate.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/telemetry.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/terminal.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/text_utils.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/tool_summary.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/tools.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/tui.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/src/sliceagent/web.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/README.md +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_active_focus.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_agents.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_ask_echo.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_ask_user.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_background_review.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_binary_view.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_bound_is_relevance.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_breadth_e2e.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_bugfix_2b.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_bugfix_breadth_wave.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_bugfix_current_request.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_bugfix_linenum.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_bugfix_loop_wave.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_bugfix_memory_wave.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_bugfix_p0a.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_bugfix_p0b.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_bugfix_p0c.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_bugfix_selfreview.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_bugfix_selfreview_siblings.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_bugfix_skills_h1.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_bugfix_slice_wave.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_bugfix_subagent_wave.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_bugfix_tools_wave.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_bughunt_fixes.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_cache_manifest.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_checkpoint.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_cli_smoke.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_closure.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_code_grep.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_code_index.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_consolidate.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_context_overflow.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_conversation.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_coresidency.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_echo_before_blocking.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_episode.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_errors_backoff.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_esc_sentinel.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_exec_env.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_finding_types.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_flags.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_fuzzy.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_ghost_index.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_guardrails.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_guidance.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_history.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_invariant_fixes.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_live_composer.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_llm_streaming.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_llm_usage_cache.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_llm_watchdog.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_log_redaction.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_loop_overflow.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_mcp_output_cap.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_mcp_runtime.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_memory.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_memory_persist.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_metrics.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_mining.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_mission_tier.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_model_catalog.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_monitor.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_pageout_compaction.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_permission_patterns.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_pinned_composer.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_plan_tier.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_procman.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_product_features.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_progress_status.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_prompt_ab.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_provenance.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_proxy_interrupt.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_reach.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_readonly_subagent.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_recall_search.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_records.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_refault.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_registry_validation.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_reliability.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_requirements.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_review_fixes.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_route_lexical.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_safety.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_seal_markdown.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_search_index.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_skill_meta.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_skill_metadata.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_subdir_hints.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_task_agnostic.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_task_state_roundtrip.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_telemetry_convergence.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_terminal.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_tool_dedup.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_tool_result_ok.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_tools_robust.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_topic_switch.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_trajectory.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_tui.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_tui_menus.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_tui_render.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_tui_widgets.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_turn_budget.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_verification_agent.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_web.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_workspace.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_world_region.py +0 -0
- {sliceagent-0.1.1 → sliceagent-0.1.3}/tests/test_world_state.py +0 -0
|
@@ -5,6 +5,39 @@ this project aims for [Semantic Versioning](https://semver.org/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.1.3] — 2026-07-02
|
|
9
|
+
|
|
10
|
+
The five-door provider lineup + a wizard that feels like one.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Providers**: OpenRouter (hundreds of models, one key — now the first door), OpenAI,
|
|
14
|
+
**Anthropic/Claude** (new — via Anthropic's OpenAI-compatible endpoint), DeepSeek, Moonshot/Kimi,
|
|
15
|
+
plus custom endpoints. All five ride the single adapter — zero new dependencies.
|
|
16
|
+
- **OpenRouter quirks**: reasoning intent maps to OpenRouter's unified `reasoning` object (works
|
|
17
|
+
WITH tools — the raw `reasoning_effort` param never could); tool-calling requests pin routing to
|
|
18
|
+
hosts that honor every param (`require_parameters`) so nothing silently degrades; per-call
|
|
19
|
+
`usage.cost` is parsed into the cost meter.
|
|
20
|
+
- `ANTHROPIC_API_KEY` / `DEEPSEEK_API_KEY` / `OPENROUTER_API_KEY` accepted as key fallbacks.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- **Wizard UX**: API key shows `******` as you type (no more invisible field); provider and model
|
|
24
|
+
are arrow-key menus (↑/↓ + Enter, Esc cancels) with per-provider model suggestions +
|
|
25
|
+
"type another model id…". Scripted/CI runs keep the plain typed flow.
|
|
26
|
+
|
|
27
|
+
## [0.1.2] — 2026-07-02
|
|
28
|
+
|
|
29
|
+
Nothing may hang the user — three fixes from a live first-run reproduction.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
- **Repo-map walk is hard-bounded** (`max_dirs` budget): the first slice build can no longer hang for
|
|
33
|
+
minutes when the workspace root is huge (a home directory mistaken for a project, a giant monorepo).
|
|
34
|
+
Output caps existed; now the walk itself is bounded — worst case ~1s, maps what it saw.
|
|
35
|
+
- **The OS-account home never counts as a project root**, independent of `$HOME`: a stray
|
|
36
|
+
`package.json` in the real home no longer turns the entire home directory into a "project"
|
|
37
|
+
(the prior guard compared against `$HOME`, which containers/sandboxes/sudo can override).
|
|
38
|
+
- **Ctrl-C during the slice build cancels the turn cleanly** (`· cancelled`) instead of crashing the
|
|
39
|
+
REPL with a traceback — the build phase ran before the turn's interrupt handling.
|
|
40
|
+
|
|
8
41
|
## [0.1.1] — 2026-07-02
|
|
9
42
|
|
|
10
43
|
First-run onboarding, hardened by a live stranger walkthrough.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sliceagent
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: A coding agent with a new context-engineering framework: bounded, deterministic, reconstructed context (slice architecture) — built for long-horizon work.
|
|
5
5
|
Author: TT-Wang
|
|
6
6
|
License-Expression: MIT
|
|
@@ -145,11 +145,10 @@ Footprint is light (no torch). `pip install -e .` works for a clone too. Homebre
|
|
|
145
145
|
## Quickstart
|
|
146
146
|
|
|
147
147
|
```bash
|
|
148
|
-
sliceagent
|
|
149
|
-
sliceagent # start the agent
|
|
148
|
+
sliceagent # that's it — first run walks you through setup (provider, API key, live-tested), then you're in
|
|
150
149
|
```
|
|
151
150
|
|
|
152
|
-
|
|
151
|
+
Setup happens once, in-process, and writes `~/.sliceagent/config.toml` (0600) so every later run just starts. Re-configure anytime with `sliceagent init` (add/switch providers). Prefer env vars? Export **both** `LLM_API_KEY` and `AGENT_MODEL` (plus `LLM_BASE_URL` for non-OpenAI endpoints) — there is no default model; sliceagent never picks one for you. Discover every setting with `sliceagent config --list`.
|
|
153
152
|
|
|
154
153
|
→ Full walkthrough in **[QUICKSTART.md](QUICKSTART.md)** · **[CONTRIBUTING.md](CONTRIBUTING.md)** · **[CHANGELOG.md](CHANGELOG.md)**
|
|
155
154
|
|
|
@@ -15,12 +15,14 @@ installed by default; everything still works without it (plain stdout).
|
|
|
15
15
|
## 2. Set up your provider
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
sliceagent
|
|
18
|
+
sliceagent
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
`~/.sliceagent/config.toml` (mode `0600` —
|
|
21
|
+
Nothing to memorize — the **first run walks you through setup automatically**: pick a provider
|
|
22
|
+
(OpenRouter — hundreds of models with one key — or OpenAI, Anthropic/Claude, DeepSeek, Moonshot/Kimi, or a custom OpenAI-compatible endpoint), paste your API key (hidden),
|
|
23
|
+
choose a model. It **tests the key with one request**, writes `~/.sliceagent/config.toml` (mode `0600` —
|
|
24
|
+
it holds your key), and drops you straight into the session. Every later `sliceagent` just starts.
|
|
25
|
+
(Re-configure anytime — add or switch providers — with `sliceagent init`.)
|
|
24
26
|
|
|
25
27
|
Prefer environment variables? Skip `init` and export them instead:
|
|
26
28
|
|
|
@@ -112,11 +112,10 @@ Footprint is light (no torch). `pip install -e .` works for a clone too. Homebre
|
|
|
112
112
|
## Quickstart
|
|
113
113
|
|
|
114
114
|
```bash
|
|
115
|
-
sliceagent
|
|
116
|
-
sliceagent # start the agent
|
|
115
|
+
sliceagent # that's it — first run walks you through setup (provider, API key, live-tested), then you're in
|
|
117
116
|
```
|
|
118
117
|
|
|
119
|
-
|
|
118
|
+
Setup happens once, in-process, and writes `~/.sliceagent/config.toml` (0600) so every later run just starts. Re-configure anytime with `sliceagent init` (add/switch providers). Prefer env vars? Export **both** `LLM_API_KEY` and `AGENT_MODEL` (plus `LLM_BASE_URL` for non-OpenAI endpoints) — there is no default model; sliceagent never picks one for you. Discover every setting with `sliceagent config --list`.
|
|
120
119
|
|
|
121
120
|
→ Full walkthrough in **[QUICKSTART.md](QUICKSTART.md)** · **[CONTRIBUTING.md](CONTRIBUTING.md)** · **[CHANGELOG.md](CHANGELOG.md)**
|
|
122
121
|
|
|
@@ -96,9 +96,8 @@ cat <<'EOF'
|
|
|
96
96
|
|
|
97
97
|
✓ sliceagent installed.
|
|
98
98
|
|
|
99
|
-
Next:
|
|
100
|
-
sliceagent
|
|
101
|
-
sliceagent # start the agent
|
|
99
|
+
Next — just one command:
|
|
100
|
+
sliceagent # first run walks you through setup (provider, API key), then you're chatting
|
|
102
101
|
|
|
103
102
|
If 'sliceagent' isn't found, open a NEW terminal (PATH was just updated).
|
|
104
103
|
Docs: https://github.com/TT-Wang/sliceagent
|
|
@@ -171,7 +171,8 @@ def main() -> None:
|
|
|
171
171
|
|
|
172
172
|
def _key_present() -> bool:
|
|
173
173
|
return bool(os.environ.get("LLM_API_KEY") or os.environ.get("OPENAI_API_KEY")
|
|
174
|
-
or os.environ.get("
|
|
174
|
+
or os.environ.get("ANTHROPIC_API_KEY") or os.environ.get("DEEPSEEK_API_KEY")
|
|
175
|
+
or os.environ.get("OPENROUTER_API_KEY") or os.environ.get("MOONSHOT_API_KEY"))
|
|
175
176
|
|
|
176
177
|
def _first_run_setup(reason: str) -> bool:
|
|
177
178
|
"""First-run UX: a bare interactive `sliceagent` with nothing configured drops STRAIGHT into the
|
|
@@ -831,7 +832,13 @@ def main() -> None:
|
|
|
831
832
|
_stats["topic"] = one_line(session.active().goal, 40) if session.active_id else ""
|
|
832
833
|
record_user(session.active(), line) # short-range continuity: the RECENT CONVERSATION tier
|
|
833
834
|
_expand_mentions(line) # @path → pin the file into OPEN FILES
|
|
834
|
-
|
|
835
|
+
try:
|
|
836
|
+
# slice-build phase happens BEFORE run_turn's own KeyboardInterrupt handling — a ctrl-c
|
|
837
|
+
# here (e.g. during the one-time repo-map build) must cancel the turn, not crash the REPL.
|
|
838
|
+
build = make_build_slice(session, tools, retriever, memory, line, session.session_id)
|
|
839
|
+
except KeyboardInterrupt:
|
|
840
|
+
print("\n · cancelled")
|
|
841
|
+
continue
|
|
835
842
|
if os.environ.get("AGENT_TIMING"): # per-turn latency breakdown (build vs model) → find the hang
|
|
836
843
|
import time as _tt
|
|
837
844
|
_b = build
|
|
@@ -81,6 +81,9 @@ def _usage_dict(raw) -> dict | None:
|
|
|
81
81
|
}
|
|
82
82
|
if cache_read:
|
|
83
83
|
usage["cached_tokens"] = cache_read # legacy key (only when present)
|
|
84
|
+
_cost = getattr(raw, "cost", None) # OpenRouter: authoritative $ per call
|
|
85
|
+
if isinstance(_cost, (int, float)) and _cost >= 0:
|
|
86
|
+
usage["cost_usd"] = float(_cost)
|
|
84
87
|
return usage
|
|
85
88
|
|
|
86
89
|
|
|
@@ -189,7 +192,9 @@ class OpenAILLM:
|
|
|
189
192
|
# export OPENAI_API_KEY) — the surface the user configures says "LLM", not a provider name.
|
|
190
193
|
# Resolve the ENDPOINT first: the proxy choice below depends on which provider it is.
|
|
191
194
|
kwargs: dict = {"api_key": api_key or os.environ.get("LLM_API_KEY")
|
|
192
|
-
or os.environ.get("OPENAI_API_KEY") or os.environ.get("
|
|
195
|
+
or os.environ.get("OPENAI_API_KEY") or os.environ.get("ANTHROPIC_API_KEY")
|
|
196
|
+
or os.environ.get("DEEPSEEK_API_KEY") or os.environ.get("OPENROUTER_API_KEY")
|
|
197
|
+
or os.environ.get("MOONSHOT_API_KEY")}
|
|
193
198
|
resolved_base = base_url or os.environ.get("LLM_BASE_URL") or os.environ.get("OPENAI_BASE_URL")
|
|
194
199
|
if not resolved_base and os.environ.get("MOONSHOT_API_KEY") and not (
|
|
195
200
|
os.environ.get("LLM_API_KEY") or os.environ.get("OPENAI_API_KEY")):
|
|
@@ -466,6 +471,12 @@ class OpenAILLM:
|
|
|
466
471
|
from .model_catalog import capability
|
|
467
472
|
r = self.reasoning
|
|
468
473
|
model, base = self.model.lower(), self._base_url.lower()
|
|
474
|
+
if "openrouter" in base:
|
|
475
|
+
# OpenRouter's UNIFIED reasoning object translates effort per upstream vendor (OpenAI
|
|
476
|
+
# reasoning_effort / Anthropic thinking budget / Gemini thinkingLevel) and — unlike raw
|
|
477
|
+
# reasoning_effort on chat/completions — works WITH tools. "full" stays provider-default.
|
|
478
|
+
effort = {"fast": "low", "high": "high", "max": "high"}.get(r)
|
|
479
|
+
return {"extra_body": {"reasoning": {"effort": effort}}} if effort else {}
|
|
469
480
|
if "deepseek" in model or "deepseek" in base:
|
|
470
481
|
return {"extra_body": {"thinking": {"type": "disabled"}}} if r == "fast" else {}
|
|
471
482
|
if not capability(self.model, self._base_url).supports_reasoning_effort:
|
|
@@ -637,6 +648,12 @@ class OpenAILLM:
|
|
|
637
648
|
self._merge_kwargs(kwargs, self._cache_routing_kwargs()) # session-stable cache routing (0 added tokens)
|
|
638
649
|
self._merge_kwargs(kwargs, self._reasoning_kwargs())
|
|
639
650
|
self._merge_kwargs(kwargs, self._cache_kwargs(messages))
|
|
651
|
+
if "openrouter" in self._base_url.lower() and tools:
|
|
652
|
+
# OpenRouter routes one model slug across MANY hosts; some serve quantizations with broken
|
|
653
|
+
# tool calling, and unsupported params are DROPPED silently by default. require_parameters
|
|
654
|
+
# pins routing to hosts that honor every param we sent — a silent degrade becomes a visible
|
|
655
|
+
# error instead (the exact failure class behind the old reasoning-effort ceiling).
|
|
656
|
+
self._merge_kwargs(kwargs, {"extra_body": {"provider": {"require_parameters": True}}})
|
|
640
657
|
# Provider quirk (isolated here, llm-agnostic): some reasoning models (gpt-5.5) reject
|
|
641
658
|
# reasoning_effort TOGETHER with function tools on /v1/chat/completions (400 — "use /v1/responses").
|
|
642
659
|
# Once seen, drop reasoning_effort whenever tools are present so we degrade to default reasoning
|
|
@@ -13,14 +13,64 @@ import os
|
|
|
13
13
|
from .envspec import GROUPS, REGISTRY, current_value
|
|
14
14
|
|
|
15
15
|
# provider presets: key → (label, base_url, default_model). 'custom' prompts for the base_url.
|
|
16
|
+
# The lineup: one AGGREGATOR door (OpenRouter — breadth, one key) + four first-party doors
|
|
17
|
+
# (OpenAI, Anthropic, DeepSeek, Moonshot). Anthropic rides its OpenAI-compatible endpoint, so all
|
|
18
|
+
# five share the single OpenAILLM adapter — no per-provider SDKs.
|
|
16
19
|
PROVIDERS = {
|
|
17
|
-
"1": ("
|
|
20
|
+
"1": ("openrouter", "OpenRouter (hundreds of models, one key)", "https://openrouter.ai/api/v1", "openai/gpt-5.5"),
|
|
18
21
|
"2": ("openai", "OpenAI", "", "gpt-5.5"),
|
|
19
|
-
"3": ("
|
|
20
|
-
"4": ("
|
|
22
|
+
"3": ("anthropic", "Anthropic (Claude)", "https://api.anthropic.com/v1", "claude-sonnet-5"),
|
|
23
|
+
"4": ("deepseek", "DeepSeek", "https://api.deepseek.com/v1", "deepseek-chat"),
|
|
24
|
+
"5": ("moonshot", "Moonshot (Kimi)", "https://api.moonshot.cn/v1", "kimi-k2.7-code"),
|
|
25
|
+
"6": ("custom", "Custom OpenAI-compatible endpoint", "", ""),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
# wizard model MENU per provider — suggestions for the arrow picker, NOT a cap: "type another…"
|
|
29
|
+
# always accepts any model id (the catalog/pricing layers stay the capability source of truth).
|
|
30
|
+
MODEL_SUGGESTIONS = {
|
|
31
|
+
"openrouter": ["openai/gpt-5.5", "anthropic/claude-sonnet-5", "deepseek/deepseek-chat",
|
|
32
|
+
"moonshotai/kimi-k2.7"],
|
|
33
|
+
"openai": ["gpt-5.5", "gpt-5", "gpt-5-mini", "o3"],
|
|
34
|
+
"anthropic": ["claude-sonnet-5", "claude-opus-4-8", "claude-haiku-4-5"],
|
|
35
|
+
"deepseek": ["deepseek-chat", "deepseek-reasoner"],
|
|
36
|
+
"moonshot": ["kimi-k2.7-code", "kimi-k2-0905-preview"],
|
|
37
|
+
"custom": [],
|
|
21
38
|
}
|
|
22
39
|
|
|
23
40
|
|
|
41
|
+
def _tty() -> bool:
|
|
42
|
+
import sys
|
|
43
|
+
try:
|
|
44
|
+
return sys.stdin.isatty() and sys.stdout.isatty()
|
|
45
|
+
except Exception: # noqa: BLE001
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _masked_input(prompt_text: str, fallback):
|
|
50
|
+
"""API-key entry with VISIBLE feedback — asterisks per keystroke (prompt_toolkit is_password)
|
|
51
|
+
instead of getpass's fully-invisible field, which first-run users read as 'nothing is happening'.
|
|
52
|
+
Falls back to getpass when the TUI stack is absent or anything goes wrong."""
|
|
53
|
+
try:
|
|
54
|
+
from prompt_toolkit import prompt as _ptk_prompt
|
|
55
|
+
return _ptk_prompt(prompt_text, is_password=True)
|
|
56
|
+
except (ImportError, OSError, EOFError):
|
|
57
|
+
return fallback(prompt_text)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _pick(options: list, default: int = 0):
|
|
61
|
+
"""Arrow-key selector for the wizard (reuses the TUI's _arrow_select: ↑/↓ or ←/→ + Enter).
|
|
62
|
+
Returns the index; raises KeyboardInterrupt on Esc (→ the wizard's normal 'cancelled' path);
|
|
63
|
+
returns None when a selector can't safely run so the caller falls back to typed input."""
|
|
64
|
+
try:
|
|
65
|
+
from .tui import _arrow_select
|
|
66
|
+
except ImportError:
|
|
67
|
+
return None
|
|
68
|
+
idx = _arrow_select(options, default=default)
|
|
69
|
+
if idx == -1:
|
|
70
|
+
raise KeyboardInterrupt
|
|
71
|
+
return idx
|
|
72
|
+
|
|
73
|
+
|
|
24
74
|
def _version() -> str:
|
|
25
75
|
try:
|
|
26
76
|
from . import __version__
|
|
@@ -182,11 +232,24 @@ def run_init(*, inp=input, getpw=None, llm_factory=None, home=None) -> int:
|
|
|
182
232
|
if ans in ("n", "no"):
|
|
183
233
|
out(" Leaving the existing config unchanged. Run `sliceagent` to start."); return 0
|
|
184
234
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
235
|
+
# arrow-key niceties only on a REAL interactive run with the default input hooks — injected
|
|
236
|
+
# inp/getpw (tests, scripted runs) keep the typed flow byte-for-byte.
|
|
237
|
+
fancy = inp is input and getpw is __import__("getpass").getpass and _tty()
|
|
238
|
+
keys = sorted(PROVIDERS)
|
|
188
239
|
try:
|
|
189
|
-
|
|
240
|
+
out("\n Choose a provider (↑/↓ + Enter):" if fancy else "\n Choose a provider:")
|
|
241
|
+
idx = None
|
|
242
|
+
if fancy:
|
|
243
|
+
labels = [f"{PROVIDERS[k][1]}" + (f" — {PROVIDERS[k][3]}" if PROVIDERS[k][3] else "")
|
|
244
|
+
for k in keys]
|
|
245
|
+
idx = _pick(labels) # Esc → KeyboardInterrupt → 'cancelled' below
|
|
246
|
+
if idx is not None:
|
|
247
|
+
choice = keys[idx]
|
|
248
|
+
else: # typed fallback (no tty / selector unavailable)
|
|
249
|
+
for k in keys:
|
|
250
|
+
_id, label, _base, model0 = PROVIDERS[k]
|
|
251
|
+
out(f" {k}. {label}" + (f" ({model0})" if model0 else ""))
|
|
252
|
+
choice = inp(" > ").strip() or "1"
|
|
190
253
|
pid, label, base_url, model = PROVIDERS.get(choice, PROVIDERS["1"])
|
|
191
254
|
if pid == "custom":
|
|
192
255
|
base_url = inp(" Base URL (OpenAI-compatible, e.g. https://host/v1): ").strip()
|
|
@@ -198,10 +261,27 @@ def run_init(*, inp=input, getpw=None, llm_factory=None, home=None) -> int:
|
|
|
198
261
|
existing = existing.get(pid) if isinstance(existing, dict) else None
|
|
199
262
|
existing = existing if isinstance(existing, dict) else {}
|
|
200
263
|
existing_key = existing.get("api_key") or ""
|
|
201
|
-
key_prompt = " API key (
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
264
|
+
key_prompt = (" API key (typed as ******, Enter to keep existing): " if existing_key
|
|
265
|
+
else " API key (typed as ******): ") if fancy else (
|
|
266
|
+
" API key (hidden, Enter to keep existing): " if existing_key
|
|
267
|
+
else " API key (hidden): ")
|
|
268
|
+
key = (_masked_input(key_prompt, getpw) if fancy else getpw(key_prompt)).strip() or existing_key
|
|
269
|
+
picked_model = None
|
|
270
|
+
if fancy:
|
|
271
|
+
cur = existing.get("model") or model
|
|
272
|
+
opts = [m for m in ([cur] if cur else []) + MODEL_SUGGESTIONS.get(pid, []) if m]
|
|
273
|
+
opts = list(dict.fromkeys(opts)) + ["type another model id…"]
|
|
274
|
+
out(" Model (↑/↓ + Enter):")
|
|
275
|
+
midx = _pick(opts)
|
|
276
|
+
if midx is not None:
|
|
277
|
+
picked_model = None if opts[midx].startswith("type another") else opts[midx]
|
|
278
|
+
if picked_model is None:
|
|
279
|
+
picked_model = inp(" Model id: ").strip()
|
|
280
|
+
if picked_model:
|
|
281
|
+
model = picked_model
|
|
282
|
+
else:
|
|
283
|
+
model = (inp(f" Model [{existing.get('model') or model or 'required'}]: ").strip()
|
|
284
|
+
or existing.get("model") or model)
|
|
205
285
|
except (EOFError, KeyboardInterrupt):
|
|
206
286
|
out("\n cancelled."); return 1
|
|
207
287
|
if not key:
|
|
@@ -94,6 +94,17 @@ def _home() -> Optional[Path]:
|
|
|
94
94
|
return None
|
|
95
95
|
|
|
96
96
|
|
|
97
|
+
def _os_home() -> Optional[Path]:
|
|
98
|
+
"""The account's home from the OS user database — independent of the $HOME env var. Guards the
|
|
99
|
+
home-is-not-a-project rule even when $HOME is overridden (containers, sudo, test sandboxes):
|
|
100
|
+
without this, a stray package.json in the real home turns the whole home dir into a 'project'."""
|
|
101
|
+
try:
|
|
102
|
+
import pwd
|
|
103
|
+
return Path(pwd.getpwuid(os.getuid()).pw_dir).resolve()
|
|
104
|
+
except (ImportError, KeyError, OSError): # pwd is POSIX-only; degrade to the env-based home
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
|
|
97
108
|
def _marker_root(cwd: Path) -> Optional[Path]:
|
|
98
109
|
"""Nearest ancestor (≤6 levels) that looks like a project root, or ``None``.
|
|
99
110
|
|
|
@@ -104,11 +115,11 @@ def _marker_root(cwd: Path) -> Optional[Path]:
|
|
|
104
115
|
current = cwd.resolve()
|
|
105
116
|
except (OSError, RuntimeError, ValueError):
|
|
106
117
|
return None
|
|
107
|
-
|
|
118
|
+
homes = {h for h in (_home(), _os_home()) if h is not None}
|
|
108
119
|
for depth, parent in enumerate([current, *current.parents]):
|
|
109
120
|
if depth > 6:
|
|
110
121
|
break
|
|
111
|
-
if parent
|
|
122
|
+
if parent in homes:
|
|
112
123
|
continue
|
|
113
124
|
try:
|
|
114
125
|
for marker in _PROJECT_MARKERS:
|
|
@@ -443,7 +454,8 @@ _CODE_SUFFIX = (".py", ".js", ".ts", ".jsx", ".tsx", ".go", ".rs", ".java", ".rb
|
|
|
443
454
|
".exs", ".clj", ".r", ".jl", ".vue", ".sql")
|
|
444
455
|
|
|
445
456
|
|
|
446
|
-
def repo_map(root: str, *, max_entries: int = 300, max_per_dir: int = 25, max_chars: int = 12000
|
|
457
|
+
def repo_map(root: str, *, max_entries: int = 300, max_per_dir: int = 25, max_chars: int = 12000,
|
|
458
|
+
max_dirs: int = 4000) -> str:
|
|
447
459
|
"""A compact, ignore-aware STRUCTURAL MAP of the project (SENSORY CORTEX — the derived-view tier-B
|
|
448
460
|
resident view, memoized for the session, never a persisted store): directories with their files,
|
|
449
461
|
pruned of VCS/venv/cache + asset/log noise, RANKED by source-density so the real code tree shows
|
|
@@ -456,8 +468,17 @@ def repo_map(root: str, *, max_entries: int = 300, max_per_dir: int = 25, max_ch
|
|
|
456
468
|
if not root or not os.path.isdir(root):
|
|
457
469
|
return ""
|
|
458
470
|
rows: list[tuple[str, list[str], int, int]] = [] # (rel, files, total, code_count)
|
|
471
|
+
walked = 0
|
|
459
472
|
try:
|
|
460
473
|
for dirpath, dirnames, filenames in os.walk(root): # symlinks not followed
|
|
474
|
+
# HARD WALK BUDGET — the invariant is that repo_map completes in bounded time on ANY root.
|
|
475
|
+
# max_entries/max_chars cap the OUTPUT but only after the walk; without this, a huge root
|
|
476
|
+
# (a home dir mistaken for a project, a giant monorepo) hangs the first slice build for
|
|
477
|
+
# minutes inside os.walk. Past the budget we map what we saw — bounded beats complete.
|
|
478
|
+
walked += 1
|
|
479
|
+
if walked > max_dirs:
|
|
480
|
+
dirnames[:] = []
|
|
481
|
+
break
|
|
461
482
|
dirnames[:] = sorted(d for d in dirnames if not _is_ignored(d))
|
|
462
483
|
files = sorted(f for f in filenames
|
|
463
484
|
if not _is_ignored(f) and not f.endswith(_MAP_SKIP_SUFFIX))
|
|
@@ -134,7 +134,7 @@ class _OkLLM:
|
|
|
134
134
|
def init_writes_a_valid_config_on_success():
|
|
135
135
|
home = tempfile.mkdtemp(prefix="init-")
|
|
136
136
|
rc = onboarding.run_init(
|
|
137
|
-
inp=_seq("
|
|
137
|
+
inp=_seq("5", ""), # provider=moonshot (menu slot 5), model=default
|
|
138
138
|
getpw=_seq("sk-test-key"),
|
|
139
139
|
llm_factory=lambda model: _OkLLM(),
|
|
140
140
|
home=home)
|
|
@@ -180,7 +180,7 @@ def init_offers_to_save_after_a_failed_key_test():
|
|
|
180
180
|
def init_custom_provider_prompts_base_url():
|
|
181
181
|
home = tempfile.mkdtemp(prefix="init-custom-")
|
|
182
182
|
rc = onboarding.run_init(
|
|
183
|
-
inp=_seq("
|
|
183
|
+
inp=_seq("6", "https://my.host/v1", "my-model"), # custom → base url → model (menu: 6 = custom)
|
|
184
184
|
getpw=_seq("k"),
|
|
185
185
|
llm_factory=lambda model: _OkLLM(),
|
|
186
186
|
home=home)
|
|
@@ -193,7 +193,7 @@ def init_custom_provider_prompts_base_url():
|
|
|
193
193
|
@check
|
|
194
194
|
def init_aborts_without_a_key():
|
|
195
195
|
home = tempfile.mkdtemp(prefix="init-nokey-")
|
|
196
|
-
rc = onboarding.run_init(inp=_seq("
|
|
196
|
+
rc = onboarding.run_init(inp=_seq("5", ""), getpw=_seq(""), llm_factory=lambda m: _OkLLM(), home=home)
|
|
197
197
|
assert rc == 1, "no key → abort"
|
|
198
198
|
assert not os.path.exists(os.path.join(home, ".sliceagent", "config.toml"))
|
|
199
199
|
|
|
@@ -224,7 +224,7 @@ def write_config_is_atomic_and_cleans_up_on_failure():
|
|
|
224
224
|
@check
|
|
225
225
|
def init_merge_keeps_existing_providers():
|
|
226
226
|
home = tempfile.mkdtemp(prefix="init-merge-")
|
|
227
|
-
onboarding.run_init(inp=_seq("
|
|
227
|
+
onboarding.run_init(inp=_seq("5", ""), getpw=_seq("k-moon"), llm_factory=lambda m: _OkLLM(), home=home)
|
|
228
228
|
# 2nd init on the existing config: "Add/update? [Y/n]" → "" (yes) → provider 2 (openai) → model ""
|
|
229
229
|
onboarding.run_init(inp=_seq("", "2", ""), getpw=_seq("k-oai"), llm_factory=lambda m: _OkLLM(), home=home)
|
|
230
230
|
data = tomllib.load(open(os.path.join(home, ".sliceagent", "config.toml"), "rb"))
|
|
@@ -239,10 +239,10 @@ def reinit_blank_key_keeps_existing_key_for_the_same_provider():
|
|
|
239
239
|
# prompt (no retype) must KEEP the saved key/model, not abort — the abort-on-blank behavior
|
|
240
240
|
# (init_aborts_without_a_key) is only correct for a provider with no existing entry.
|
|
241
241
|
home = tempfile.mkdtemp(prefix="init-reblank-")
|
|
242
|
-
onboarding.run_init(inp=_seq("
|
|
242
|
+
onboarding.run_init(inp=_seq("5", ""), getpw=_seq("k-moon"), llm_factory=lambda m: _OkLLM(), home=home)
|
|
243
243
|
# 2nd init: "Add/update? [Y/n]" → "" (yes) → provider 1 (moonshot, already saved) → key "" (blank,
|
|
244
244
|
# keep existing) → model "" (blank, keep existing)
|
|
245
|
-
rc = onboarding.run_init(inp=_seq("", "
|
|
245
|
+
rc = onboarding.run_init(inp=_seq("", "5", ""), getpw=_seq(""), llm_factory=lambda m: _OkLLM(), home=home)
|
|
246
246
|
assert rc == 0, "blank key on an already-configured provider must NOT abort"
|
|
247
247
|
data = tomllib.load(open(os.path.join(home, ".sliceagent", "config.toml"), "rb"))
|
|
248
248
|
assert data["providers"]["moonshot"]["api_key"] == "k-moon", "the existing key must be kept, not wiped"
|
|
@@ -252,7 +252,7 @@ def reinit_blank_key_keeps_existing_key_for_the_same_provider():
|
|
|
252
252
|
@check
|
|
253
253
|
def config_use_switches_default_provider():
|
|
254
254
|
home = tempfile.mkdtemp(prefix="cfg-use-")
|
|
255
|
-
onboarding.run_init(inp=_seq("
|
|
255
|
+
onboarding.run_init(inp=_seq("5", ""), getpw=_seq("k1"), llm_factory=lambda m: _OkLLM(), home=home)
|
|
256
256
|
onboarding.run_init(inp=_seq("", "2", ""), getpw=_seq("k2"), llm_factory=lambda m: _OkLLM(), home=home)
|
|
257
257
|
assert onboarding.run_config(["--use", "moonshot"], home=home) == 0
|
|
258
258
|
data = tomllib.load(open(os.path.join(home, ".sliceagent", "config.toml"), "rb"))
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""The five-door provider lineup (OpenRouter + OpenAI/Anthropic/DeepSeek/Moonshot + custom) and the
|
|
2
|
+
OpenRouter/Anthropic adapter quirks. All offline — stub objects, no network, no pytest.
|
|
3
|
+
Run: PYTHONPATH=src python tests/test_provider_lineup.py
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from types import SimpleNamespace as NS
|
|
8
|
+
|
|
9
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
|
10
|
+
|
|
11
|
+
from sliceagent.llm import OpenAILLM, _usage_dict # noqa: E402
|
|
12
|
+
from sliceagent.onboarding import MODEL_SUGGESTIONS, PROVIDERS # noqa: E402
|
|
13
|
+
|
|
14
|
+
CHECKS = []
|
|
15
|
+
def check(fn):
|
|
16
|
+
CHECKS.append(fn)
|
|
17
|
+
return fn
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _stub(model: str, base: str, reasoning: str = "full") -> OpenAILLM:
|
|
21
|
+
obj = OpenAILLM.__new__(OpenAILLM) # bypass __init__ (no network)
|
|
22
|
+
obj.model = model
|
|
23
|
+
obj._base_url = base
|
|
24
|
+
obj.reasoning = reasoning
|
|
25
|
+
return obj
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@check
|
|
29
|
+
def lineup_is_the_agreed_five_doors_plus_custom():
|
|
30
|
+
ids = [PROVIDERS[k][0] for k in sorted(PROVIDERS)]
|
|
31
|
+
assert ids == ["openrouter", "openai", "anthropic", "deepseek", "moonshot", "custom"], ids
|
|
32
|
+
for _pid, _label, base, model in PROVIDERS.values():
|
|
33
|
+
if _pid != "custom":
|
|
34
|
+
assert model, f"{_pid} preset needs a default model"
|
|
35
|
+
assert PROVIDERS["3"][2] == "https://api.anthropic.com/v1" # Claude via the OpenAI-compat endpoint
|
|
36
|
+
assert all(PROVIDERS[k][0] in MODEL_SUGGESTIONS for k in PROVIDERS), "every preset needs a model menu"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@check
|
|
40
|
+
def openrouter_reasoning_maps_to_the_unified_object():
|
|
41
|
+
llm = _stub("anthropic/claude-sonnet-5", "https://openrouter.ai/api/v1", reasoning="fast")
|
|
42
|
+
assert llm._reasoning_kwargs() == {"extra_body": {"reasoning": {"effort": "low"}}}
|
|
43
|
+
llm.reasoning = "high"
|
|
44
|
+
assert llm._reasoning_kwargs() == {"extra_body": {"reasoning": {"effort": "high"}}}
|
|
45
|
+
llm.reasoning = "full" # default → provider default, no param at all (never force spend)
|
|
46
|
+
assert llm._reasoning_kwargs() == {}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@check
|
|
50
|
+
def openrouter_never_sends_raw_reasoning_effort():
|
|
51
|
+
# the raw chat param is exactly what 400s/DROPS silently upstream — the unified object replaces it
|
|
52
|
+
llm = _stub("openai/gpt-5.5", "https://openrouter.ai/api/v1", reasoning="max")
|
|
53
|
+
kw = llm._reasoning_kwargs()
|
|
54
|
+
assert "reasoning_effort" not in kw and kw["extra_body"]["reasoning"]["effort"] == "high"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@check
|
|
58
|
+
def anthropic_direct_stays_on_provider_defaults():
|
|
59
|
+
# claude via api.anthropic.com: no reasoning_effort (capability=False), no prompt_cache_key routing
|
|
60
|
+
llm = _stub("claude-sonnet-5", "https://api.anthropic.com/v1", reasoning="high")
|
|
61
|
+
assert llm._reasoning_kwargs() == {}, "anthropic family must not receive reasoning_effort"
|
|
62
|
+
assert llm._cache_routing_kwargs() == {}, "OpenAI cache routing keys must not go to Anthropic"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@check
|
|
66
|
+
def usage_dict_parses_openrouter_cost():
|
|
67
|
+
raw = NS(prompt_tokens=100, completion_tokens=10, prompt_tokens_details=None,
|
|
68
|
+
cache_creation_input_tokens=0, cost=0.0042)
|
|
69
|
+
u = _usage_dict(raw)
|
|
70
|
+
assert u["cost_usd"] == 0.0042 and u["prompt_tokens"] == 100
|
|
71
|
+
raw_no_cost = NS(prompt_tokens=1, completion_tokens=1, prompt_tokens_details=None,
|
|
72
|
+
cache_creation_input_tokens=0)
|
|
73
|
+
assert "cost_usd" not in _usage_dict(raw_no_cost), "absent cost must not fabricate a field"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@check
|
|
77
|
+
def openrouter_tools_requests_pin_param_honoring_hosts():
|
|
78
|
+
llm = _stub("openai/gpt-5.5", "https://openrouter.ai/api/v1", reasoning="fast")
|
|
79
|
+
kwargs = {"extra_body": {}}
|
|
80
|
+
llm._merge_kwargs(kwargs, llm._reasoning_kwargs())
|
|
81
|
+
llm._merge_kwargs(kwargs, {"extra_body": {"provider": {"require_parameters": True}}})
|
|
82
|
+
eb = kwargs["extra_body"]
|
|
83
|
+
assert eb["reasoning"]["effort"] == "low" and eb["provider"]["require_parameters"] is True, \
|
|
84
|
+
"both quirks must coexist in one extra_body (deep merge)"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
ok = 0
|
|
89
|
+
for fn in CHECKS:
|
|
90
|
+
try:
|
|
91
|
+
fn(); ok += 1; print(f"PASS {fn.__name__}")
|
|
92
|
+
except Exception as e: # noqa: BLE001
|
|
93
|
+
print(f"FAIL {fn.__name__}: {type(e).__name__}: {e}")
|
|
94
|
+
print(f"\n{ok}/{len(CHECKS)} passed")
|
|
95
|
+
sys.exit(0 if ok == len(CHECKS) else 1)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""The repo-map walk must be BOUNDED — it can never hang the first slice build, no matter what the
|
|
2
|
+
workspace root is (a huge monorepo, or a home dir that a stray package.json turned into a 'project').
|
|
3
|
+
Found live: launching from /Users/<u> with HOME overridden made os.walk crawl the entire home dir for
|
|
4
|
+
minutes; ctrl-c then killed the REPL with a traceback. Invariant: repo_map completes within its walk
|
|
5
|
+
budget on ANY root, and the OS-account home never counts as a project root regardless of $HOME.
|
|
6
|
+
Run: PYTHONPATH=src python tests/test_repo_map_bounded.py
|
|
7
|
+
"""
|
|
8
|
+
import os
|
|
9
|
+
import pathlib
|
|
10
|
+
import sys
|
|
11
|
+
import time
|
|
12
|
+
|
|
13
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
|
14
|
+
|
|
15
|
+
from sliceagent import sensory_cortex as sc # noqa: E402
|
|
16
|
+
|
|
17
|
+
CHECKS = []
|
|
18
|
+
def check(fn):
|
|
19
|
+
CHECKS.append(fn)
|
|
20
|
+
return fn
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _make_tree(root: pathlib.Path, n_dirs: int) -> None:
|
|
24
|
+
for i in range(n_dirs):
|
|
25
|
+
d = root / f"pkg{i:03d}"
|
|
26
|
+
d.mkdir(parents=True)
|
|
27
|
+
(d / "mod.py").write_text("x = 1\n")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@check
|
|
31
|
+
def walk_stops_at_the_dir_budget():
|
|
32
|
+
import tempfile
|
|
33
|
+
root = pathlib.Path(tempfile.mkdtemp())
|
|
34
|
+
_make_tree(root, 60)
|
|
35
|
+
out = sc.repo_map(str(root), max_dirs=10)
|
|
36
|
+
# bounded: only what the budget allowed got mapped (root dir itself may count as one visit)
|
|
37
|
+
mapped = [ln for ln in out.splitlines() if ln.strip()]
|
|
38
|
+
assert 0 < len(mapped) <= 10, f"expected ≤10 mapped dirs under a 10-dir budget, got {len(mapped)}"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@check
|
|
42
|
+
def bounded_walk_is_fast_even_on_a_big_tree():
|
|
43
|
+
import tempfile
|
|
44
|
+
root = pathlib.Path(tempfile.mkdtemp())
|
|
45
|
+
_make_tree(root, 300)
|
|
46
|
+
t0 = time.monotonic()
|
|
47
|
+
out = sc.repo_map(str(root), max_dirs=50)
|
|
48
|
+
dt = time.monotonic() - t0
|
|
49
|
+
assert out, "map must still be produced within the budget"
|
|
50
|
+
assert dt < 5.0, f"bounded walk took {dt:.1f}s — the budget is not bounding"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@check
|
|
54
|
+
def default_budget_maps_a_normal_project_fully():
|
|
55
|
+
import tempfile
|
|
56
|
+
root = pathlib.Path(tempfile.mkdtemp())
|
|
57
|
+
_make_tree(root, 20)
|
|
58
|
+
out = sc.repo_map(str(root)) # default max_dirs — far above a normal project
|
|
59
|
+
assert sum(1 for ln in out.splitlines() if "mod.py" in ln) == 20, "normal projects must be unaffected"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@check
|
|
63
|
+
def os_account_home_never_counts_as_a_project_even_with_home_overridden():
|
|
64
|
+
import tempfile
|
|
65
|
+
fake_os_home = pathlib.Path(tempfile.mkdtemp()) # stands in for pw_dir
|
|
66
|
+
(fake_os_home / "package.json").write_text("{}") # the stray marker that caused the live bug
|
|
67
|
+
orig = sc._os_home
|
|
68
|
+
sc._os_home = lambda: fake_os_home.resolve()
|
|
69
|
+
try:
|
|
70
|
+
# $HOME points elsewhere (the live-repro condition), yet the OS home must still be skipped
|
|
71
|
+
assert sc._marker_root(fake_os_home) is None, \
|
|
72
|
+
"a marker in the OS-account home must not make the home a project"
|
|
73
|
+
# ...while a real project UNDER the home is still detected
|
|
74
|
+
proj = fake_os_home / "code" / "myproj"
|
|
75
|
+
proj.mkdir(parents=True)
|
|
76
|
+
(proj / "pyproject.toml").write_text("[project]\nname='x'\n")
|
|
77
|
+
got = sc._marker_root(proj)
|
|
78
|
+
assert got is not None and got.name == "myproj", f"real subproject must still resolve, got {got}"
|
|
79
|
+
finally:
|
|
80
|
+
sc._os_home = orig
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
ok = 0
|
|
85
|
+
for fn in CHECKS:
|
|
86
|
+
try:
|
|
87
|
+
fn(); ok += 1; print(f"PASS {fn.__name__}")
|
|
88
|
+
except Exception as e: # noqa: BLE001
|
|
89
|
+
print(f"FAIL {fn.__name__}: {type(e).__name__}: {e}")
|
|
90
|
+
print(f"\n{ok}/{len(CHECKS)} passed")
|
|
91
|
+
sys.exit(0 if ok == len(CHECKS) else 1)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|