kiwimatecoder 0.1.0__tar.gz → 0.2.0__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.
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/PKG-INFO +36 -1
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/README.md +35 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/agent.py +19 -5
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/commands.py +108 -1
- kiwimatecoder-0.2.0/kiwimatecoder/goal.py +229 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/repl.py +47 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/session.py +5 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/__init__.py +2 -0
- kiwimatecoder-0.2.0/kiwimatecoder/tools/goal.py +69 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder.egg-info/PKG-INFO +36 -1
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder.egg-info/SOURCES.txt +3 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/pyproject.toml +1 -1
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_config_markup.py +3 -1
- kiwimatecoder-0.2.0/tests/test_goal.py +828 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_slash_markup.py +7 -3
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_tool_registry.py +1 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/LICENSE +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/__init__.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/__main__.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/acp/__init__.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/acp/protocol.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/acp/server.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/acp/stdio.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/ai.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/audit.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/browser.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/catalog.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/checkpoints.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/client.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/clipboard.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/config.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/diagnostics.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/documents.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/evals/__init__.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/evals/cases.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/evals/render.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/evals/runner.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/events.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/forge.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/git.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/headless.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/hooks.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/hunks.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/i18n.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/images.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/index/__init__.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/index/builder.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/index/embeddings.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/index/search.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/index/store.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/instructions.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/jobs.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/lsp/__init__.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/lsp/client.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/lsp/manager.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/lsp/protocol.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/main.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/mcp/__init__.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/mcp/client.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/mcp/errors.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/mcp/manager.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/mcp/protocol.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/media.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/memory.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/network.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/notify.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/permissions.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/pickers.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/plugins.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/pricing.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/prompts.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/providers.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/redaction.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/remote.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/routing.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/sandbox.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/sdk.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/share.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/shell.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/skills.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/sync.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/team.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/telemetry.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/templates.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/ask.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/base.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/browser.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/edit_file.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/forge.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/git.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/image.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/io.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/list_dir.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/lsp.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/memory.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/paths.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/read_file.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/registry.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/run_bash.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/search.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/selection.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/shell.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/skill.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/task.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/todo.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/web.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/tools/write_file.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/ui.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/updater.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder/web.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder.egg-info/dependency_links.txt +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder.egg-info/entry_points.txt +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder.egg-info/requires.txt +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/kiwimatecoder.egg-info/top_level.txt +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/setup.cfg +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_accessibility.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_acp_protocol.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_acp_server.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_agent.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_ai.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_anthropic.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_audit.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_browser.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_catalog.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_checkpoints.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_ci_assets.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_cli_interfaces.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_cli_markup.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_client.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_clipboard.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_commands.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_config.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_config_menu.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_config_nonfinite.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_config_shapes.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_config_v2.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_diagnostics.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_documents.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_edit.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_evals.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_events.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_forge.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_git.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_headless.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_hooks.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_hunks.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_i18n.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_images.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_index.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_instructions.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_io_tools.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_jobs.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_lsp.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_lsp_protocol.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_main.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_markup_literals.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_mcp.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_media.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_media_models.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_media_video.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_memory.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_network.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_notify.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_output_markup.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_packaging_assets.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_permissions.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_pickers.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_plugins.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_pricing.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_profiles.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_prompts.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_providers.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_redaction.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_remote.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_repl.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_routing.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_sandbox.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_sdk.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_search_optimized.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_session_persistence.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_share.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_shell.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_skills.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_slash_command_coverage.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_subagents.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_sync.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_team.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_telemetry.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_templates.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_tools.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_ui.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_updater.py +0 -0
- {kiwimatecoder-0.1.0 → kiwimatecoder-0.2.0}/tests/test_web.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kiwimatecoder
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: AI-powered coding assistant CLI. Bring your own API key.
|
|
5
5
|
License: MIT License
|
|
6
6
|
|
|
@@ -217,6 +217,7 @@ paths outside the workspace root; writes stay sandboxed.
|
|
|
217
217
|
| `/dry-run [on\|off\|toggle]` | Preview mutating actions without running them. |
|
|
218
218
|
| `/undo [count]`, `/checkpoints` | Restore files changed by recent tools, or list checkpoints. |
|
|
219
219
|
| `/todos` | Show the agent's task list. |
|
|
220
|
+
| `/goal [--rounds N] <objective>`, `/goal pause\|resume\|clear` | Keep the agent working on an objective across rounds until it reports the goal achieved or blocked, or the round limit is reached. Bare `/goal` shows the current goal (see [Goals](#goals)). |
|
|
220
221
|
| `/templates` | List custom prompt templates discovered in the workspace and user command directories. |
|
|
221
222
|
| `/compact [budget]` | Trim older history to fit a token budget. |
|
|
222
223
|
| `/save`, `/load`, `/sessions`, `/fork [name]`, `/export [path]` | Save, resume, branch, or export sessions as Markdown. |
|
|
@@ -316,6 +317,8 @@ The assistant has these capabilities, all scoped to the workspace:
|
|
|
316
317
|
- `shell_jobs` — start, list, read, or kill detached background commands
|
|
317
318
|
(approval-gated; see below).
|
|
318
319
|
- `update_todos` — keep the visible task list in sync with multi-step work.
|
|
320
|
+
- `update_goal` — report progress on, or the outcome of, a `/goal`. It is only
|
|
321
|
+
offered to the model while a goal is active (see Goals below).
|
|
319
322
|
- `task` — delegate a focused investigation to a subagent with its own context
|
|
320
323
|
(approval-gated; see below).
|
|
321
324
|
- `ask_user` — ask a clarifying question with optional choices (interactive
|
|
@@ -1659,6 +1662,38 @@ kiwimatecoder config verify show
|
|
|
1659
1662
|
kiwimatecoder config verify clear
|
|
1660
1663
|
```
|
|
1661
1664
|
|
|
1665
|
+
## Goals
|
|
1666
|
+
|
|
1667
|
+
`/goal` keeps the agent working on an objective across several rounds, so a
|
|
1668
|
+
larger task can run without you prompting each step:
|
|
1669
|
+
|
|
1670
|
+
```text
|
|
1671
|
+
/goal make every test in tests/parser pass without changing expected output
|
|
1672
|
+
/goal --rounds 20 migrate the config loader to TOML
|
|
1673
|
+
/goal
|
|
1674
|
+
/goal pause
|
|
1675
|
+
/goal resume
|
|
1676
|
+
/goal clear
|
|
1677
|
+
```
|
|
1678
|
+
|
|
1679
|
+
Each round is an ordinary agent turn. After a round, the agent starts another
|
|
1680
|
+
unless it reports the goal with `update_goal`:
|
|
1681
|
+
|
|
1682
|
+
- `achieved`, once it has checked the result (the summary says how);
|
|
1683
|
+
- `blocked`, when it needs something only you can provide (the summary says what).
|
|
1684
|
+
|
|
1685
|
+
A goal also pauses when the round limit is reached (10 by default, up to 100
|
|
1686
|
+
with `--rounds`), when a round ends abnormally (the budget is spent, the provider
|
|
1687
|
+
fails, or the tool-call limit is hit), or when you press Ctrl-C or run
|
|
1688
|
+
`/goal pause`. Messages you type during a round steer it, as usual.
|
|
1689
|
+
`/goal resume` gives it a fresh round allowance. A goal saved with `/save`
|
|
1690
|
+
comes back paused, so nothing runs until you resume it.
|
|
1691
|
+
|
|
1692
|
+
Permission prompts still apply to every tool call. For an unattended run, pick
|
|
1693
|
+
`/mode auto-accept`, or answer `a` (always) at a prompt for the tools you trust.
|
|
1694
|
+
If the objective starts with a word such as `clear`, put `--` first:
|
|
1695
|
+
`/goal -- clear the build cache and rerun the tests`.
|
|
1696
|
+
|
|
1662
1697
|
## Budgets
|
|
1663
1698
|
|
|
1664
1699
|
Cap a session's spend by tokens, cost, or both. The agent warns at 80% and
|
|
@@ -176,6 +176,7 @@ paths outside the workspace root; writes stay sandboxed.
|
|
|
176
176
|
| `/dry-run [on\|off\|toggle]` | Preview mutating actions without running them. |
|
|
177
177
|
| `/undo [count]`, `/checkpoints` | Restore files changed by recent tools, or list checkpoints. |
|
|
178
178
|
| `/todos` | Show the agent's task list. |
|
|
179
|
+
| `/goal [--rounds N] <objective>`, `/goal pause\|resume\|clear` | Keep the agent working on an objective across rounds until it reports the goal achieved or blocked, or the round limit is reached. Bare `/goal` shows the current goal (see [Goals](#goals)). |
|
|
179
180
|
| `/templates` | List custom prompt templates discovered in the workspace and user command directories. |
|
|
180
181
|
| `/compact [budget]` | Trim older history to fit a token budget. |
|
|
181
182
|
| `/save`, `/load`, `/sessions`, `/fork [name]`, `/export [path]` | Save, resume, branch, or export sessions as Markdown. |
|
|
@@ -275,6 +276,8 @@ The assistant has these capabilities, all scoped to the workspace:
|
|
|
275
276
|
- `shell_jobs` — start, list, read, or kill detached background commands
|
|
276
277
|
(approval-gated; see below).
|
|
277
278
|
- `update_todos` — keep the visible task list in sync with multi-step work.
|
|
279
|
+
- `update_goal` — report progress on, or the outcome of, a `/goal`. It is only
|
|
280
|
+
offered to the model while a goal is active (see Goals below).
|
|
278
281
|
- `task` — delegate a focused investigation to a subagent with its own context
|
|
279
282
|
(approval-gated; see below).
|
|
280
283
|
- `ask_user` — ask a clarifying question with optional choices (interactive
|
|
@@ -1618,6 +1621,38 @@ kiwimatecoder config verify show
|
|
|
1618
1621
|
kiwimatecoder config verify clear
|
|
1619
1622
|
```
|
|
1620
1623
|
|
|
1624
|
+
## Goals
|
|
1625
|
+
|
|
1626
|
+
`/goal` keeps the agent working on an objective across several rounds, so a
|
|
1627
|
+
larger task can run without you prompting each step:
|
|
1628
|
+
|
|
1629
|
+
```text
|
|
1630
|
+
/goal make every test in tests/parser pass without changing expected output
|
|
1631
|
+
/goal --rounds 20 migrate the config loader to TOML
|
|
1632
|
+
/goal
|
|
1633
|
+
/goal pause
|
|
1634
|
+
/goal resume
|
|
1635
|
+
/goal clear
|
|
1636
|
+
```
|
|
1637
|
+
|
|
1638
|
+
Each round is an ordinary agent turn. After a round, the agent starts another
|
|
1639
|
+
unless it reports the goal with `update_goal`:
|
|
1640
|
+
|
|
1641
|
+
- `achieved`, once it has checked the result (the summary says how);
|
|
1642
|
+
- `blocked`, when it needs something only you can provide (the summary says what).
|
|
1643
|
+
|
|
1644
|
+
A goal also pauses when the round limit is reached (10 by default, up to 100
|
|
1645
|
+
with `--rounds`), when a round ends abnormally (the budget is spent, the provider
|
|
1646
|
+
fails, or the tool-call limit is hit), or when you press Ctrl-C or run
|
|
1647
|
+
`/goal pause`. Messages you type during a round steer it, as usual.
|
|
1648
|
+
`/goal resume` gives it a fresh round allowance. A goal saved with `/save`
|
|
1649
|
+
comes back paused, so nothing runs until you resume it.
|
|
1650
|
+
|
|
1651
|
+
Permission prompts still apply to every tool call. For an unattended run, pick
|
|
1652
|
+
`/mode auto-accept`, or answer `a` (always) at a prompt for the tools you trust.
|
|
1653
|
+
If the objective starts with a word such as `clear`, put `--` first:
|
|
1654
|
+
`/goal -- clear the build cache and rerun the tests`.
|
|
1655
|
+
|
|
1621
1656
|
## Budgets
|
|
1622
1657
|
|
|
1623
1658
|
Cap a session's spend by tokens, cost, or both. The agent warns at 80% and
|
|
@@ -116,6 +116,9 @@ class Agent:
|
|
|
116
116
|
self.confirm = confirm
|
|
117
117
|
self.bus = bus if bus is not None else events.BUS
|
|
118
118
|
self._budget_warned = False
|
|
119
|
+
# Why the most recent run_turn ended ("stop", "budget", ...); the REPL
|
|
120
|
+
# reads it to decide whether a /goal keeps going. None while a turn runs.
|
|
121
|
+
self.last_stop_reason: str | None = None
|
|
119
122
|
self.ascii_mode = bool(ascii_mode)
|
|
120
123
|
self.output_mode = output_mode if output_mode in ui.OUTPUT_MODES else "normal"
|
|
121
124
|
self.event_handler = event_handler
|
|
@@ -133,6 +136,11 @@ class Agent:
|
|
|
133
136
|
except Exception:
|
|
134
137
|
_log.debug("event handler failed for %s", name, exc_info=True)
|
|
135
138
|
|
|
139
|
+
def _done(self, reason: str, **payload: Any) -> None:
|
|
140
|
+
"""Record how a turn ended, then tell the event handler."""
|
|
141
|
+
self.last_stop_reason = reason
|
|
142
|
+
self._emit("done", reason=reason, **payload)
|
|
143
|
+
|
|
136
144
|
def _redacted_args(self, args: dict[str, Any]) -> dict[str, Any]:
|
|
137
145
|
"""Return a JSON-safe copy of tool arguments with secrets redacted."""
|
|
138
146
|
try:
|
|
@@ -211,10 +219,11 @@ class Agent:
|
|
|
211
219
|
|
|
212
220
|
async def run_turn(self, user_input: str) -> None:
|
|
213
221
|
"""Process one user message, looping over tool calls until the model stops."""
|
|
222
|
+
self.last_stop_reason = None
|
|
214
223
|
blocked, reason = self._budget_exceeded()
|
|
215
224
|
if blocked:
|
|
216
225
|
self.console.print(f"[red]{escape(reason)}[/red]")
|
|
217
|
-
self.
|
|
226
|
+
self._done("budget", error=reason)
|
|
218
227
|
return
|
|
219
228
|
|
|
220
229
|
from kiwimatecoder.config import get_model_routing
|
|
@@ -246,7 +255,7 @@ class Agent:
|
|
|
246
255
|
except ProviderError as exc:
|
|
247
256
|
self.console.print(f"\n[red]{escape(str(exc))}[/red]")
|
|
248
257
|
self._emit("error", kind="provider", error=str(exc))
|
|
249
|
-
self.
|
|
258
|
+
self._done("provider_error")
|
|
250
259
|
return
|
|
251
260
|
|
|
252
261
|
self.session.messages.append(assistant_msg)
|
|
@@ -260,11 +269,11 @@ class Agent:
|
|
|
260
269
|
# The model must answer the steered message.
|
|
261
270
|
continue
|
|
262
271
|
self._warn_budget()
|
|
263
|
-
self.
|
|
272
|
+
self._done("stop")
|
|
264
273
|
return
|
|
265
274
|
|
|
266
275
|
if self.max_turns is not None and tool_turns >= self.max_turns:
|
|
267
|
-
self.
|
|
276
|
+
self._done("max_turns", limit=self.max_turns)
|
|
268
277
|
return
|
|
269
278
|
edited = await self._handle_tool_calls(tool_calls) or edited
|
|
270
279
|
tool_turns += 1
|
|
@@ -384,9 +393,11 @@ class Agent:
|
|
|
384
393
|
rest as a second line of defense). Subagents never see ``task`` (no
|
|
385
394
|
nested subagents) or ``ask_user`` (no interactive user), and ``task``
|
|
386
395
|
is hidden for everyone while subagents are disabled. ``browser`` is
|
|
387
|
-
hidden until browser automation is enabled in config.
|
|
396
|
+
hidden until browser automation is enabled in config. ``update_goal``
|
|
397
|
+
is offered only while a /goal is active.
|
|
388
398
|
"""
|
|
389
399
|
from kiwimatecoder.config import get_browser, get_subagents
|
|
400
|
+
from kiwimatecoder.goal import ACTIVE
|
|
390
401
|
|
|
391
402
|
schemas = tools.tool_schemas(
|
|
392
403
|
read_only=self.session.mode is PermissionMode.PLAN
|
|
@@ -398,6 +409,9 @@ class Agent:
|
|
|
398
409
|
excluded.add("task")
|
|
399
410
|
if not get_browser()["enabled"]:
|
|
400
411
|
excluded.add("browser")
|
|
412
|
+
goal = self.session.goal
|
|
413
|
+
if goal is None or goal.status != ACTIVE:
|
|
414
|
+
excluded.add("update_goal")
|
|
401
415
|
if excluded:
|
|
402
416
|
schemas = [
|
|
403
417
|
schema
|
|
@@ -18,6 +18,7 @@ from rich.markup import escape
|
|
|
18
18
|
from rich.panel import Panel
|
|
19
19
|
from rich.table import Table
|
|
20
20
|
|
|
21
|
+
from kiwimatecoder import goal as goal_module
|
|
21
22
|
from kiwimatecoder import tools
|
|
22
23
|
from kiwimatecoder import media as media_module
|
|
23
24
|
from kiwimatecoder import memory as memory_module
|
|
@@ -956,13 +957,17 @@ def _expand_context_input(
|
|
|
956
957
|
if pattern.is_absolute() or pattern.root or pattern.drive:
|
|
957
958
|
return [], [f"Glob patterns must be relative to the workspace: {raw_path}"]
|
|
958
959
|
|
|
960
|
+
# Python 3.10-3.12 raise ValueError for "a**b", but 3.13 just returns no matches,
|
|
961
|
+
# so the rule is checked here: '**' is a wildcard only as a whole path component.
|
|
962
|
+
if any("**" in part and part != "**" for part in pattern.parts):
|
|
963
|
+
return [], ["Invalid glob pattern: '**' can only be an entire path component"]
|
|
964
|
+
|
|
959
965
|
root = session.workspace_root.resolve()
|
|
960
966
|
errors: list[str] = []
|
|
961
967
|
matches: list[Path] = []
|
|
962
968
|
try:
|
|
963
969
|
candidates = sorted(root.glob(raw_path))
|
|
964
970
|
except (NotImplementedError, ValueError, OSError) as exc:
|
|
965
|
-
# e.g. "a**b": '**' can only be an entire path component
|
|
966
971
|
return [], [f"Invalid glob pattern: {exc}"]
|
|
967
972
|
for candidate in candidates:
|
|
968
973
|
try:
|
|
@@ -6279,6 +6284,92 @@ def _media(
|
|
|
6279
6284
|
return CommandResult.CONTINUE
|
|
6280
6285
|
|
|
6281
6286
|
|
|
6287
|
+
def _goal(arg: str, session: Session, console: Console) -> str:
|
|
6288
|
+
"""Show, set, pause, resume or clear the session goal (see kiwimatecoder.goal).
|
|
6289
|
+
|
|
6290
|
+
Setting or resuming a goal only marks it due; the REPL runs the rounds once
|
|
6291
|
+
this command returns.
|
|
6292
|
+
"""
|
|
6293
|
+
try:
|
|
6294
|
+
command = goal_module.parse_goal_command(arg)
|
|
6295
|
+
except ValueError as exc:
|
|
6296
|
+
console.print(f"[yellow]{escape(str(exc))}[/yellow]")
|
|
6297
|
+
return CommandResult.CONTINUE
|
|
6298
|
+
|
|
6299
|
+
goal = session.goal
|
|
6300
|
+
if command.verb == "show":
|
|
6301
|
+
_print_goal(goal, console)
|
|
6302
|
+
elif command.verb == "set":
|
|
6303
|
+
session.goal = goal_module.new_goal(command.objective, command.max_rounds)
|
|
6304
|
+
console.print(
|
|
6305
|
+
f"[green]Goal set[/green] (up to {session.goal.max_rounds} rounds): "
|
|
6306
|
+
f"{escape(session.goal.objective)}"
|
|
6307
|
+
)
|
|
6308
|
+
console.print(
|
|
6309
|
+
"[dim]Ctrl-C stops it. Permission prompts still apply; /mode auto-accept "
|
|
6310
|
+
"lets it run unattended.[/dim]"
|
|
6311
|
+
)
|
|
6312
|
+
elif command.verb == "pause":
|
|
6313
|
+
if goal is None or goal.status != goal_module.ACTIVE:
|
|
6314
|
+
console.print("[yellow]No running goal to pause.[/yellow]")
|
|
6315
|
+
else:
|
|
6316
|
+
# Not due means a round is in progress, which finishes before the pause.
|
|
6317
|
+
running = not goal.due
|
|
6318
|
+
goal_module.pause(goal, "paused by you")
|
|
6319
|
+
console.print(
|
|
6320
|
+
"[yellow]Goal will pause after the current round.[/yellow]"
|
|
6321
|
+
if running
|
|
6322
|
+
else "[yellow]Goal paused.[/yellow]"
|
|
6323
|
+
)
|
|
6324
|
+
console.print("[dim]/goal resume continues it.[/dim]")
|
|
6325
|
+
elif command.verb == "resume":
|
|
6326
|
+
if goal is None:
|
|
6327
|
+
console.print("[yellow]No goal to resume. Set one with /goal <objective>.[/yellow]")
|
|
6328
|
+
elif goal.status == goal_module.ACHIEVED:
|
|
6329
|
+
console.print(
|
|
6330
|
+
"[dim]That goal is already achieved. Set a new one with /goal <objective>.[/dim]"
|
|
6331
|
+
)
|
|
6332
|
+
elif goal.status == goal_module.ACTIVE:
|
|
6333
|
+
console.print("[dim]The goal is already running.[/dim]")
|
|
6334
|
+
else:
|
|
6335
|
+
goal_module.resume(goal)
|
|
6336
|
+
console.print(f"[green]Resuming goal[/green] (up to {goal.max_rounds} more rounds).")
|
|
6337
|
+
elif command.verb == "clear":
|
|
6338
|
+
if goal is None:
|
|
6339
|
+
console.print("[dim]No goal to clear.[/dim]")
|
|
6340
|
+
else:
|
|
6341
|
+
session.goal = None
|
|
6342
|
+
console.print("[dim]Goal cleared.[/dim]")
|
|
6343
|
+
return CommandResult.CONTINUE
|
|
6344
|
+
|
|
6345
|
+
|
|
6346
|
+
def _print_goal(goal: goal_module.Goal | None, console: Console) -> None:
|
|
6347
|
+
if goal is None:
|
|
6348
|
+
console.print("[dim]No goal set. Start one with /goal <objective>.[/dim]")
|
|
6349
|
+
return
|
|
6350
|
+
detail = f"{goal.status}, round {goal.rounds} of {goal.max_rounds}"
|
|
6351
|
+
if not goal.rounds:
|
|
6352
|
+
detail = f"{goal.status}, up to {goal.max_rounds} rounds"
|
|
6353
|
+
console.print(f"[bold]Goal:[/bold] {escape(goal.objective)}")
|
|
6354
|
+
console.print(f"Status: {escape(detail)}")
|
|
6355
|
+
if goal.progress:
|
|
6356
|
+
console.print(f"Last progress: {escape(goal.progress)}")
|
|
6357
|
+
labels = {
|
|
6358
|
+
goal_module.ACHIEVED: "Result",
|
|
6359
|
+
goal_module.BLOCKED: "Needs",
|
|
6360
|
+
goal_module.PAUSED: "Why",
|
|
6361
|
+
}
|
|
6362
|
+
if goal.outcome and goal.status in labels:
|
|
6363
|
+
console.print(f"{labels[goal.status]}: {escape(goal.outcome)}")
|
|
6364
|
+
hints = {
|
|
6365
|
+
goal_module.ACTIVE: "Running. /goal pause stops it after this round.",
|
|
6366
|
+
goal_module.PAUSED: "/goal resume continues it. /goal clear drops it.",
|
|
6367
|
+
goal_module.BLOCKED: "Answer the question above, then /goal resume.",
|
|
6368
|
+
goal_module.ACHIEVED: "Done. Set a new goal with /goal <objective>.",
|
|
6369
|
+
}
|
|
6370
|
+
console.print(f"[dim]{hints[goal.status]}[/dim]")
|
|
6371
|
+
|
|
6372
|
+
|
|
6282
6373
|
_COMMANDS: dict[str, Callable[[str, Session, Console], str]] = {
|
|
6283
6374
|
"help": _help,
|
|
6284
6375
|
"exit": _exit,
|
|
@@ -6311,6 +6402,7 @@ _COMMANDS: dict[str, Callable[[str, Session, Console], str]] = {
|
|
|
6311
6402
|
"rewind": _undo,
|
|
6312
6403
|
"checkpoints": _checkpoints,
|
|
6313
6404
|
"todos": _todos,
|
|
6405
|
+
"goal": _goal,
|
|
6314
6406
|
"memory": _memory,
|
|
6315
6407
|
"compact": _compact,
|
|
6316
6408
|
"index": _index,
|
|
@@ -6400,6 +6492,12 @@ _HELP_GROUPS = [
|
|
|
6400
6492
|
("/undo [count]", "Restore files changed by recent tool actions."),
|
|
6401
6493
|
("/checkpoints", "List captured file checkpoints."),
|
|
6402
6494
|
("/todos", "Show the agent's task list."),
|
|
6495
|
+
(
|
|
6496
|
+
"/goal [--rounds N] <objective> | pause | resume | clear",
|
|
6497
|
+
"Work toward an objective across rounds until the agent reports it "
|
|
6498
|
+
"achieved or blocked, or the round limit (default 10) is reached. "
|
|
6499
|
+
"Bare /goal shows the current goal.",
|
|
6500
|
+
),
|
|
6403
6501
|
(
|
|
6404
6502
|
"/memory [list|add|add-user|clear]",
|
|
6405
6503
|
"Show or edit persistent project and user memory.",
|
|
@@ -6534,6 +6632,7 @@ _COMMAND_DESCRIPTIONS = {
|
|
|
6534
6632
|
"rewind": "Alias for /undo.",
|
|
6535
6633
|
"checkpoints": "List captured file checkpoints.",
|
|
6536
6634
|
"todos": "Show the agent's task list.",
|
|
6635
|
+
"goal": "Work toward an objective across rounds, or show, pause, resume, or clear it.",
|
|
6537
6636
|
"memory": "Show or edit persistent project and user memory.",
|
|
6538
6637
|
"compact": "Trim older history to fit a token budget.",
|
|
6539
6638
|
"index": "Show, refresh, or delete the codebase index used by semantic search.",
|
|
@@ -6576,6 +6675,12 @@ _MODE_DESCRIPTIONS = {
|
|
|
6576
6675
|
"plan": "Read-only planning mode.",
|
|
6577
6676
|
}
|
|
6578
6677
|
|
|
6678
|
+
_GOAL_ACTION_DESCRIPTIONS = {
|
|
6679
|
+
"pause": "Stop the goal after the current round.",
|
|
6680
|
+
"resume": "Continue a paused or blocked goal.",
|
|
6681
|
+
"clear": "Drop the goal.",
|
|
6682
|
+
}
|
|
6683
|
+
|
|
6579
6684
|
_MEMORY_ACTION_DESCRIPTIONS = {
|
|
6580
6685
|
"list": "Show project and user memory.",
|
|
6581
6686
|
"add": "Append a fact to project memory.",
|
|
@@ -6717,6 +6822,8 @@ def slash_argument_completions(
|
|
|
6717
6822
|
choices = _MODE_DESCRIPTIONS
|
|
6718
6823
|
elif command == "memory":
|
|
6719
6824
|
choices = _MEMORY_ACTION_DESCRIPTIONS
|
|
6825
|
+
elif command == "goal":
|
|
6826
|
+
choices = _GOAL_ACTION_DESCRIPTIONS
|
|
6720
6827
|
elif command == "index":
|
|
6721
6828
|
choices = _INDEX_ACTION_DESCRIPTIONS
|
|
6722
6829
|
elif command == "model" and session is not None:
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""Session goals: an objective the agent keeps working on across rounds.
|
|
2
|
+
|
|
3
|
+
``/goal <objective>`` sets one and the REPL runs agent rounds back to back until
|
|
4
|
+
the model reports it achieved or blocked, the round cap is reached, or a round
|
|
5
|
+
ends abnormally (budget, provider error, tool-call limit, Ctrl-C). This module
|
|
6
|
+
holds the state and builds the prompts; the REPL drives the rounds.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
ACTIVE = "active"
|
|
16
|
+
PAUSED = "paused"
|
|
17
|
+
ACHIEVED = "achieved"
|
|
18
|
+
BLOCKED = "blocked"
|
|
19
|
+
STATUSES = (ACTIVE, PAUSED, ACHIEVED, BLOCKED)
|
|
20
|
+
|
|
21
|
+
DEFAULT_MAX_ROUNDS = 10
|
|
22
|
+
MAX_ROUNDS_LIMIT = 100
|
|
23
|
+
MAX_OBJECTIVE_CHARS = 4000
|
|
24
|
+
|
|
25
|
+
# Why a round ended, from the agent's "done" reason. Anything but a normal stop
|
|
26
|
+
# pauses the goal, and the reason is shown so the user knows what happened.
|
|
27
|
+
_STOP_REASONS = {
|
|
28
|
+
"interrupted": "you interrupted it",
|
|
29
|
+
"budget": "the budget is spent",
|
|
30
|
+
"provider_error": "the provider returned an error",
|
|
31
|
+
"max_turns": "the tool-call limit was reached",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
_USAGE = (
|
|
35
|
+
"Usage: /goal <objective> · /goal --rounds N <objective> · "
|
|
36
|
+
"/goal pause | resume | clear · /goal"
|
|
37
|
+
)
|
|
38
|
+
_ROUNDS_FLAG = re.compile(r"^--rounds(?=\s|$)\s*(\S*)\s*(.*)$", re.DOTALL)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class Goal:
|
|
43
|
+
"""One standing objective and how far the agent has got with it."""
|
|
44
|
+
|
|
45
|
+
objective: str
|
|
46
|
+
max_rounds: int = DEFAULT_MAX_ROUNDS
|
|
47
|
+
status: str = ACTIVE
|
|
48
|
+
rounds: int = 0 # rounds run since the goal was set or last resumed
|
|
49
|
+
progress: str = "" # latest in_progress note from update_goal
|
|
50
|
+
outcome: str = "" # summary given with achieved/blocked, or why it paused
|
|
51
|
+
due: bool = True # runtime only: another round should run now
|
|
52
|
+
|
|
53
|
+
def to_dict(self) -> dict[str, Any]:
|
|
54
|
+
return {
|
|
55
|
+
"objective": self.objective,
|
|
56
|
+
"max_rounds": self.max_rounds,
|
|
57
|
+
"status": self.status,
|
|
58
|
+
"rounds": self.rounds,
|
|
59
|
+
"progress": self.progress,
|
|
60
|
+
"outcome": self.outcome,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def from_dict(cls, data: Any) -> Goal | None:
|
|
65
|
+
"""Restore a saved goal. A goal that was running is restored as paused,
|
|
66
|
+
so the user decides when to continue it (``/goal resume``)."""
|
|
67
|
+
if not isinstance(data, dict):
|
|
68
|
+
return None
|
|
69
|
+
raw_objective = data.get("objective")
|
|
70
|
+
objective = raw_objective.strip() if isinstance(raw_objective, str) else ""
|
|
71
|
+
if not objective:
|
|
72
|
+
return None
|
|
73
|
+
status = str(data.get("status") or PAUSED)
|
|
74
|
+
outcome = str(data.get("outcome") or "")
|
|
75
|
+
if status not in STATUSES or status == ACTIVE:
|
|
76
|
+
status = PAUSED
|
|
77
|
+
outcome = "restored from a saved session"
|
|
78
|
+
return cls(
|
|
79
|
+
objective=objective,
|
|
80
|
+
max_rounds=_clamp_rounds(data.get("max_rounds")),
|
|
81
|
+
status=status,
|
|
82
|
+
rounds=max(0, _as_int(data.get("rounds"))),
|
|
83
|
+
progress=str(data.get("progress") or ""),
|
|
84
|
+
outcome=outcome,
|
|
85
|
+
due=False,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass(frozen=True)
|
|
90
|
+
class GoalCommand:
|
|
91
|
+
"""A parsed ``/goal`` argument."""
|
|
92
|
+
|
|
93
|
+
verb: str # "show", "set", "pause", "resume" or "clear"
|
|
94
|
+
objective: str = ""
|
|
95
|
+
max_rounds: int = DEFAULT_MAX_ROUNDS
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def parse_goal_command(arg: str) -> GoalCommand:
|
|
99
|
+
"""Parse the text after ``/goal``. Raises ``ValueError`` with a message."""
|
|
100
|
+
text = arg.strip()
|
|
101
|
+
if not text:
|
|
102
|
+
return GoalCommand("show")
|
|
103
|
+
|
|
104
|
+
# "--" makes the rest an objective even when it reads like a verb.
|
|
105
|
+
if text == "--" or text.startswith("-- "):
|
|
106
|
+
objective = text[2:].strip()
|
|
107
|
+
if not objective:
|
|
108
|
+
raise ValueError(_USAGE)
|
|
109
|
+
return GoalCommand("set", _checked_objective(objective))
|
|
110
|
+
|
|
111
|
+
max_rounds = DEFAULT_MAX_ROUNDS
|
|
112
|
+
flag = _ROUNDS_FLAG.match(text)
|
|
113
|
+
if flag:
|
|
114
|
+
value, text = flag.group(1), flag.group(2).strip()
|
|
115
|
+
# isascii: str.isdigit() accepts forms such as "²" that int() rejects.
|
|
116
|
+
if not (value.isascii() and value.isdigit()) or not (
|
|
117
|
+
1 <= int(value) <= MAX_ROUNDS_LIMIT
|
|
118
|
+
):
|
|
119
|
+
raise ValueError(f"--rounds takes a number from 1 to {MAX_ROUNDS_LIMIT}.")
|
|
120
|
+
max_rounds = int(value)
|
|
121
|
+
if not text:
|
|
122
|
+
raise ValueError("--rounds needs an objective, e.g. /goal --rounds 20 fix the tests.")
|
|
123
|
+
return GoalCommand("set", _checked_objective(text), max_rounds)
|
|
124
|
+
|
|
125
|
+
verb = text.lower()
|
|
126
|
+
if verb in ("pause", "resume", "clear"):
|
|
127
|
+
return GoalCommand(verb)
|
|
128
|
+
return GoalCommand("set", _checked_objective(text), max_rounds)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _checked_objective(text: str) -> str:
|
|
132
|
+
if len(text) > MAX_OBJECTIVE_CHARS:
|
|
133
|
+
raise ValueError(f"A goal can be at most {MAX_OBJECTIVE_CHARS} characters.")
|
|
134
|
+
return text
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def new_goal(objective: str, max_rounds: int = DEFAULT_MAX_ROUNDS) -> Goal:
|
|
138
|
+
return Goal(objective=objective.strip(), max_rounds=_clamp_rounds(max_rounds))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def start_round(goal: Goal) -> str:
|
|
142
|
+
"""Begin the next round and return the user message that starts it."""
|
|
143
|
+
goal.rounds += 1
|
|
144
|
+
goal.due = False
|
|
145
|
+
if goal.rounds == 1:
|
|
146
|
+
return opening_prompt(goal)
|
|
147
|
+
return continuation_prompt(goal)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def finish_round(goal: Goal, stop_reason: str | None) -> None:
|
|
151
|
+
"""Decide, after a round, whether another round is due.
|
|
152
|
+
|
|
153
|
+
The goal keeps going only when the model stopped on its own and the round
|
|
154
|
+
budget is not spent. A status set by ``update_goal`` during the round
|
|
155
|
+
(achieved or blocked) is left alone.
|
|
156
|
+
"""
|
|
157
|
+
goal.due = False
|
|
158
|
+
if goal.status != ACTIVE:
|
|
159
|
+
return
|
|
160
|
+
if stop_reason != "stop":
|
|
161
|
+
pause(goal, _STOP_REASONS.get(stop_reason or "", "the round ended early"))
|
|
162
|
+
return
|
|
163
|
+
if goal.rounds >= goal.max_rounds:
|
|
164
|
+
pause(goal, f"reached the {goal.max_rounds}-round limit")
|
|
165
|
+
return
|
|
166
|
+
goal.due = True
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def pause(goal: Goal, reason: str) -> None:
|
|
170
|
+
goal.status = PAUSED
|
|
171
|
+
goal.outcome = reason
|
|
172
|
+
goal.due = False
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def resume(goal: Goal) -> None:
|
|
176
|
+
"""Make the goal active again with a fresh round allowance."""
|
|
177
|
+
goal.status = ACTIVE
|
|
178
|
+
goal.rounds = 0
|
|
179
|
+
goal.outcome = ""
|
|
180
|
+
goal.due = True
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def close(goal: Goal, status: str, summary: str) -> None:
|
|
184
|
+
"""Record the outcome reported through ``update_goal``."""
|
|
185
|
+
goal.status = status
|
|
186
|
+
goal.outcome = summary
|
|
187
|
+
goal.due = False
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def opening_prompt(goal: Goal) -> str:
|
|
191
|
+
return (
|
|
192
|
+
"You are working toward a goal the user set. Work through it step by step, "
|
|
193
|
+
"using tools as needed, until the objective is fully met. Do not stop to ask "
|
|
194
|
+
"for confirmation unless you are genuinely blocked.\n\n"
|
|
195
|
+
f"Goal: {goal.objective}\n\n"
|
|
196
|
+
"Report through the update_goal tool:\n"
|
|
197
|
+
'- status "in_progress" with a one-line summary after meaningful progress;\n'
|
|
198
|
+
'- status "achieved" only once you have checked the result, with a summary of '
|
|
199
|
+
"how you verified it;\n"
|
|
200
|
+
'- status "blocked" only if you need something only the user can provide, with '
|
|
201
|
+
"a summary of exactly what you need."
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def continuation_prompt(goal: Goal) -> str:
|
|
206
|
+
note = (
|
|
207
|
+
f"Last progress note: {goal.progress}"
|
|
208
|
+
if goal.progress
|
|
209
|
+
else "No progress note has been recorded yet."
|
|
210
|
+
)
|
|
211
|
+
return (
|
|
212
|
+
f"Keep working toward the goal (round {goal.rounds} of {goal.max_rounds}).\n"
|
|
213
|
+
f"Goal: {goal.objective}\n"
|
|
214
|
+
f"{note}\n\n"
|
|
215
|
+
"Continue from where you left off. When the objective is fully met and "
|
|
216
|
+
'verified, call update_goal with status "achieved"; if you are blocked on '
|
|
217
|
+
'something only the user can give, call it with status "blocked".'
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _clamp_rounds(value: Any) -> int:
|
|
222
|
+
return min(MAX_ROUNDS_LIMIT, max(1, _as_int(value, DEFAULT_MAX_ROUNDS)))
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _as_int(value: Any, default: int = 0) -> int:
|
|
226
|
+
try:
|
|
227
|
+
return int(value)
|
|
228
|
+
except (TypeError, ValueError):
|
|
229
|
+
return default
|
|
@@ -60,6 +60,7 @@ from rich.panel import Panel
|
|
|
60
60
|
from rich.syntax import Syntax
|
|
61
61
|
|
|
62
62
|
from kiwimatecoder import __version__, browser, events, hooks, images, lsp, mcp, notify, plugins, ui
|
|
63
|
+
from kiwimatecoder import goal as goal_module
|
|
63
64
|
from kiwimatecoder.agent import Agent
|
|
64
65
|
from kiwimatecoder.commands import (
|
|
65
66
|
CommandOption,
|
|
@@ -1168,6 +1169,7 @@ async def _run_turn_with_steering(
|
|
|
1168
1169
|
raise
|
|
1169
1170
|
|
|
1170
1171
|
if interrupted:
|
|
1172
|
+
agent.last_stop_reason = "interrupted"
|
|
1171
1173
|
console.print(f"\n[yellow]{t('prompt.interrupted')}[/yellow]")
|
|
1172
1174
|
|
|
1173
1175
|
if await _process_deferred_commands(session):
|
|
@@ -1175,6 +1177,48 @@ async def _run_turn_with_steering(
|
|
|
1175
1177
|
return exit_requested
|
|
1176
1178
|
|
|
1177
1179
|
|
|
1180
|
+
def _announce_goal_stop(goal: goal_module.Goal) -> None:
|
|
1181
|
+
"""Say why a goal stopped after a round (it is not due another one)."""
|
|
1182
|
+
if goal.status == goal_module.ACHIEVED:
|
|
1183
|
+
console.print(
|
|
1184
|
+
f"[green]Goal achieved after {goal.rounds} round(s).[/green] "
|
|
1185
|
+
f"{escape(goal.outcome)}"
|
|
1186
|
+
)
|
|
1187
|
+
elif goal.status == goal_module.BLOCKED:
|
|
1188
|
+
console.print(
|
|
1189
|
+
f"[yellow]Goal blocked: {escape(goal.outcome)}[/yellow]\n"
|
|
1190
|
+
"[dim]Answer above, then /goal resume.[/dim]"
|
|
1191
|
+
)
|
|
1192
|
+
elif goal.status == goal_module.PAUSED:
|
|
1193
|
+
console.print(
|
|
1194
|
+
f"[yellow]Goal paused: {escape(goal.outcome)}.[/yellow] "
|
|
1195
|
+
"[dim]/goal resume continues it, /goal clear drops it.[/dim]"
|
|
1196
|
+
)
|
|
1197
|
+
|
|
1198
|
+
|
|
1199
|
+
async def _drive_goal(
|
|
1200
|
+
agent: Agent, pt_session: PromptSession[str], session: Session
|
|
1201
|
+
) -> bool:
|
|
1202
|
+
"""Run goal rounds back to back while the session goal is due.
|
|
1203
|
+
|
|
1204
|
+
Each round is an ordinary agent turn, so steering, Ctrl-C and approvals all
|
|
1205
|
+
work as they do elsewhere. Returns True when the REPL should exit.
|
|
1206
|
+
"""
|
|
1207
|
+
while session.goal is not None and session.goal.due:
|
|
1208
|
+
goal = session.goal
|
|
1209
|
+
prompt = goal_module.start_round(goal)
|
|
1210
|
+
# Plain text rather than a box-drawing rule, which ASCII mode cannot show.
|
|
1211
|
+
console.print(f"[dim]goal round {goal.rounds} of {goal.max_rounds}[/dim]")
|
|
1212
|
+
if await _run_turn_with_steering(agent, pt_session, session, prompt):
|
|
1213
|
+
return True
|
|
1214
|
+
# A /goal command may have replaced or cleared the goal mid-round.
|
|
1215
|
+
if session.goal is goal:
|
|
1216
|
+
goal_module.finish_round(goal, agent.last_stop_reason)
|
|
1217
|
+
if not goal.due:
|
|
1218
|
+
_announce_goal_stop(goal)
|
|
1219
|
+
return False
|
|
1220
|
+
|
|
1221
|
+
|
|
1178
1222
|
def _run_lifecycle_hooks(
|
|
1179
1223
|
bus: events.EventBus, event: str, session: Session
|
|
1180
1224
|
) -> None:
|
|
@@ -1296,6 +1340,9 @@ async def _run_interactive(
|
|
|
1296
1340
|
|
|
1297
1341
|
try:
|
|
1298
1342
|
while True:
|
|
1343
|
+
# A goal set by /goal (or queued during a turn) runs before the next prompt.
|
|
1344
|
+
if await _drive_goal(agent, pt_session, session):
|
|
1345
|
+
break
|
|
1299
1346
|
try:
|
|
1300
1347
|
prompt_str = (
|
|
1301
1348
|
HTML("<ansicyan>... </ansicyan>")
|