limbo-code 0.3.5__tar.gz → 0.3.6__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.
- {limbo_code-0.3.5 → limbo_code-0.3.6}/PKG-INFO +1 -1
- {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/subagents.md +4 -1
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/agent.py +96 -13
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/app.py +10 -5
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/config.py +26 -13
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/llm/catalog.py +19 -1
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/subagents/profiles.py +13 -2
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/subagents/supervisor.py +94 -13
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/registry.py +5 -1
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/key_fixes.py +23 -7
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_catalog.py +13 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_daily_p1.py +26 -2
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_subagents.py +75 -0
- limbo_code-0.3.6/tests/ui/test_kitty_keyboard.py +62 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/.agents/skills/grill-with-docs/SKILL.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/.agents/skills/grill-with-docs/agents/openai.yaml +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/.agents/skills/improve-codebase-architecture/HTML-REPORT.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/.agents/skills/improve-codebase-architecture/SKILL.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/.agents/skills/improve-codebase-architecture/agents/openai.yaml +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/.agents/skills/tdd/SKILL.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/.agents/skills/tdd/agents/openai.yaml +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/.agents/skills/tdd/mocking.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/.agents/skills/tdd/tests.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/.github/workflows/publish.yml +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/.github/workflows/test.yml +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/.gitignore +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/AGENTS.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/CONTEXT.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/Makefile +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/README.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/design/bang-verification.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/design/confirm_view.html +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/design/limbo-ui-minimal.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/design/limbo-ui-redesign.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/design/prototype-minimal-confirm.png +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/design/prototype-minimal.html +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/design/prototype-minimal.png +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/design/prototype.html +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/design/prototype.png +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/design/rfc-bang-command.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/design/rfc-headless-cli.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/assets/limbo-current-ui.png +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/assets/limbo-hero.png +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/assets/limbo-hero.svg +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/assets/limbo-models.png +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/assets/limbo-models.svg +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/assets/limbo-new-ui.png +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/assets/walkthrough/limbo-dark/1_idle.svg +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/assets/walkthrough/limbo-dark/2_thinking_tool_running.svg +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/assets/walkthrough/limbo-dark/3_tool_success.svg +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/assets/walkthrough/limbo-dark/4_tool_error.svg +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/assets/walkthrough/limbo-dark/5_llm_error.svg +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/assets/walkthrough/limbo-dark/6_edit_diff.svg +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/assets/walkthrough/limbo-light/1_idle.svg +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/assets/walkthrough/limbo-light/2_thinking_tool_running.svg +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/assets/walkthrough/limbo-light/3_tool_success.svg +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/assets/walkthrough/limbo-light/4_tool_error.svg +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/assets/walkthrough/limbo-light/5_llm_error.svg +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/assets/walkthrough/limbo-light/6_edit_diff.svg +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/daily-mode.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/session-management.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/skills.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/ui-redesign-proposal.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/pyproject.toml +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/scripts/check_contrast.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/scripts/gen_banner.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/scripts/readme_hero.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/scripts/scroll_bench.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/scripts/ui_walkthrough.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/skills-lock.json +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/__init__.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/__main__.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/attachments.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/auth.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/btw.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/cli_auth.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/code_mode.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/compaction.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/conversation.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/README.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/arxiv/LICENSE +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/arxiv/SKILL.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/arxiv/scripts/search_arxiv.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/blocked-page-recovery/LICENSE +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/blocked-page-recovery/SKILL.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/blocked-page-recovery/scripts/recover_page.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/document-to-action-items/LICENSE +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/document-to-action-items/SKILL.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/docx/LICENSE +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/docx/SKILL.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/docx/references/revisions-and-comments.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/docx/scripts/docx_comments.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/docx/scripts/docx_common.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/docx/scripts/docx_create.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/docx/scripts/docx_edit.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/docx/scripts/docx_read.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/docx/scripts/docx_revisions.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/docx/scripts/docx_template.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/docx/scripts/docx_validate.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/grounded-citations/LICENSE +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/grounded-citations/SKILL.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/grounded-citations/references/citation-formats.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/grounded-citations/references/grounding-rationale.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/grounded-citations/scripts/sources.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/meeting-action-items/LICENSE +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/meeting-action-items/SKILL.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/LICENSE +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/SKILL.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/references/forms.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/references/nano-pdf-editing.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/references/ocr-extraction.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/scripts/_raster.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/scripts/extract_marker.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/scripts/extract_pymupdf.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/scripts/pdf_create.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/scripts/pdf_fill_form.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/scripts/pdf_form_layout.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/scripts/pdf_make_form.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/scripts/pdf_merge.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/scripts/pdf_meta.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/scripts/pdf_page_image.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/scripts/pdf_read.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/scripts/pdf_secure.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/scripts/pdf_split.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/scripts/pdf_stamp.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/scripts/pdf_watermark.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/powerpoint/LICENSE +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/powerpoint/SKILL.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/powerpoint/scripts/pptx_create.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/powerpoint/scripts/pptx_edit.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/powerpoint/scripts/pptx_from_template.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/powerpoint/scripts/pptx_read.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/powerpoint/scripts/pptx_render.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/weekly-review-planning/LICENSE +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/weekly-review-planning/SKILL.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/xlsx/LICENSE +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/xlsx/SKILL.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/xlsx/references/restructuring.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/xlsx/scripts/csv_to_xlsx.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/xlsx/scripts/xlsx_create.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/xlsx/scripts/xlsx_edit.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/xlsx/scripts/xlsx_read.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/xlsx/scripts/xlsx_recalc.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/xlsx/scripts/xlsx_restructure.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/xlsx/scripts/xlsx_to_csv.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/youtube-content/LICENSE +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/youtube-content/SKILL.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/youtube-content/references/output-formats.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/youtube-content/scripts/fetch_transcript.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/goal.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/headless.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/history.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/integrations/__init__.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/integrations/base.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/integrations/herdr.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/integrations/osc7501.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/journal.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/llm/__init__.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/llm/anthropic_client.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/llm/client.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/llm/factory.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/llm/openai_client.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/llm/responses_client.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/llm/retry.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/llm/scaffold.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/llm/sse.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/llm/usage.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/llm/verify.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/memory.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/model_switch.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/models.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/prompt.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/pump.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/sessions.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/skills.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/steer.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/subagents/board.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/subagents/commands.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/subagents/events.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/subagents/hooks.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/subagents/project_scan.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/subagents/tools.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/subagents/worktree.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/toggles.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/__init__.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/ask_user.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/base.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/bash.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/edit.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/find.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/grep.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/hooks.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/ignore.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/ls.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/memory.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/mutation_queue.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/read.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/run_code.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/session_search.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/skill_manage.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/web_fetch.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/web_search.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/write.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/trace.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tracing/__init__.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tracing/command.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tracing/index.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tracing/model.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tracing/server.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tracing/static/index.html +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/__init__.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/app.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/app.tcss +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/banner.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/clipboard.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/commands.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/contrast.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/daily_look.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/gradient.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/motion.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/omarchy.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/paste_store.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/path_input.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/recent_models.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/screens/__init__.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/screens/agents_panel.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/screens/auth_dialog.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/screens/btw.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/screens/command_palette.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/screens/dialog.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/screens/game2048.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/screens/main.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/screens/model_picker.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/screens/session_picker.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/screens/verify_picker.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/syntax.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/theme.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/widgets/__init__.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/widgets/activity.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/widgets/chat.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/widgets/command_menu.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/widgets/input.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/widgets/key_hints.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/widgets/landing.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/widgets/mode_divider.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/widgets/pills.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/widgets/sidebar.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/widgets/status_bar.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/widgets/tool_card.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/websearch/__init__.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/websearch/base.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/websearch/deepseek.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/conftest.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/fixtures/omarchy/README.md +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/fixtures/omarchy/catppuccin-latte.toml +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/fixtures/omarchy/gruvbox.toml +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/fixtures/omarchy/hackerman.toml +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/fixtures/omarchy/lumon.toml +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/fixtures/omarchy/rose-pine.toml +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/fixtures/omarchy/tokyo-night.toml +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_agent.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_anthropic_client.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_auth.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_btw.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_cli.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_code_mode_agent.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_compaction.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_config.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_conversation.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_daily_mode.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_goal.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_headless.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_herdr.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_history.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_integration.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_interrupt.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_llm_client.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_model_switch.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_osc7501.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_plan_mode.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_prompt.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_pump.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_responses_client.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_retry.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_sessions.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_skills.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_sse.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_steer.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_task_board.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_trace.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_usage.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/tools/test_base.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/tools/test_bash.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/tools/test_edit.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/tools/test_grep.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/tools/test_hooks.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/tools/test_ignore.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/tools/test_mutation_queue.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/tools/test_read.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/tools/test_registry.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/tools/test_run_code.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/tools/test_web_search.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/tools/test_write.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/tracing/test_command.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/tracing/test_index.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/tracing/test_model.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/tracing/test_server.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/__snapshots__/test_snapshots/test_snapshot_idle.raw +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/__snapshots__/test_snapshots/test_snapshot_wide_layout_with_sidebar.raw +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_app_smoke.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_bang_ui.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_btw_ui.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_command_menu.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_compact_ui.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_daily_look.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_daily_mode_ui.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_goal_ui.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_input_paste.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_interrupt_ui.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_main_screen.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_model_picker.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_omarchy.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_path_input.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_scroll_follow.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_sessions_ui.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_sidebar.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_skills_ui.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_snapshots.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_steer_ui.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_tracing_viewer_ui.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_widgets.py +0 -0
- {limbo_code-0.3.5 → limbo_code-0.3.6}/uv.lock +0 -0
|
@@ -50,11 +50,14 @@ model: inherit # inherit | cheap | 具体模型 id
|
|
|
50
50
|
max_iterations: 40
|
|
51
51
|
isolation: shared # shared | worktree
|
|
52
52
|
context: brief # brief(只看任务说明)| fork(带上主对话)
|
|
53
|
+
inherit_prompt: false # 仅 fork:沿用主会话的 system prompt 和工具列表
|
|
53
54
|
scope_paths: packages/api/**
|
|
54
55
|
---
|
|
55
56
|
给子代理的额外指令。
|
|
56
57
|
```
|
|
57
58
|
|
|
59
|
+
`inherit_prompt: true`(内置的 skill-reviewer 就是这样)让 fork 子代理逐字节沿用主会话的 system prompt 和工具定义,第一次请求就能命中 prompt 缓存。工具限制只在执行侧生效:主会话的工具都会展示给它,但不在白名单里的调用会失败。它的角色说明和指令放在第一条消息里;它没有上面那五个任务工具,最后一条纯文本回复就是结果。这个开关只在子代理和主会话用同一个模型时生效,换了模型缓存本来就是冷的,会回到普通 fork。
|
|
60
|
+
|
|
58
61
|
## 打点与任务编排
|
|
59
62
|
|
|
60
63
|
子代理有五个工具:
|
|
@@ -121,7 +124,7 @@ enabled = true # false:完全关闭
|
|
|
121
124
|
auto = "suggest" # off | suggest | on
|
|
122
125
|
max_parallel = 3 # 同时运行的子代理
|
|
123
126
|
max_tasks = 12 # 未结束任务上限
|
|
124
|
-
task_token_budget = 400000 # 单任务 token
|
|
127
|
+
task_token_budget = 400000 # 单任务 token 预算(0 = 不限):只算新增 token(总量减去缓存命中),超出后当前这批工具跑完、下一次请求前收尾
|
|
125
128
|
cheap_model = "" # model: cheap 的角色用它(空 = 主模型)
|
|
126
129
|
wake_main_on = ["question", "alert", "wave_done"]
|
|
127
130
|
on_dep_fail = "notify" # notify | cascade
|
|
@@ -77,10 +77,11 @@ from limbo.trace import TRACE_SCHEMA, TraceLogger, trace_path_for, utc_now_iso
|
|
|
77
77
|
# it burns the whole iteration budget re-sending what can never fit.
|
|
78
78
|
_MAX_CONSECUTIVE_LENGTH_STOPS = 3
|
|
79
79
|
|
|
80
|
-
# Placeholder result for tool calls that never started because the
|
|
81
|
-
# interrupted
|
|
82
|
-
#
|
|
83
|
-
|
|
80
|
+
# Placeholder result for tool calls that never started because the batch
|
|
81
|
+
# was interrupted (RFC LIM-53). Only ever used for calls that genuinely
|
|
82
|
+
# never ran, so history and disk cannot diverge. ``interrupt(reason)``
|
|
83
|
+
# names who stopped it (the supervisor pausing or cancelling a subagent).
|
|
84
|
+
_USER_INTERRUPT_REASON = "用户打断(ESC)"
|
|
84
85
|
|
|
85
86
|
|
|
86
87
|
class _StreamInterrupted(Exception):
|
|
@@ -139,6 +140,8 @@ class UsageUpdate:
|
|
|
139
140
|
"""Cumulative token usage for the session, emitted after each LLM call."""
|
|
140
141
|
|
|
141
142
|
total_tokens: int
|
|
143
|
+
# Tokens the provider had to process anew: total minus cache hits.
|
|
144
|
+
fresh_tokens: int = 0
|
|
142
145
|
|
|
143
146
|
|
|
144
147
|
@dataclass(frozen=True)
|
|
@@ -233,7 +236,11 @@ class Agent:
|
|
|
233
236
|
# stream / tool batch / compact summary at the nearest
|
|
234
237
|
# history-consistency point.
|
|
235
238
|
self._interrupt_requested = False
|
|
239
|
+
self._interrupt_reason = _USER_INTERRUPT_REASON
|
|
236
240
|
self._turn_interrupted = False
|
|
241
|
+
# request_stop(): end the turn at the next loop top, before another
|
|
242
|
+
# request goes out (the current tool batch still runs).
|
|
243
|
+
self._stop_reason: str | None = None
|
|
237
244
|
self._stream_task: asyncio.Task[Any] | None = None
|
|
238
245
|
self._stream_cut = False
|
|
239
246
|
self._iteration_count = 0
|
|
@@ -244,6 +251,11 @@ class Agent:
|
|
|
244
251
|
# Extra system-prompt section and tools installed after
|
|
245
252
|
# construction (subagent orchestration, see install()).
|
|
246
253
|
self._prompt_extra = ""
|
|
254
|
+
# Cache parity (install()): a fork child sends its parent's exact
|
|
255
|
+
# system prompt and tool definitions so the provider's prefix cache
|
|
256
|
+
# hits; the child's own registry and hooks still decide dispatch.
|
|
257
|
+
self._system_override: str | None = None
|
|
258
|
+
self._advertised_tools: list[dict[str, Any]] | None = None
|
|
247
259
|
# Frozen memory snapshot (limbo.memory): taken once per session so
|
|
248
260
|
# mid-session memory writes never change the prompt prefix.
|
|
249
261
|
store = self.registry.memory_store
|
|
@@ -498,6 +510,22 @@ class Agent:
|
|
|
498
510
|
"""Cumulative tokens across every model this session has used."""
|
|
499
511
|
return sum(entry.total_tokens for entry in self._meta.usage.values())
|
|
500
512
|
|
|
513
|
+
@property
|
|
514
|
+
def session_fresh_tokens(self) -> int:
|
|
515
|
+
"""Cumulative tokens minus prompt-cache hits: what the provider had
|
|
516
|
+
to process anew. Re-sending a cached prefix every iteration costs
|
|
517
|
+
little, so budgets count this, not ``session_total_tokens``."""
|
|
518
|
+
return sum(
|
|
519
|
+
max(0, entry.total_tokens - entry.cached_tokens)
|
|
520
|
+
for entry in self._meta.usage.values()
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
def _usage_update(self) -> UsageUpdate:
|
|
524
|
+
return UsageUpdate(
|
|
525
|
+
total_tokens=self.session_total_tokens,
|
|
526
|
+
fresh_tokens=self.session_fresh_tokens,
|
|
527
|
+
)
|
|
528
|
+
|
|
501
529
|
@property
|
|
502
530
|
def usage_ledger(self) -> dict[str, UsageEntry]:
|
|
503
531
|
return dict(self._meta.usage)
|
|
@@ -616,12 +644,19 @@ class Agent:
|
|
|
616
644
|
hooks: Sequence[ToolHook] = (),
|
|
617
645
|
remove_tools: Sequence[str] = (),
|
|
618
646
|
prompt_section: str = "",
|
|
647
|
+
system_prompt: str | None = None,
|
|
648
|
+
advertised_tools: list[dict[str, Any]] | None = None,
|
|
619
649
|
) -> None:
|
|
620
650
|
"""Extend this agent after construction (subagent orchestration).
|
|
621
651
|
|
|
622
652
|
Registers tool instances and hooks, removes tools a profile does
|
|
623
653
|
not grant, and appends ``prompt_section`` to the system prompt,
|
|
624
654
|
which is rebuilt in place (resume regenerates it anyway).
|
|
655
|
+
|
|
656
|
+
``system_prompt`` / ``advertised_tools`` replace what the model is
|
|
657
|
+
shown verbatim (cache parity with a parent session); calls are
|
|
658
|
+
still dispatched through this agent's own registry, so a tool that
|
|
659
|
+
is advertised but not registered fails as unknown.
|
|
625
660
|
"""
|
|
626
661
|
for name in remove_tools:
|
|
627
662
|
self.registry.unregister(name)
|
|
@@ -630,6 +665,10 @@ class Agent:
|
|
|
630
665
|
self.registry.hooks.extend(hooks)
|
|
631
666
|
if prompt_section:
|
|
632
667
|
self._prompt_extra = prompt_section
|
|
668
|
+
if system_prompt is not None:
|
|
669
|
+
self._system_override = system_prompt
|
|
670
|
+
if advertised_tools is not None:
|
|
671
|
+
self._advertised_tools = advertised_tools
|
|
633
672
|
self._init_system_message()
|
|
634
673
|
|
|
635
674
|
@property
|
|
@@ -642,9 +681,12 @@ class Agent:
|
|
|
642
681
|
return self._end_turn_requested
|
|
643
682
|
|
|
644
683
|
def _init_system_message(self) -> None:
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
684
|
+
if self._system_override is not None:
|
|
685
|
+
prompt = self._system_override
|
|
686
|
+
else:
|
|
687
|
+
prompt = build_system_prompt(self.registry, self.workdir, memory=self._memory)
|
|
688
|
+
if self._prompt_extra:
|
|
689
|
+
prompt += "\n\n" + self._prompt_extra
|
|
648
690
|
if self.messages and self.messages[0].role == "system":
|
|
649
691
|
# Code Mode toggles rebuild the system prompt in place (the
|
|
650
692
|
# SDK section changes with the presentation).
|
|
@@ -652,6 +694,19 @@ class Agent:
|
|
|
652
694
|
else:
|
|
653
695
|
self.messages.append(Message(role="system", content=prompt))
|
|
654
696
|
|
|
697
|
+
@property
|
|
698
|
+
def system_prompt(self) -> str:
|
|
699
|
+
"""The system prompt the next request sends."""
|
|
700
|
+
if self.messages and self.messages[0].role == "system":
|
|
701
|
+
return self.messages[0].content or ""
|
|
702
|
+
return ""
|
|
703
|
+
|
|
704
|
+
def tool_definitions(self) -> list[dict[str, Any]]:
|
|
705
|
+
"""The tool definitions the next request sends."""
|
|
706
|
+
if self._advertised_tools is not None:
|
|
707
|
+
return self._advertised_tools
|
|
708
|
+
return self.registry.definitions()
|
|
709
|
+
|
|
655
710
|
@property
|
|
656
711
|
def messages(self) -> list[Message]:
|
|
657
712
|
"""The conversation history (owned by the tool-call bookkeeper)."""
|
|
@@ -833,7 +888,7 @@ class Agent:
|
|
|
833
888
|
|
|
834
889
|
# -- interrupt (LIM-53) ---------------------------------------------------
|
|
835
890
|
|
|
836
|
-
def interrupt(self) -> None:
|
|
891
|
+
def interrupt(self, reason: str = _USER_INTERRUPT_REASON) -> None:
|
|
837
892
|
"""Interrupt the current turn / compact at the next consistency point.
|
|
838
893
|
|
|
839
894
|
Non-blocking; safe to call from the UI at any time (a no-op when
|
|
@@ -845,11 +900,27 @@ class Agent:
|
|
|
845
900
|
cancellable by design (they finish and record real results).
|
|
846
901
|
"""
|
|
847
902
|
self._interrupt_requested = True
|
|
903
|
+
self._interrupt_reason = reason
|
|
848
904
|
task = self._stream_task
|
|
849
905
|
if task is not None and not task.done():
|
|
850
906
|
task.cancel()
|
|
851
907
|
self.registry.cancel_active()
|
|
852
908
|
|
|
909
|
+
def request_stop(self, reason: str) -> None:
|
|
910
|
+
"""End the current turn before its next LLM request.
|
|
911
|
+
|
|
912
|
+
Unlike interrupt() nothing is cancelled: the in-flight response
|
|
913
|
+
and its tool batch complete and are recorded, then the loop top
|
|
914
|
+
stops instead of sending another request (a subagent over its
|
|
915
|
+
token budget). Reset at run() entry; see ``stop_reason``.
|
|
916
|
+
"""
|
|
917
|
+
self._stop_reason = reason
|
|
918
|
+
|
|
919
|
+
@property
|
|
920
|
+
def stop_reason(self) -> str | None:
|
|
921
|
+
"""Why request_stop() ended the last turn (None: it did not)."""
|
|
922
|
+
return self._stop_reason
|
|
923
|
+
|
|
853
924
|
@property
|
|
854
925
|
def last_run_failed(self) -> bool:
|
|
855
926
|
"""Whether the last turn ended on an LLM error (after its retries).
|
|
@@ -964,7 +1035,9 @@ class Agent:
|
|
|
964
1035
|
# RFC LIM-53: an interrupt requested while idle must not leak into
|
|
965
1036
|
# this turn (one of the two reset points; compact() is the other).
|
|
966
1037
|
self._interrupt_requested = False
|
|
1038
|
+
self._interrupt_reason = _USER_INTERRUPT_REASON
|
|
967
1039
|
self._turn_interrupted = False
|
|
1040
|
+
self._stop_reason = None
|
|
968
1041
|
self._turn_count += 1
|
|
969
1042
|
self._turn_start = time.monotonic()
|
|
970
1043
|
self._turn_started_at = utc_now_iso()
|
|
@@ -1135,6 +1208,16 @@ class Agent:
|
|
|
1135
1208
|
|
|
1136
1209
|
async def _conversation_loop(self) -> AsyncIterator[AgentEvent]:
|
|
1137
1210
|
while self._iteration_count < self.config.llm.max_iterations:
|
|
1211
|
+
if self._stop_reason is not None:
|
|
1212
|
+
# request_stop(): history is consistent here (the previous
|
|
1213
|
+
# batch is recorded) and nothing is in flight.
|
|
1214
|
+
self.trace.log(
|
|
1215
|
+
"turn_stopped",
|
|
1216
|
+
turn=self._turn_count,
|
|
1217
|
+
iteration=self._iteration_count,
|
|
1218
|
+
reason=self._stop_reason,
|
|
1219
|
+
)
|
|
1220
|
+
break
|
|
1138
1221
|
self._iteration_count += 1
|
|
1139
1222
|
# Loop-top check: history is consistent here (all tool results
|
|
1140
1223
|
# from the previous batch are recorded) and no request has been
|
|
@@ -1365,7 +1448,7 @@ class Agent:
|
|
|
1365
1448
|
# start of the batch (cancel_active() had nothing to kill
|
|
1366
1449
|
# yet) — no call starts, every call gets the placeholder.
|
|
1367
1450
|
for tc in tool_calls:
|
|
1368
|
-
result = ToolResult(success=False, error=
|
|
1451
|
+
result = ToolResult(success=False, error=f"未执行:{self._interrupt_reason}")
|
|
1369
1452
|
results[tc["id"]] = result
|
|
1370
1453
|
unexecuted.append(tc["id"])
|
|
1371
1454
|
yield ToolResultEvent(
|
|
@@ -1395,7 +1478,7 @@ class Agent:
|
|
|
1395
1478
|
# Only genuinely un-executed calls get it — started
|
|
1396
1479
|
# tools always record their real result, so history
|
|
1397
1480
|
# and disk cannot diverge.
|
|
1398
|
-
result = ToolResult(success=False, error=
|
|
1481
|
+
result = ToolResult(success=False, error=f"未执行:{self._interrupt_reason}")
|
|
1399
1482
|
unexecuted.append(tc["id"])
|
|
1400
1483
|
else:
|
|
1401
1484
|
admission = (await self._admit([tc]))[tc["id"]]
|
|
@@ -1555,7 +1638,7 @@ class Agent:
|
|
|
1555
1638
|
pass
|
|
1556
1639
|
|
|
1557
1640
|
async def _call_llm(self) -> AsyncIterator[AgentEvent]:
|
|
1558
|
-
tool_definitions = self.
|
|
1641
|
+
tool_definitions = self.tool_definitions()
|
|
1559
1642
|
assistant_content = ""
|
|
1560
1643
|
assistant_reasoning = ""
|
|
1561
1644
|
assistant_signature: str | None = None
|
|
@@ -1678,7 +1761,7 @@ class Agent:
|
|
|
1678
1761
|
totals = self.record_usage(usage)
|
|
1679
1762
|
self._last_finish_reason = meta.finish_reason if meta else None
|
|
1680
1763
|
if totals.total_tokens:
|
|
1681
|
-
yield
|
|
1764
|
+
yield self._usage_update()
|
|
1682
1765
|
self.trace.log(
|
|
1683
1766
|
"llm_response",
|
|
1684
1767
|
turn=self._turn_count,
|
|
@@ -1907,7 +1990,7 @@ class Agent:
|
|
|
1907
1990
|
)
|
|
1908
1991
|
# The summary call is billed too: refresh the counter.
|
|
1909
1992
|
if self.session_total_tokens:
|
|
1910
|
-
yield
|
|
1993
|
+
yield self._usage_update()
|
|
1911
1994
|
|
|
1912
1995
|
def _save_session_sync(self) -> None:
|
|
1913
1996
|
"""Persist the session: incremental append by default, full rewrite
|
|
@@ -29,13 +29,18 @@ def _apply_kitty_keyboard_workaround(config) -> None:
|
|
|
29
29
|
|
|
30
30
|
Textual reads TEXTUAL_DISABLE_KITTY_KEY at import time, so this must run
|
|
31
31
|
before the first `textual` import. See KittyKeyboardMode for why typing
|
|
32
|
-
Chinese inside the herdr multiplexer turns into spaces
|
|
33
|
-
keyboard protocol
|
|
32
|
+
Chinese inside the herdr multiplexer turns into spaces under the full
|
|
33
|
+
kitty keyboard protocol, and why herdr still needs the disambiguate flag.
|
|
34
34
|
"""
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
):
|
|
35
|
+
mode = config.ui.kitty_keyboard
|
|
36
|
+
if mode.disable_textual_kitty_key():
|
|
38
37
|
os.environ["TEXTUAL_DISABLE_KITTY_KEY"] = "1"
|
|
38
|
+
elif mode.disambiguate_only(in_herdr=bool(os.environ.get("HERDR_ENV"))):
|
|
39
|
+
from limbo.ui.key_fixes import ( # noqa: PLC0415 - imports textual; must follow the env check above
|
|
40
|
+
request_kitty_disambiguate_only,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
request_kitty_disambiguate_only()
|
|
39
44
|
|
|
40
45
|
|
|
41
46
|
def _first_run_login(spec, config) -> bool: # noqa: ANN001
|
|
@@ -138,23 +138,33 @@ class KittyKeyboardMode(str, Enum):
|
|
|
138
138
|
terminal (e.g. Ghostty), and Ghostty then encodes IME commits as the
|
|
139
139
|
physical commit key (space) while dropping the composed text — typing
|
|
140
140
|
Chinese turns into spaces (pasting is unaffected, it bypasses the key
|
|
141
|
-
encoder).
|
|
142
|
-
|
|
141
|
+
encoder).
|
|
142
|
+
|
|
143
|
+
Turning the protocol off entirely is not an option either: herdr then
|
|
144
|
+
encodes Shift+Enter for the pane as a bare ``\r`` (indistinguishable from
|
|
145
|
+
Enter), so the input box can no longer insert a newline. Requesting only
|
|
146
|
+
``DISAMBIGUATE_ESCAPE_CODES`` is the middle ground: herdr leaves the host
|
|
147
|
+
flags alone (IME text still arrives as raw UTF-8) and forwards Shift+Enter
|
|
148
|
+
as ``ESC [ 13 ; 2 u``.
|
|
143
149
|
"""
|
|
144
150
|
|
|
145
|
-
#
|
|
146
|
-
#
|
|
151
|
+
# Inside the herdr multiplexer (detected via HERDR_ENV) request only the
|
|
152
|
+
# disambiguate flag; the full Textual request everywhere else. Safe
|
|
153
|
+
# default for CJK users.
|
|
147
154
|
AUTO = "auto"
|
|
148
|
-
# Always request the kitty keyboard protocol (Textual's default
|
|
155
|
+
# Always request the full kitty keyboard protocol (Textual's default).
|
|
149
156
|
ENABLED = "enabled"
|
|
150
|
-
# Never request it — forces raw/legacy terminal input everywhere
|
|
157
|
+
# Never request it — forces raw/legacy terminal input everywhere
|
|
158
|
+
# (Shift+Enter is then indistinguishable from Enter inside herdr).
|
|
151
159
|
DISABLED = "disabled"
|
|
152
160
|
|
|
153
|
-
def disable_textual_kitty_key(self
|
|
161
|
+
def disable_textual_kitty_key(self) -> bool:
|
|
154
162
|
"""Whether Textual's kitty keyboard protocol should be disabled."""
|
|
155
|
-
return self is KittyKeyboardMode.DISABLED
|
|
156
|
-
|
|
157
|
-
|
|
163
|
+
return self is KittyKeyboardMode.DISABLED
|
|
164
|
+
|
|
165
|
+
def disambiguate_only(self, *, in_herdr: bool) -> bool:
|
|
166
|
+
"""Whether to request only the kitty disambiguate flag."""
|
|
167
|
+
return self is KittyKeyboardMode.AUTO and in_herdr
|
|
158
168
|
|
|
159
169
|
|
|
160
170
|
class UIConfig(BaseModel):
|
|
@@ -169,8 +179,9 @@ class UIConfig(BaseModel):
|
|
|
169
179
|
# full, or reduced inside an SSH session. LIMBO_MOTION overrides it.
|
|
170
180
|
motion: str | None = None
|
|
171
181
|
# Kitty keyboard protocol handling (see KittyKeyboardMode):
|
|
172
|
-
# auto - enable normally
|
|
173
|
-
#
|
|
182
|
+
# auto - enable normally; inside the herdr multiplexer request only
|
|
183
|
+
# the disambiguate flag, the full set breaks IME/Chinese
|
|
184
|
+
# input there (default)
|
|
174
185
|
# enabled - always request the kitty keyboard protocol
|
|
175
186
|
# disabled - never request it (forces raw/legacy terminal input)
|
|
176
187
|
kitty_keyboard: KittyKeyboardMode = KittyKeyboardMode.AUTO
|
|
@@ -325,7 +336,9 @@ class SubagentSettings(BaseModel):
|
|
|
325
336
|
# Live (unfinished) tasks at a time. Subagents never plan themselves
|
|
326
337
|
# (depth 1): they can only propose_task, which the main agent approves.
|
|
327
338
|
max_tasks: int = 12
|
|
328
|
-
# Per-task
|
|
339
|
+
# Per-task budget of fresh tokens (total minus prompt-cache hits;
|
|
340
|
+
# 0 = unlimited). Over it, the current tool batch completes and the
|
|
341
|
+
# task ends before its next request, reporting what it changed.
|
|
329
342
|
task_token_budget: int = 400_000
|
|
330
343
|
# Model for profiles with ``model: cheap`` (empty = the main model).
|
|
331
344
|
cheap_model: str = ""
|
|
@@ -255,6 +255,18 @@ CATALOG: dict[str, ModelSpec] = {
|
|
|
255
255
|
# 64K default output cap is deliberately conservative — thinking
|
|
256
256
|
# tokens count toward the output limit, so a near-max cap would let a
|
|
257
257
|
# long chain of thought starve the visible answer.
|
|
258
|
+
# deepseek-flash is the official Flash name (DeepSeek-V4.1-Flash, per
|
|
259
|
+
# the pricing page 2026-10): 1M context, 384K max output, image input.
|
|
260
|
+
# deepseek-v4-flash / -vision-exp are legacy aliases served by it.
|
|
261
|
+
"deepseek-flash": ModelSpec(
|
|
262
|
+
id="deepseek-flash",
|
|
263
|
+
provider=DEEPSEEK,
|
|
264
|
+
context_window=1_000_000,
|
|
265
|
+
max_tokens=65_536,
|
|
266
|
+
reasoning=True,
|
|
267
|
+
vision=True,
|
|
268
|
+
thinking_format="deepseek",
|
|
269
|
+
),
|
|
258
270
|
"deepseek-v4-pro": ModelSpec(
|
|
259
271
|
id="deepseek-v4-pro",
|
|
260
272
|
provider=DEEPSEEK,
|
|
@@ -328,7 +340,13 @@ CATALOG: dict[str, ModelSpec] = {
|
|
|
328
340
|
"k3": _kimi_coding(
|
|
329
341
|
"k3", context_window=1_048_576, max_tokens=131_072
|
|
330
342
|
),
|
|
331
|
-
|
|
343
|
+
# k3-256k: K3 capped at 256K context (images only, no video).
|
|
344
|
+
"k3-256k": _kimi_coding("k3-256k"),
|
|
345
|
+
# kimi-for-coding now serves K2.8 Preview with a 1M window; highspeed
|
|
346
|
+
# stays at 256K (Kimi Code models page, 2026-10).
|
|
347
|
+
"kimi-for-coding": _kimi_coding(
|
|
348
|
+
"kimi-for-coding", context_window=1_048_576, max_tokens=131_072
|
|
349
|
+
),
|
|
332
350
|
"kimi-for-coding-highspeed": _kimi_coding("kimi-for-coding-highspeed"),
|
|
333
351
|
# -- GLM Coding Plan (subscription whitelist) ----------------------------
|
|
334
352
|
"glm-4.5-air": _glm("glm-4.5-air", context_window=131_072, max_tokens=98_304),
|
|
@@ -12,8 +12,11 @@ Frontmatter keys: ``name``, ``description``, ``when_to_use``, ``tools``
|
|
|
12
12
|
(``read_only`` | ``all`` | ``no_edit`` | comma list), ``model``
|
|
13
13
|
(``inherit``, ``cheap`` or a model id), ``max_iterations``,
|
|
14
14
|
``isolation`` (``shared`` | ``worktree``), ``context`` (``brief`` |
|
|
15
|
-
``fork``), ``
|
|
16
|
-
|
|
15
|
+
``fork``), ``inherit_prompt`` (``true``: a fork child keeps the main
|
|
16
|
+
session's system prompt and tool list for prompt-cache parity),
|
|
17
|
+
``scope_paths`` (comma list). The body is the profile's instructions,
|
|
18
|
+
appended to the subagent's system prompt (with ``inherit_prompt``, sent in
|
|
19
|
+
its first message instead).
|
|
17
20
|
"""
|
|
18
21
|
|
|
19
22
|
from __future__ import annotations
|
|
@@ -73,6 +76,10 @@ class Profile:
|
|
|
73
76
|
isolation: str = "shared"
|
|
74
77
|
context: str = "brief" # brief | fork
|
|
75
78
|
scope_paths: tuple[str, ...] = ()
|
|
79
|
+
# context=fork only: keep the main session's system prompt and tool
|
|
80
|
+
# list verbatim (prompt-cache parity); the body and role go in the
|
|
81
|
+
# first message and the final reply is the result (no task tools).
|
|
82
|
+
inherit_prompt: bool = False
|
|
76
83
|
body: str = ""
|
|
77
84
|
path: Path | None = None
|
|
78
85
|
builtin: bool = False
|
|
@@ -174,6 +181,7 @@ BUILTIN_PROFILES: tuple[Profile, ...] = (
|
|
|
174
181
|
model="cheap",
|
|
175
182
|
max_iterations=20,
|
|
176
183
|
context="fork",
|
|
184
|
+
inherit_prompt=True,
|
|
177
185
|
body=SKILL_REVIEW_PROMPT,
|
|
178
186
|
builtin=True,
|
|
179
187
|
),
|
|
@@ -210,6 +218,7 @@ def parse_profile_md(text: str, fallback_name: str, path: Path | None = None) ->
|
|
|
210
218
|
if fields.get("context") in ("brief", "fork")
|
|
211
219
|
else "brief",
|
|
212
220
|
scope_paths=tuple(s.strip().strip("\"'") for s in scope.split(",") if s.strip()),
|
|
221
|
+
inherit_prompt=fields.get("inherit_prompt", "").lower() in ("true", "yes", "1"),
|
|
213
222
|
body=body.strip(),
|
|
214
223
|
path=path,
|
|
215
224
|
)
|
|
@@ -284,6 +293,8 @@ def render_profile_md(profile: Profile) -> str:
|
|
|
284
293
|
f"isolation: {profile.isolation}",
|
|
285
294
|
f"context: {profile.context}",
|
|
286
295
|
]
|
|
296
|
+
if profile.inherit_prompt:
|
|
297
|
+
lines.append("inherit_prompt: true")
|
|
287
298
|
if profile.scope_paths:
|
|
288
299
|
lines.append("scope_paths: " + ", ".join(profile.scope_paths))
|
|
289
300
|
lines += ["---", profile.body, ""]
|
|
@@ -33,7 +33,7 @@ from dataclasses import dataclass, field
|
|
|
33
33
|
from pathlib import Path
|
|
34
34
|
from typing import TYPE_CHECKING, Any
|
|
35
35
|
|
|
36
|
-
from limbo.agent import ErrorEvent, TextDelta, ToolCallRequest, UsageUpdate
|
|
36
|
+
from limbo.agent import ErrorEvent, TextDelta, ToolCallRequest, ToolResultEvent, UsageUpdate
|
|
37
37
|
from limbo.config import Config
|
|
38
38
|
from limbo.llm.client import LLMClient
|
|
39
39
|
from limbo.sessions import save_session
|
|
@@ -62,7 +62,7 @@ from limbo.subagents.events import (
|
|
|
62
62
|
PlanProposed,
|
|
63
63
|
TaskChanged,
|
|
64
64
|
)
|
|
65
|
-
from limbo.subagents.hooks import ClaimHook, NoEditHook, ReadOnlyHook
|
|
65
|
+
from limbo.subagents.hooks import SUBAGENT_TOOL_NAMES, ClaimHook, NoEditHook, ReadOnlyHook
|
|
66
66
|
from limbo.subagents.profiles import (
|
|
67
67
|
NO_EDIT,
|
|
68
68
|
READ_ONLY,
|
|
@@ -131,6 +131,20 @@ user does not watch you live.
|
|
|
131
131
|
finish(status="failed") and say why.
|
|
132
132
|
{scope}{others}</subagent_role>"""
|
|
133
133
|
|
|
134
|
+
# A cache-parity fork child (profile inherit_prompt) keeps the main
|
|
135
|
+
# session's system prompt and tool list byte for byte, so its role travels
|
|
136
|
+
# in the first user message instead, and it has no task tools of its own:
|
|
137
|
+
# its final plain-text reply is the result.
|
|
138
|
+
FORK_CHILD_PROMPT = """<subagent_role>
|
|
139
|
+
From here on you are the subagent "{agent}" of limbo, working on task {id}:
|
|
140
|
+
{title}. The conversation above is the main session's, shown for context;
|
|
141
|
+
the user does not watch you live.
|
|
142
|
+
- The tool list is the main session's, but you may only call: {tools}.
|
|
143
|
+
Any other tool fails.
|
|
144
|
+
- When you are done, reply with your result as plain text and no tool
|
|
145
|
+
calls: that reply is all the main agent reads.
|
|
146
|
+
</subagent_role>"""
|
|
147
|
+
|
|
134
148
|
|
|
135
149
|
@dataclass
|
|
136
150
|
class PlanResult:
|
|
@@ -148,6 +162,10 @@ class _Runner:
|
|
|
148
162
|
last_text: str = ""
|
|
149
163
|
last_error: str = ""
|
|
150
164
|
over_budget: bool = False
|
|
165
|
+
budget_reason: str = ""
|
|
166
|
+
# First line of every successful state-changing call (named in the
|
|
167
|
+
# outcome when the budget ends the task before it reports).
|
|
168
|
+
writes: list[str] = field(default_factory=list)
|
|
151
169
|
resumed: bool = False
|
|
152
170
|
|
|
153
171
|
|
|
@@ -396,6 +414,7 @@ class Supervisor:
|
|
|
396
414
|
async for event in agent.run(text):
|
|
397
415
|
self._observe(runner, event)
|
|
398
416
|
if runner.over_budget:
|
|
417
|
+
self._finish_over_budget(runner)
|
|
399
418
|
break
|
|
400
419
|
if self.board.get(task_id).status != "running":
|
|
401
420
|
break
|
|
@@ -460,20 +479,49 @@ class Supervisor:
|
|
|
460
479
|
self._set_activity(task_id, "✎ 回复中")
|
|
461
480
|
elif isinstance(event, ErrorEvent):
|
|
462
481
|
runner.last_error = event.message
|
|
482
|
+
elif isinstance(event, ToolResultEvent):
|
|
483
|
+
agent = runner.agent
|
|
484
|
+
if (
|
|
485
|
+
event.result.success
|
|
486
|
+
and event.name not in SUBAGENT_TOOL_NAMES
|
|
487
|
+
and agent is not None
|
|
488
|
+
and agent.registry.replay_policy(event.name) != "safe"
|
|
489
|
+
):
|
|
490
|
+
first = (event.result.output or "").strip().splitlines()
|
|
491
|
+
runner.writes.append(f"{event.name}:{first[0][:160] if first else '完成'}")
|
|
463
492
|
elif isinstance(event, UsageUpdate):
|
|
464
493
|
self._tokens[task_id] = event.total_tokens
|
|
465
494
|
task = self.board.tasks.get(task_id)
|
|
466
495
|
budget = task.max_tokens if task is not None else 0
|
|
467
|
-
|
|
496
|
+
# The budget counts fresh tokens: a fork child re-sends the
|
|
497
|
+
# whole inherited conversation every iteration, and those cache
|
|
498
|
+
# hits would otherwise exhaust it in a handful of requests.
|
|
499
|
+
if budget and event.fresh_tokens > budget and not runner.over_budget:
|
|
468
500
|
runner.over_budget = True
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
self._finish(
|
|
472
|
-
task_id,
|
|
473
|
-
summary=runner.last_text,
|
|
474
|
-
failed=True,
|
|
475
|
-
reason=f"超出 token 预算({event.total_tokens} > {budget})",
|
|
501
|
+
runner.budget_reason = (
|
|
502
|
+
f"超出 token 预算(新增 {event.fresh_tokens} > {budget},缓存命中不计)"
|
|
476
503
|
)
|
|
504
|
+
# Not interrupt(): the response that crossed the line and
|
|
505
|
+
# its tool batch complete (their writes land), then the
|
|
506
|
+
# turn ends before the next request.
|
|
507
|
+
if runner.agent is not None:
|
|
508
|
+
runner.agent.request_stop(runner.budget_reason)
|
|
509
|
+
|
|
510
|
+
def _finish_over_budget(self, runner: _Runner) -> None:
|
|
511
|
+
"""The budget ended the turn: report what the task got done."""
|
|
512
|
+
task = self.board.tasks.get(runner.task_id)
|
|
513
|
+
if task is None or task.terminal:
|
|
514
|
+
return # it called finish in the crossing batch
|
|
515
|
+
lines = [runner.budget_reason + ",已在下一次请求前收尾,结果可能不完整"]
|
|
516
|
+
if runner.writes:
|
|
517
|
+
lines.append("已执行的改动:\n" + "\n".join(f"- {w}" for w in runner.writes[-20:]))
|
|
518
|
+
text = runner.last_text.strip()
|
|
519
|
+
self._finish(
|
|
520
|
+
runner.task_id,
|
|
521
|
+
summary=text,
|
|
522
|
+
failed=not (text or runner.writes),
|
|
523
|
+
reason="\n".join(lines),
|
|
524
|
+
)
|
|
477
525
|
|
|
478
526
|
def _set_activity(self, task_id: str, text: str) -> None:
|
|
479
527
|
self._activity[task_id] = text
|
|
@@ -545,6 +593,17 @@ class Supervisor:
|
|
|
545
593
|
self._configure(agent, task, profile, workdir)
|
|
546
594
|
return agent, resumed
|
|
547
595
|
|
|
596
|
+
def _inherits_prompt(self, task: Task, profile: Profile) -> bool:
|
|
597
|
+
"""Cache parity (hermes' background review): a fork child on the
|
|
598
|
+
main model sends the main session's exact prefix, so even its first
|
|
599
|
+
request is a prompt-cache hit. On another model the cache is cold
|
|
600
|
+
anyway and the child keeps its own prompt and task tools."""
|
|
601
|
+
return (
|
|
602
|
+
task.context == "fork"
|
|
603
|
+
and profile.inherit_prompt
|
|
604
|
+
and self._model_for(profile) == self.main.config.llm.model
|
|
605
|
+
)
|
|
606
|
+
|
|
548
607
|
def _write_fork(self, path: Path, task: Task) -> None:
|
|
549
608
|
"""context=fork: the child starts from the main history up to the
|
|
550
609
|
current turn's user message (never a half-finished tool batch)."""
|
|
@@ -587,6 +646,18 @@ class Supervisor:
|
|
|
587
646
|
hooks.append(NoEditHook())
|
|
588
647
|
if profile.writes and task.isolation == "shared":
|
|
589
648
|
hooks.append(ClaimHook(task.id, self.board, workdir))
|
|
649
|
+
if self._inherits_prompt(task, profile):
|
|
650
|
+
# Advertising is not permission: the main tool list is shown,
|
|
651
|
+
# the child's trimmed registry and hooks decide what runs.
|
|
652
|
+
agent.install(
|
|
653
|
+
hooks=hooks,
|
|
654
|
+
remove_tools=remove,
|
|
655
|
+
system_prompt=self.main.system_prompt,
|
|
656
|
+
advertised_tools=self.main.tool_definitions(),
|
|
657
|
+
)
|
|
658
|
+
if hasattr(agent.llm_client, "prompt_cache_key"):
|
|
659
|
+
agent.llm_client.prompt_cache_key = self.main.session_id
|
|
660
|
+
return
|
|
590
661
|
agent.install(
|
|
591
662
|
tools=subagent_tools(workdir, self, task.id),
|
|
592
663
|
hooks=hooks,
|
|
@@ -625,6 +696,16 @@ class Supervisor:
|
|
|
625
696
|
parts.append("主 agent / 用户补充:\n" + inbox)
|
|
626
697
|
return "\n\n".join(parts)
|
|
627
698
|
parts = [f"任务 {task.id}:{task.title}", task.brief.strip()]
|
|
699
|
+
profile = resolve_profile(self.profiles, task.profile)
|
|
700
|
+
if self._inherits_prompt(task, profile):
|
|
701
|
+
names = profile.tool_names or []
|
|
702
|
+
role = FORK_CHILD_PROMPT.format(
|
|
703
|
+
agent=task.agent_name,
|
|
704
|
+
id=task.id,
|
|
705
|
+
title=task.title,
|
|
706
|
+
tools=", ".join(names) or "(主会话的全部工具)",
|
|
707
|
+
)
|
|
708
|
+
parts = [role, profile.body, *parts]
|
|
628
709
|
if task.acceptance:
|
|
629
710
|
parts.append(f"验收标准:{task.acceptance}")
|
|
630
711
|
if task.scope_paths:
|
|
@@ -645,7 +726,7 @@ class Supervisor:
|
|
|
645
726
|
self._stopped = True
|
|
646
727
|
for runner in list(self._runners.values()):
|
|
647
728
|
if runner.agent is not None:
|
|
648
|
-
runner.agent.interrupt()
|
|
729
|
+
runner.agent.interrupt("limbo 退出,任务会在下次启动时恢复")
|
|
649
730
|
if runner.future is not None:
|
|
650
731
|
runner.future.cancel()
|
|
651
732
|
self.board.set_sink(None)
|
|
@@ -951,7 +1032,7 @@ class Supervisor:
|
|
|
951
1032
|
)
|
|
952
1033
|
runner = self._runners.get(task.id)
|
|
953
1034
|
if runner is not None and runner.agent is not None:
|
|
954
|
-
runner.agent.interrupt()
|
|
1035
|
+
runner.agent.interrupt("任务被暂停")
|
|
955
1036
|
return f"{task.id} 已暂停"
|
|
956
1037
|
|
|
957
1038
|
def _resume(self, task: Task, note: str) -> str:
|
|
@@ -978,7 +1059,7 @@ class Supervisor:
|
|
|
978
1059
|
)
|
|
979
1060
|
runner = self._runners.get(task.id)
|
|
980
1061
|
if runner is not None and runner.agent is not None:
|
|
981
|
-
runner.agent.interrupt()
|
|
1062
|
+
runner.agent.interrupt("任务被取消")
|
|
982
1063
|
if by == "system":
|
|
983
1064
|
self._push(self._result_notice(self.board.get(task.id)))
|
|
984
1065
|
return f"{task.id} 已取消"
|