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.
Files changed (333) hide show
  1. {limbo_code-0.3.5 → limbo_code-0.3.6}/PKG-INFO +1 -1
  2. {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/subagents.md +4 -1
  3. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/agent.py +96 -13
  4. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/app.py +10 -5
  5. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/config.py +26 -13
  6. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/llm/catalog.py +19 -1
  7. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/subagents/profiles.py +13 -2
  8. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/subagents/supervisor.py +94 -13
  9. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/registry.py +5 -1
  10. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/key_fixes.py +23 -7
  11. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_catalog.py +13 -0
  12. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_daily_p1.py +26 -2
  13. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_subagents.py +75 -0
  14. limbo_code-0.3.6/tests/ui/test_kitty_keyboard.py +62 -0
  15. {limbo_code-0.3.5 → limbo_code-0.3.6}/.agents/skills/grill-with-docs/SKILL.md +0 -0
  16. {limbo_code-0.3.5 → limbo_code-0.3.6}/.agents/skills/grill-with-docs/agents/openai.yaml +0 -0
  17. {limbo_code-0.3.5 → limbo_code-0.3.6}/.agents/skills/improve-codebase-architecture/HTML-REPORT.md +0 -0
  18. {limbo_code-0.3.5 → limbo_code-0.3.6}/.agents/skills/improve-codebase-architecture/SKILL.md +0 -0
  19. {limbo_code-0.3.5 → limbo_code-0.3.6}/.agents/skills/improve-codebase-architecture/agents/openai.yaml +0 -0
  20. {limbo_code-0.3.5 → limbo_code-0.3.6}/.agents/skills/tdd/SKILL.md +0 -0
  21. {limbo_code-0.3.5 → limbo_code-0.3.6}/.agents/skills/tdd/agents/openai.yaml +0 -0
  22. {limbo_code-0.3.5 → limbo_code-0.3.6}/.agents/skills/tdd/mocking.md +0 -0
  23. {limbo_code-0.3.5 → limbo_code-0.3.6}/.agents/skills/tdd/tests.md +0 -0
  24. {limbo_code-0.3.5 → limbo_code-0.3.6}/.github/workflows/publish.yml +0 -0
  25. {limbo_code-0.3.5 → limbo_code-0.3.6}/.github/workflows/test.yml +0 -0
  26. {limbo_code-0.3.5 → limbo_code-0.3.6}/.gitignore +0 -0
  27. {limbo_code-0.3.5 → limbo_code-0.3.6}/AGENTS.md +0 -0
  28. {limbo_code-0.3.5 → limbo_code-0.3.6}/CONTEXT.md +0 -0
  29. {limbo_code-0.3.5 → limbo_code-0.3.6}/Makefile +0 -0
  30. {limbo_code-0.3.5 → limbo_code-0.3.6}/README.md +0 -0
  31. {limbo_code-0.3.5 → limbo_code-0.3.6}/design/bang-verification.md +0 -0
  32. {limbo_code-0.3.5 → limbo_code-0.3.6}/design/confirm_view.html +0 -0
  33. {limbo_code-0.3.5 → limbo_code-0.3.6}/design/limbo-ui-minimal.md +0 -0
  34. {limbo_code-0.3.5 → limbo_code-0.3.6}/design/limbo-ui-redesign.md +0 -0
  35. {limbo_code-0.3.5 → limbo_code-0.3.6}/design/prototype-minimal-confirm.png +0 -0
  36. {limbo_code-0.3.5 → limbo_code-0.3.6}/design/prototype-minimal.html +0 -0
  37. {limbo_code-0.3.5 → limbo_code-0.3.6}/design/prototype-minimal.png +0 -0
  38. {limbo_code-0.3.5 → limbo_code-0.3.6}/design/prototype.html +0 -0
  39. {limbo_code-0.3.5 → limbo_code-0.3.6}/design/prototype.png +0 -0
  40. {limbo_code-0.3.5 → limbo_code-0.3.6}/design/rfc-bang-command.md +0 -0
  41. {limbo_code-0.3.5 → limbo_code-0.3.6}/design/rfc-headless-cli.md +0 -0
  42. {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/assets/limbo-current-ui.png +0 -0
  43. {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/assets/limbo-hero.png +0 -0
  44. {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/assets/limbo-hero.svg +0 -0
  45. {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/assets/limbo-models.png +0 -0
  46. {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/assets/limbo-models.svg +0 -0
  47. {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/assets/limbo-new-ui.png +0 -0
  48. {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/assets/walkthrough/limbo-dark/1_idle.svg +0 -0
  49. {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/assets/walkthrough/limbo-dark/2_thinking_tool_running.svg +0 -0
  50. {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/assets/walkthrough/limbo-dark/3_tool_success.svg +0 -0
  51. {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/assets/walkthrough/limbo-dark/4_tool_error.svg +0 -0
  52. {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/assets/walkthrough/limbo-dark/5_llm_error.svg +0 -0
  53. {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/assets/walkthrough/limbo-dark/6_edit_diff.svg +0 -0
  54. {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/assets/walkthrough/limbo-light/1_idle.svg +0 -0
  55. {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/assets/walkthrough/limbo-light/2_thinking_tool_running.svg +0 -0
  56. {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/assets/walkthrough/limbo-light/3_tool_success.svg +0 -0
  57. {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/assets/walkthrough/limbo-light/4_tool_error.svg +0 -0
  58. {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/assets/walkthrough/limbo-light/5_llm_error.svg +0 -0
  59. {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/assets/walkthrough/limbo-light/6_edit_diff.svg +0 -0
  60. {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/daily-mode.md +0 -0
  61. {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/session-management.md +0 -0
  62. {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/skills.md +0 -0
  63. {limbo_code-0.3.5 → limbo_code-0.3.6}/docs/ui-redesign-proposal.md +0 -0
  64. {limbo_code-0.3.5 → limbo_code-0.3.6}/pyproject.toml +0 -0
  65. {limbo_code-0.3.5 → limbo_code-0.3.6}/scripts/check_contrast.py +0 -0
  66. {limbo_code-0.3.5 → limbo_code-0.3.6}/scripts/gen_banner.py +0 -0
  67. {limbo_code-0.3.5 → limbo_code-0.3.6}/scripts/readme_hero.py +0 -0
  68. {limbo_code-0.3.5 → limbo_code-0.3.6}/scripts/scroll_bench.py +0 -0
  69. {limbo_code-0.3.5 → limbo_code-0.3.6}/scripts/ui_walkthrough.py +0 -0
  70. {limbo_code-0.3.5 → limbo_code-0.3.6}/skills-lock.json +0 -0
  71. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/__init__.py +0 -0
  72. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/__main__.py +0 -0
  73. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/attachments.py +0 -0
  74. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/auth.py +0 -0
  75. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/btw.py +0 -0
  76. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/cli_auth.py +0 -0
  77. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/code_mode.py +0 -0
  78. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/compaction.py +0 -0
  79. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/conversation.py +0 -0
  80. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily.py +0 -0
  81. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/README.md +0 -0
  82. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/arxiv/LICENSE +0 -0
  83. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/arxiv/SKILL.md +0 -0
  84. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/arxiv/scripts/search_arxiv.py +0 -0
  85. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/blocked-page-recovery/LICENSE +0 -0
  86. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/blocked-page-recovery/SKILL.md +0 -0
  87. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/blocked-page-recovery/scripts/recover_page.py +0 -0
  88. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/document-to-action-items/LICENSE +0 -0
  89. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/document-to-action-items/SKILL.md +0 -0
  90. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/docx/LICENSE +0 -0
  91. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/docx/SKILL.md +0 -0
  92. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/docx/references/revisions-and-comments.md +0 -0
  93. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/docx/scripts/docx_comments.py +0 -0
  94. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/docx/scripts/docx_common.py +0 -0
  95. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/docx/scripts/docx_create.py +0 -0
  96. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/docx/scripts/docx_edit.py +0 -0
  97. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/docx/scripts/docx_read.py +0 -0
  98. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/docx/scripts/docx_revisions.py +0 -0
  99. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/docx/scripts/docx_template.py +0 -0
  100. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/docx/scripts/docx_validate.py +0 -0
  101. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/grounded-citations/LICENSE +0 -0
  102. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/grounded-citations/SKILL.md +0 -0
  103. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/grounded-citations/references/citation-formats.md +0 -0
  104. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/grounded-citations/references/grounding-rationale.md +0 -0
  105. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/grounded-citations/scripts/sources.py +0 -0
  106. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/meeting-action-items/LICENSE +0 -0
  107. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/meeting-action-items/SKILL.md +0 -0
  108. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/LICENSE +0 -0
  109. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/SKILL.md +0 -0
  110. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/references/forms.md +0 -0
  111. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/references/nano-pdf-editing.md +0 -0
  112. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/references/ocr-extraction.md +0 -0
  113. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/scripts/_raster.py +0 -0
  114. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/scripts/extract_marker.py +0 -0
  115. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/scripts/extract_pymupdf.py +0 -0
  116. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/scripts/pdf_create.py +0 -0
  117. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/scripts/pdf_fill_form.py +0 -0
  118. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/scripts/pdf_form_layout.py +0 -0
  119. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/scripts/pdf_make_form.py +0 -0
  120. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/scripts/pdf_merge.py +0 -0
  121. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/scripts/pdf_meta.py +0 -0
  122. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/scripts/pdf_page_image.py +0 -0
  123. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/scripts/pdf_read.py +0 -0
  124. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/scripts/pdf_secure.py +0 -0
  125. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/scripts/pdf_split.py +0 -0
  126. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/scripts/pdf_stamp.py +0 -0
  127. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/pdf/scripts/pdf_watermark.py +0 -0
  128. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/powerpoint/LICENSE +0 -0
  129. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/powerpoint/SKILL.md +0 -0
  130. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/powerpoint/scripts/pptx_create.py +0 -0
  131. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/powerpoint/scripts/pptx_edit.py +0 -0
  132. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/powerpoint/scripts/pptx_from_template.py +0 -0
  133. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/powerpoint/scripts/pptx_read.py +0 -0
  134. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/powerpoint/scripts/pptx_render.py +0 -0
  135. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/weekly-review-planning/LICENSE +0 -0
  136. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/weekly-review-planning/SKILL.md +0 -0
  137. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/xlsx/LICENSE +0 -0
  138. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/xlsx/SKILL.md +0 -0
  139. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/xlsx/references/restructuring.md +0 -0
  140. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/xlsx/scripts/csv_to_xlsx.py +0 -0
  141. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/xlsx/scripts/xlsx_create.py +0 -0
  142. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/xlsx/scripts/xlsx_edit.py +0 -0
  143. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/xlsx/scripts/xlsx_read.py +0 -0
  144. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/xlsx/scripts/xlsx_recalc.py +0 -0
  145. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/xlsx/scripts/xlsx_restructure.py +0 -0
  146. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/xlsx/scripts/xlsx_to_csv.py +0 -0
  147. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/youtube-content/LICENSE +0 -0
  148. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/youtube-content/SKILL.md +0 -0
  149. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/youtube-content/references/output-formats.md +0 -0
  150. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/daily_skills/youtube-content/scripts/fetch_transcript.py +0 -0
  151. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/goal.py +0 -0
  152. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/headless.py +0 -0
  153. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/history.py +0 -0
  154. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/integrations/__init__.py +0 -0
  155. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/integrations/base.py +0 -0
  156. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/integrations/herdr.py +0 -0
  157. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/integrations/osc7501.py +0 -0
  158. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/journal.py +0 -0
  159. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/llm/__init__.py +0 -0
  160. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/llm/anthropic_client.py +0 -0
  161. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/llm/client.py +0 -0
  162. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/llm/factory.py +0 -0
  163. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/llm/openai_client.py +0 -0
  164. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/llm/responses_client.py +0 -0
  165. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/llm/retry.py +0 -0
  166. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/llm/scaffold.py +0 -0
  167. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/llm/sse.py +0 -0
  168. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/llm/usage.py +0 -0
  169. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/llm/verify.py +0 -0
  170. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/memory.py +0 -0
  171. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/model_switch.py +0 -0
  172. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/models.py +0 -0
  173. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/prompt.py +0 -0
  174. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/pump.py +0 -0
  175. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/sessions.py +0 -0
  176. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/skills.py +0 -0
  177. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/steer.py +0 -0
  178. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/subagents/board.py +0 -0
  179. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/subagents/commands.py +0 -0
  180. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/subagents/events.py +0 -0
  181. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/subagents/hooks.py +0 -0
  182. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/subagents/project_scan.py +0 -0
  183. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/subagents/tools.py +0 -0
  184. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/subagents/worktree.py +0 -0
  185. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/toggles.py +0 -0
  186. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/__init__.py +0 -0
  187. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/ask_user.py +0 -0
  188. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/base.py +0 -0
  189. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/bash.py +0 -0
  190. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/edit.py +0 -0
  191. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/find.py +0 -0
  192. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/grep.py +0 -0
  193. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/hooks.py +0 -0
  194. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/ignore.py +0 -0
  195. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/ls.py +0 -0
  196. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/memory.py +0 -0
  197. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/mutation_queue.py +0 -0
  198. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/read.py +0 -0
  199. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/run_code.py +0 -0
  200. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/session_search.py +0 -0
  201. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/skill_manage.py +0 -0
  202. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/web_fetch.py +0 -0
  203. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/web_search.py +0 -0
  204. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tools/write.py +0 -0
  205. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/trace.py +0 -0
  206. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tracing/__init__.py +0 -0
  207. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tracing/command.py +0 -0
  208. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tracing/index.py +0 -0
  209. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tracing/model.py +0 -0
  210. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tracing/server.py +0 -0
  211. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/tracing/static/index.html +0 -0
  212. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/__init__.py +0 -0
  213. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/app.py +0 -0
  214. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/app.tcss +0 -0
  215. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/banner.py +0 -0
  216. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/clipboard.py +0 -0
  217. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/commands.py +0 -0
  218. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/contrast.py +0 -0
  219. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/daily_look.py +0 -0
  220. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/gradient.py +0 -0
  221. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/motion.py +0 -0
  222. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/omarchy.py +0 -0
  223. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/paste_store.py +0 -0
  224. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/path_input.py +0 -0
  225. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/recent_models.py +0 -0
  226. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/screens/__init__.py +0 -0
  227. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/screens/agents_panel.py +0 -0
  228. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/screens/auth_dialog.py +0 -0
  229. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/screens/btw.py +0 -0
  230. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/screens/command_palette.py +0 -0
  231. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/screens/dialog.py +0 -0
  232. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/screens/game2048.py +0 -0
  233. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/screens/main.py +0 -0
  234. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/screens/model_picker.py +0 -0
  235. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/screens/session_picker.py +0 -0
  236. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/screens/verify_picker.py +0 -0
  237. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/syntax.py +0 -0
  238. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/theme.py +0 -0
  239. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/widgets/__init__.py +0 -0
  240. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/widgets/activity.py +0 -0
  241. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/widgets/chat.py +0 -0
  242. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/widgets/command_menu.py +0 -0
  243. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/widgets/input.py +0 -0
  244. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/widgets/key_hints.py +0 -0
  245. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/widgets/landing.py +0 -0
  246. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/widgets/mode_divider.py +0 -0
  247. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/widgets/pills.py +0 -0
  248. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/widgets/sidebar.py +0 -0
  249. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/widgets/status_bar.py +0 -0
  250. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/ui/widgets/tool_card.py +0 -0
  251. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/websearch/__init__.py +0 -0
  252. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/websearch/base.py +0 -0
  253. {limbo_code-0.3.5 → limbo_code-0.3.6}/src/limbo/websearch/deepseek.py +0 -0
  254. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/conftest.py +0 -0
  255. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/fixtures/omarchy/README.md +0 -0
  256. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/fixtures/omarchy/catppuccin-latte.toml +0 -0
  257. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/fixtures/omarchy/gruvbox.toml +0 -0
  258. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/fixtures/omarchy/hackerman.toml +0 -0
  259. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/fixtures/omarchy/lumon.toml +0 -0
  260. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/fixtures/omarchy/rose-pine.toml +0 -0
  261. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/fixtures/omarchy/tokyo-night.toml +0 -0
  262. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_agent.py +0 -0
  263. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_anthropic_client.py +0 -0
  264. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_auth.py +0 -0
  265. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_btw.py +0 -0
  266. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_cli.py +0 -0
  267. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_code_mode_agent.py +0 -0
  268. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_compaction.py +0 -0
  269. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_config.py +0 -0
  270. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_conversation.py +0 -0
  271. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_daily_mode.py +0 -0
  272. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_goal.py +0 -0
  273. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_headless.py +0 -0
  274. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_herdr.py +0 -0
  275. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_history.py +0 -0
  276. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_integration.py +0 -0
  277. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_interrupt.py +0 -0
  278. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_llm_client.py +0 -0
  279. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_model_switch.py +0 -0
  280. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_osc7501.py +0 -0
  281. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_plan_mode.py +0 -0
  282. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_prompt.py +0 -0
  283. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_pump.py +0 -0
  284. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_responses_client.py +0 -0
  285. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_retry.py +0 -0
  286. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_sessions.py +0 -0
  287. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_skills.py +0 -0
  288. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_sse.py +0 -0
  289. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_steer.py +0 -0
  290. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_task_board.py +0 -0
  291. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_trace.py +0 -0
  292. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/test_usage.py +0 -0
  293. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/tools/test_base.py +0 -0
  294. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/tools/test_bash.py +0 -0
  295. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/tools/test_edit.py +0 -0
  296. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/tools/test_grep.py +0 -0
  297. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/tools/test_hooks.py +0 -0
  298. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/tools/test_ignore.py +0 -0
  299. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/tools/test_mutation_queue.py +0 -0
  300. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/tools/test_read.py +0 -0
  301. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/tools/test_registry.py +0 -0
  302. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/tools/test_run_code.py +0 -0
  303. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/tools/test_web_search.py +0 -0
  304. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/tools/test_write.py +0 -0
  305. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/tracing/test_command.py +0 -0
  306. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/tracing/test_index.py +0 -0
  307. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/tracing/test_model.py +0 -0
  308. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/tracing/test_server.py +0 -0
  309. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/__snapshots__/test_snapshots/test_snapshot_idle.raw +0 -0
  310. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/__snapshots__/test_snapshots/test_snapshot_wide_layout_with_sidebar.raw +0 -0
  311. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_app_smoke.py +0 -0
  312. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_bang_ui.py +0 -0
  313. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_btw_ui.py +0 -0
  314. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_command_menu.py +0 -0
  315. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_compact_ui.py +0 -0
  316. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_daily_look.py +0 -0
  317. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_daily_mode_ui.py +0 -0
  318. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_goal_ui.py +0 -0
  319. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_input_paste.py +0 -0
  320. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_interrupt_ui.py +0 -0
  321. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_main_screen.py +0 -0
  322. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_model_picker.py +0 -0
  323. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_omarchy.py +0 -0
  324. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_path_input.py +0 -0
  325. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_scroll_follow.py +0 -0
  326. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_sessions_ui.py +0 -0
  327. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_sidebar.py +0 -0
  328. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_skills_ui.py +0 -0
  329. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_snapshots.py +0 -0
  330. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_steer_ui.py +0 -0
  331. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_tracing_viewer_ui.py +0 -0
  332. {limbo_code-0.3.5 → limbo_code-0.3.6}/tests/ui/test_widgets.py +0 -0
  333. {limbo_code-0.3.5 → limbo_code-0.3.6}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: limbo-code
3
- Version: 0.3.5
3
+ Version: 0.3.6
4
4
  Summary: A minimal terminal AI coding agent
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: httpx>=0.27
@@ -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 预算,超出即失败(0 = 不限)
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 user
81
- # interrupted the batch (RFC LIM-53). Only ever used for calls that
82
- # genuinely never ran, so history and disk cannot diverge.
83
- _NOT_EXECUTED_MESSAGE = "未执行:用户打断(ESC)"
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
- prompt = build_system_prompt(self.registry, self.workdir, memory=self._memory)
646
- if self._prompt_extra:
647
- prompt += "\n\n" + self._prompt_extra
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=_NOT_EXECUTED_MESSAGE)
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=_NOT_EXECUTED_MESSAGE)
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.registry.definitions()
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 UsageUpdate(total_tokens=self.session_total_tokens)
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 UsageUpdate(total_tokens=self.session_total_tokens)
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 when the kitty
33
- keyboard protocol is enabled.
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
- if config.ui.kitty_keyboard.disable_textual_kitty_key(
36
- in_herdr=bool(os.environ.get("HERDR_ENV"))
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). Disabling the protocol makes herdr negotiate IME-compatible host
142
- flags so committed IME text arrives as raw UTF-8.
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
- # Disable only inside the herdr multiplexer (detected via HERDR_ENV);
146
- # keep the protocol everywhere else. Safe default for CJK users.
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 behavior).
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, *, in_herdr: bool) -> bool:
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 or (
156
- self is KittyKeyboardMode.AUTO and in_herdr
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, except inside the herdr multiplexer where
173
- # it breaks IME/Chinese input (default)
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 token budget (0 = unlimited); a task over budget fails.
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
- "kimi-for-coding": _kimi_coding("kimi-for-coding"),
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``), ``scope_paths`` (comma list). The body is the profile's
16
- instructions, appended to the subagent's system prompt.
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
- if budget and event.total_tokens > budget and not runner.over_budget:
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
- if runner.agent is not None:
470
- runner.agent.interrupt()
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} 已取消"