induscode 0.1.1__tar.gz → 0.1.4__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 (222) hide show
  1. induscode-0.1.4/CHANGELOG.md +167 -0
  2. {induscode-0.1.1 → induscode-0.1.4}/PKG-INFO +2 -2
  3. {induscode-0.1.1 → induscode-0.1.4}/pyproject.toml +4 -3
  4. induscode-0.1.4/src/induscode/boot/runners/delegate_runner.py +376 -0
  5. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/boot/runners/session.py +73 -5
  6. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/capability_deck/cards/task.py +66 -5
  7. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/conductor/__init__.py +2 -0
  8. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/conductor/contract.py +22 -0
  9. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/conductor/signal_hub.py +23 -2
  10. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console/__init__.py +2 -0
  11. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console/app.py +458 -49
  12. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console/components/__init__.py +60 -0
  13. induscode-0.1.4/src/induscode/console/components/agents_view.py +388 -0
  14. induscode-0.1.4/src/induscode/console/components/background_agents.py +323 -0
  15. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console/components/banner.py +299 -47
  16. induscode-0.1.4/src/induscode/console/components/welcome.py +219 -0
  17. induscode-0.1.4/src/induscode/console/components/working_indicator.py +237 -0
  18. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console/contract.py +7 -3
  19. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console/input/chord.py +6 -3
  20. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console/mount.py +11 -5
  21. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console/overlays/__init__.py +5 -0
  22. induscode-0.1.4/src/induscode/console/overlays/boards.py +656 -0
  23. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console/overlays/pickers.py +26 -4
  24. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console/slash_commands/transcript.py +4 -3
  25. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console/slash_commands/workbench.py +2 -1
  26. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console/theme/__init__.py +2 -0
  27. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console/theme/palette.py +27 -0
  28. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console/theme/resolve.py +1 -0
  29. {induscode-0.1.1 → induscode-0.1.4}/tests/boot/test_session_persist.py +20 -1
  30. induscode-0.1.4/tests/conductor/test_delegate_progress.py +264 -0
  31. induscode-0.1.4/tests/console/test_agents_view.py +295 -0
  32. induscode-0.1.4/tests/console/test_background_agents.py +274 -0
  33. {induscode-0.1.1 → induscode-0.1.4}/tests/console/test_banner.py +115 -35
  34. {induscode-0.1.1 → induscode-0.1.4}/tests/console/test_commands_transcript.py +7 -6
  35. {induscode-0.1.1 → induscode-0.1.4}/tests/console/test_commands_workbench.py +2 -1
  36. {induscode-0.1.1 → induscode-0.1.4}/tests/console/test_console_app.py +241 -9
  37. {induscode-0.1.1 → induscode-0.1.4}/tests/console/test_e2e_pilot.py +195 -9
  38. {induscode-0.1.1 → induscode-0.1.4}/tests/console/test_overlays.py +223 -3
  39. {induscode-0.1.1 → induscode-0.1.4}/tests/console/test_reducer.py +2 -2
  40. {induscode-0.1.1 → induscode-0.1.4}/tests/console/test_slash_handlers.py +2 -1
  41. {induscode-0.1.1 → induscode-0.1.4}/tests/console/test_theme.py +49 -3
  42. induscode-0.1.4/tests/console/test_welcome.py +101 -0
  43. induscode-0.1.4/tests/console/test_working_indicator.py +187 -0
  44. induscode-0.1.1/CHANGELOG.md +0 -72
  45. {induscode-0.1.1 → induscode-0.1.4}/.gitignore +0 -0
  46. {induscode-0.1.1 → induscode-0.1.4}/.pindusagi/settings.json +0 -0
  47. {induscode-0.1.1 → induscode-0.1.4}/CREDITS.md +0 -0
  48. {induscode-0.1.1 → induscode-0.1.4}/GAP_REPORT.md +0 -0
  49. {induscode-0.1.1 → induscode-0.1.4}/NOTICE +0 -0
  50. {induscode-0.1.1 → induscode-0.1.4}/PARITY_REPORT.md +0 -0
  51. {induscode-0.1.1 → induscode-0.1.4}/README.md +0 -0
  52. {induscode-0.1.1 → induscode-0.1.4}/scripts/lineage_scan.py +0 -0
  53. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/__init__.py +0 -0
  54. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/addons/__init__.py +0 -0
  55. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/addons/contract.py +0 -0
  56. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/addons/dispatch/__init__.py +0 -0
  57. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/addons/dispatch/event_dispatcher.py +0 -0
  58. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/addons/dispatch/tool_interceptor.py +0 -0
  59. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/addons/host.py +0 -0
  60. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/addons/loader.py +0 -0
  61. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/addons/manifest.py +0 -0
  62. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/addons/surface.py +0 -0
  63. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/boot/__init__.py +0 -0
  64. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/boot/auth_vault.py +0 -0
  65. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/boot/boot.py +0 -0
  66. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/boot/contract.py +0 -0
  67. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/boot/invocation.py +0 -0
  68. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/boot/runners/__init__.py +0 -0
  69. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/boot/runners/link_runner.py +0 -0
  70. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/boot/runners/oneshot_runner.py +0 -0
  71. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/boot/runners/registry.py +0 -0
  72. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/boot/runners/repl_runner.py +0 -0
  73. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/boot/stages.py +0 -0
  74. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/boot/upgrade/__init__.py +0 -0
  75. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/boot/upgrade/apply.py +0 -0
  76. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/boot/upgrade/upgrades.py +0 -0
  77. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/briefing/__init__.py +0 -0
  78. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/briefing/compose.py +0 -0
  79. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/briefing/contract.py +0 -0
  80. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/briefing/macros.py +0 -0
  81. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/briefing/skills.py +0 -0
  82. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/capability_deck/__init__.py +0 -0
  83. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/capability_deck/bridge_ledger/__init__.py +0 -0
  84. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/capability_deck/bridge_ledger/key.py +0 -0
  85. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/capability_deck/bridge_ledger/ledger.py +0 -0
  86. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/capability_deck/bridge_ledger/network.py +0 -0
  87. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/capability_deck/builtin_bridge.py +0 -0
  88. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/capability_deck/cards/__init__.py +0 -0
  89. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/capability_deck/cards/bg_process.py +0 -0
  90. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/capability_deck/cards/memory.py +0 -0
  91. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/capability_deck/cards/saas.py +0 -0
  92. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/capability_deck/cards/todo.py +0 -0
  93. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/capability_deck/contract.py +0 -0
  94. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/capability_deck/manifest.py +0 -0
  95. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/capability_deck/provision.py +0 -0
  96. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/channels/__init__.py +0 -0
  97. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/channels/contract.py +0 -0
  98. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/channels/framer.py +0 -0
  99. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/channels/link/__init__.py +0 -0
  100. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/channels/link/dialog.py +0 -0
  101. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/channels/link/driver.py +0 -0
  102. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/channels/link/server.py +0 -0
  103. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/channels/oneshot.py +0 -0
  104. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/channels/ops.py +0 -0
  105. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/channels/session_ops.py +0 -0
  106. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/conductor/catalog.py +0 -0
  107. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/conductor/conductor.py +0 -0
  108. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/conductor/matcher.py +0 -0
  109. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/conductor/serialize.py +0 -0
  110. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/conductor/skill_parse.py +0 -0
  111. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/conductor/transcript_store.py +0 -0
  112. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console/components/banner_sweep.py +0 -0
  113. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console/components/emblem.py +0 -0
  114. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console/components/status_bar.py +0 -0
  115. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console/input/__init__.py +0 -0
  116. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console/input/dir_reader.py +0 -0
  117. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console/input/intents.py +0 -0
  118. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console/input/providers.py +0 -0
  119. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console/overlays/auth.py +0 -0
  120. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console/overlays/router.py +0 -0
  121. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console/overlays/sessions.py +0 -0
  122. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console/reducer.py +0 -0
  123. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console/resume_picker.py +0 -0
  124. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console/slash_commands/__init__.py +0 -0
  125. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console/slash_commands/builtins.py +0 -0
  126. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console/slash_commands/dynamic.py +0 -0
  127. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console/slash_commands/integrations.py +0 -0
  128. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console/startup.py +0 -0
  129. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console/theme/adapter.py +0 -0
  130. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console/theme/tokens.py +0 -0
  131. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console_slash/__init__.py +0 -0
  132. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console_slash/contract.py +0 -0
  133. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console_slash/registry.py +0 -0
  134. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console_slash/resolve.py +0 -0
  135. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/console_slash/shared.py +0 -0
  136. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/entry.py +0 -0
  137. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/insight/__init__.py +0 -0
  138. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/insight/collector.py +0 -0
  139. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/insight/replay.py +0 -0
  140. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/insight/wrapper.py +0 -0
  141. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/kit/__init__.py +0 -0
  142. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/kit/clipboard_image.py +0 -0
  143. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/kit/external_editor.py +0 -0
  144. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/kit/image.py +0 -0
  145. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/kit/shell.py +0 -0
  146. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/kit/tool_fetch.py +0 -0
  147. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/launch/__init__.py +0 -0
  148. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/launch/catalog.py +0 -0
  149. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/launch/contract.py +0 -0
  150. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/launch/credentials.py +0 -0
  151. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/launch/invocation/__init__.py +0 -0
  152. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/launch/invocation/attachments.py +0 -0
  153. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/launch/invocation/flags.py +0 -0
  154. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/launch/invocation/read.py +0 -0
  155. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/launch/invocation/usage.py +0 -0
  156. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/launch/oauth.py +0 -0
  157. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/launch/packages.py +0 -0
  158. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/launch/pickers.py +0 -0
  159. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/py.typed +0 -0
  160. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/runtime_bridge/__init__.py +0 -0
  161. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/runtime_bridge/bridges/__init__.py +0 -0
  162. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/runtime_bridge/bridges/_drive.py +0 -0
  163. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/runtime_bridge/bridges/builtins.py +0 -0
  164. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/runtime_bridge/bridges/claude_cli.py +0 -0
  165. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/runtime_bridge/bridges/codex_cli.py +0 -0
  166. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/runtime_bridge/bridges/indusagi_cli.py +0 -0
  167. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/runtime_bridge/broker.py +0 -0
  168. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/runtime_bridge/contract.py +0 -0
  169. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/runtime_bridge/sink.py +0 -0
  170. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/sessions/__init__.py +0 -0
  171. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/sessions/contract.py +0 -0
  172. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/sessions/library.py +0 -0
  173. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/settings/__init__.py +0 -0
  174. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/settings/contract.py +0 -0
  175. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/settings/manager.py +0 -0
  176. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/transcript_export/__init__.py +0 -0
  177. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/transcript_export/contract.py +0 -0
  178. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/transcript_export/publish.py +0 -0
  179. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/transcript_export/sgr.py +0 -0
  180. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/transcript_export/template.py +0 -0
  181. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/transcript_export/theme_bridge.py +0 -0
  182. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/window_budget/__init__.py +0 -0
  183. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/window_budget/budget/__init__.py +0 -0
  184. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/window_budget/budget/estimate.py +0 -0
  185. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/window_budget/budget/gate.py +0 -0
  186. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/window_budget/budget/slice.py +0 -0
  187. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/window_budget/condenser.py +0 -0
  188. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/window_budget/contract.py +0 -0
  189. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/window_budget/summarize/__init__.py +0 -0
  190. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/window_budget/summarize/condense.py +0 -0
  191. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/window_budget/summarize/prompt.py +0 -0
  192. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/workspace/__init__.py +0 -0
  193. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/workspace/brand.py +0 -0
  194. {induscode-0.1.1 → induscode-0.1.4}/src/induscode/workspace/locator.py +0 -0
  195. {induscode-0.1.1 → induscode-0.1.4}/tests/addons/test_addons.py +0 -0
  196. {induscode-0.1.1 → induscode-0.1.4}/tests/boot/test_boot.py +0 -0
  197. {induscode-0.1.1 → induscode-0.1.4}/tests/boot/test_invocation.py +0 -0
  198. {induscode-0.1.1 → induscode-0.1.4}/tests/boot/test_resume_picker.py +0 -0
  199. {induscode-0.1.1 → induscode-0.1.4}/tests/briefing/test_briefing.py +0 -0
  200. {induscode-0.1.1 → induscode-0.1.4}/tests/capability_deck/test_cards_provision.py +0 -0
  201. {induscode-0.1.1 → induscode-0.1.4}/tests/capability_deck/test_contract_ledger.py +0 -0
  202. {induscode-0.1.1 → induscode-0.1.4}/tests/channels/test_channels.py +0 -0
  203. {induscode-0.1.1 → induscode-0.1.4}/tests/conductor/test_catalog_store.py +0 -0
  204. {induscode-0.1.1 → induscode-0.1.4}/tests/conductor/test_contract_hub_skill.py +0 -0
  205. {induscode-0.1.1 → induscode-0.1.4}/tests/conductor/test_queue_fork.py +0 -0
  206. {induscode-0.1.1 → induscode-0.1.4}/tests/conductor/test_submit.py +0 -0
  207. {induscode-0.1.1 → induscode-0.1.4}/tests/console/test_commands_dynamic.py +0 -0
  208. {induscode-0.1.1 → induscode-0.1.4}/tests/console/test_commands_integrations.py +0 -0
  209. {induscode-0.1.1 → induscode-0.1.4}/tests/console/test_input.py +0 -0
  210. {induscode-0.1.1 → induscode-0.1.4}/tests/console/test_startup.py +0 -0
  211. {induscode-0.1.1 → induscode-0.1.4}/tests/console_slash/test_slash_core.py +0 -0
  212. {induscode-0.1.1 → induscode-0.1.4}/tests/insight/test_insight.py +0 -0
  213. {induscode-0.1.1 → induscode-0.1.4}/tests/kit/test_kit.py +0 -0
  214. {induscode-0.1.1 → induscode-0.1.4}/tests/launch/test_launch.py +0 -0
  215. {induscode-0.1.1 → induscode-0.1.4}/tests/launch/test_packages.py +0 -0
  216. {induscode-0.1.1 → induscode-0.1.4}/tests/runtime_bridge/test_runtime_bridge.py +0 -0
  217. {induscode-0.1.1 → induscode-0.1.4}/tests/sessions/test_sessions.py +0 -0
  218. {induscode-0.1.1 → induscode-0.1.4}/tests/settings/test_settings.py +0 -0
  219. {induscode-0.1.1 → induscode-0.1.4}/tests/test_public_api.py +0 -0
  220. {induscode-0.1.1 → induscode-0.1.4}/tests/test_scaffold.py +0 -0
  221. {induscode-0.1.1 → induscode-0.1.4}/tests/transcript_export/test_transcript_export.py +0 -0
  222. {induscode-0.1.1 → induscode-0.1.4}/tests/window_budget/test_window_budget.py +0 -0
@@ -0,0 +1,167 @@
1
+ # Changelog
2
+
3
+ ## 0.1.4 — 2026-06-28
4
+
5
+ Interaction and reliability fixes for the live turn, the Ctrl+C / exit flow,
6
+ and the picker boards. Requires `indusagi >= 0.1.3` (the background-free user
7
+ turn). The version is single-sourced from `[project].version` via
8
+ `importlib.metadata`, so `--version` and the banner pick it up after a
9
+ reinstall.
10
+
11
+ ### Live turn
12
+
13
+ - **Working spinner.** A new animated row — `⠴ Noodling… (5s · esc to
14
+ interrupt)` — shows while a turn is in flight: a braille spinner, a rotating
15
+ whimsical word (a concrete `Running tools` / `Compacting context` label
16
+ during those phases instead), an elapsed-seconds clock, and the interrupt
17
+ hint. It self-animates only while busy and hides itself when idle.
18
+ - **Spinner spans the whole turn.** Busy state is no longer cleared on the
19
+ per-round `turn_end` the framework emits between tool rounds, so the spinner
20
+ stays up *through* tool calls until the final answer lands — it clears only
21
+ at true settlement (`idle` / fault / abort).
22
+
23
+ ### Pickers
24
+
25
+ - **`/model` and `/settings` boards rebuilt to match the design.** Both are now
26
+ hand-rolled modal screens rather than thin restyles of the framework
27
+ dialogs: a teal round-bordered `⌕` search box, a saffron `›` cursor on the
28
+ selected row, per-row value tinting, a `↓ N more below` window hint, and a
29
+ description / choices detail line — matching the Saffron reference boards.
30
+
31
+ ### Ctrl+C, exit & abort
32
+
33
+ - **Ctrl+C no longer hard-quits on the first press.** The first Ctrl+C now
34
+ shows `Press Ctrl+C again to exit.` and the confirm window widened to 2s. A
35
+ `SIGINT` handler routes Ctrl+C that arrives as a signal (some terminals
36
+ deliver it that way) through the same flow, so it no longer kills the app on
37
+ the first press.
38
+ - **Silent abort.** Stopping a running turn with Ctrl+C / Esc no longer raises
39
+ a red `turn aborted by caller` error toast — the turn just stops quietly.
40
+ - **Clean exit.** The conversation transcript is no longer dumped into the
41
+ terminal scrollback on an interactive exit; it is emitted only when a host
42
+ explicitly captures it.
43
+
44
+ ### Fixes
45
+
46
+ - **`/clear` (and `/new`, `/reset`) crash.** These commands dispatched raw
47
+ `dict` events, which the live reducer rejected with `AttributeError: 'dict'
48
+ object has no attribute 'type'`; they now dispatch the proper event
49
+ dataclasses (`RowsSet` / `BlocksClear` / `StatusClear`, and `ToggleReasoning`
50
+ for `/debug`).
51
+
52
+ ## 0.1.3 — 2026-06-26
53
+
54
+ Saffron interface refresh and live multi-agent visibility. The console gains
55
+ a per-session welcome experience, a saffron theme as the default, denser
56
+ in-chat tooling, and a pinned view onto background agents — including
57
+ drill-in sub-agent progress streamed live from the task tool. No new
58
+ subsystem packages; the version is single-sourced from this file's
59
+ `[project].version` via `importlib.metadata`, so `--version` and the startup
60
+ banner pick it up after a reinstall (the dev-checkout fallback is unchanged).
61
+
62
+ ### Theme & welcome
63
+
64
+ - **Saffron theme is the new default** — the saffron scheme registers as the
65
+ startup console theme, replacing the prior default.
66
+ - **Per-session saffron welcome card** — shown once per session, with a
67
+ rotating mascot, a rotating tip, and a *What's new* line summarising this
68
+ release.
69
+ - **INDUS CODE chat masthead** — a saffron masthead is stamped above the
70
+ transcript after the first message of a session.
71
+
72
+ ### Pickers & menus
73
+
74
+ - **Saffron `/settings` board** — the settings overlay re-skinned in the
75
+ saffron palette.
76
+ - **Saffron `/model` board** — the model picker re-skinned to match.
77
+ - **2-column saffron slash menu** — the slash-command autocomplete renders as
78
+ a two-column saffron menu.
79
+
80
+ ### Tool output & activity
81
+
82
+ - **Concise tool output** — tool results collapse to a 3-line preview by
83
+ default, expandable to ~20 lines with **Ctrl+O**.
84
+ - **Live Tool Activity panel disabled** — the streaming tool-activity panel is
85
+ turned off behind the `SHOW_TOOL_ACTIVITY` flag (default off).
86
+ - **Workflow tool hidden** — the workflow tool is removed from the capability
87
+ deck behind the `WORKFLOW_TOOL_ENABLED` flag (default off).
88
+
89
+ ### Background agents
90
+
91
+ - **Pinned Background Agents panel** — a saffron panel pinned in the console
92
+ listing active background agents.
93
+ - **Subagent drill-in view** — press **←** on a background agent to open a
94
+ dedicated Subagent View.
95
+ - **Live sub-agent progress** — sub-agent token counts and activity stream
96
+ live from the task tool into the panel and drill-in view.
97
+
98
+ ## 0.1.0 — 2026-06-11
99
+
100
+ First release of the Python rebuild. Full port of the TypeScript
101
+ `indusagi-coding-agent` (lineage **v0.1.62**, `indus-code-rebuild`) onto the
102
+ Python `indusagi` framework, built milestone-by-milestone (M0–M6) per
103
+ `indus-code-rebuild/PYTHON_PORT_PLAN/PLAN.md`. **649 tests green**, lineage
104
+ scan clean, wheel verified in a fresh venv.
105
+
106
+ ### Subsystems
107
+
108
+ - **workspace** — brand/version (single-sourced via `importlib.metadata`,
109
+ literal fallback) and the `~/.pindusagi` locator composed over the
110
+ framework `Locator` (`INDUSAGI_CODING_AGENT_DIR` > `INDUSAGI_HOME`).
111
+ - **kit** — leaf utilities.
112
+ - **settings** — two-tier `PreferenceStore` (project-write-only).
113
+ - **briefing** — system-prompt composition, macro scanner, SKILL.md walker.
114
+ - **window_budget** — context-window budgeting and condense seam
115
+ (3.6 chars/token heuristic kept).
116
+ - **insight** — tracing wrapper over `indusagi.tracing` with collector sink,
117
+ replay, and FNV-1a gate.
118
+ - **transcript_export** — SGR state machine, theme bridge, HTML publisher on
119
+ `markdown-it-py` + `pygments` (replacing `marked` + `highlight.js`).
120
+ - **conductor** — turn loop with retry/queue-drain/persist-tail, condense
121
+ seam, fork/navigate, signal hub (10-tag translator table), model
122
+ catalog/matcher over `indusagi.ai`, branchable NDJSON `TranscriptStore`,
123
+ message⇄dict codec (`serialize.py`), skill parsing.
124
+ - **runtime_bridge** — snapshot sink (mutable builder, per-push frozen
125
+ snapshots), broker (route/exchange/resume-tap), exchange driver + dialect
126
+ parsers.
127
+ - **capability_deck** — builtin-bridge factory table, `PROFILE_TABLE`
128
+ provisioning, cards (todo ledger, bg-process daemon table on asyncio
129
+ subprocesses with SIGTERM→SIGKILL grace, task/saas/memory), bridge ledger
130
+ with ULID keys (`python-ulid`) and `mount_protocol_bridge` networking.
131
+ - **addons** — discovery walk, importlib loader, event dispatcher
132
+ (gate-throw fails open, transform-throw keeps prior payload), host with
133
+ reserved names and first-wins conflicts.
134
+ - **launch / boot / channels / sessions** — 17-flag invocation table on the
135
+ extended framework parser (`@file` attachments, `--`, list flags, `-pi`
136
+ clustering), credentials + OAuth adapter, multi-account `AuthVault` as the
137
+ single `auth.json` writer (0600, tolerant of framework single-record
138
+ shape), boot orchestrator + runners (oneshot/link/repl), NDJSON channel
139
+ framer (U+2028/29 escapes), session ops + link server + dialog bridge,
140
+ `SessionLibrary` over the transcript store.
141
+ - **console / console_slash** — Textual TUI replacing the Ink console:
142
+ theme engine (4 schemes as native Textual themes), reducer, chrome
143
+ widgets, editor/keybindings with chord latch, autocomplete, overlays as
144
+ `push_screen` flows (incl. OAuth sign-in on `asyncio.Future`), and **26
145
+ slash commands** (18 transcript/workbench built-ins + 8 integration
146
+ commands, plus dynamic skill/template commands).
147
+ - **entry** — console scripts `pindus` and `induscode`.
148
+
149
+ ### Key decisions
150
+
151
+ - **Clean-room discipline** — independent implementation; no source copied
152
+ from any prior codebase; enforced by `scripts/lineage_scan.py` and the
153
+ public-API guard test, both release gates.
154
+ - **Framework reuse over reinvention** — LLM abstraction, agent core, MCP
155
+ client, Textual shell primitives, and stage runner come from `indusagi`
156
+ (`[mcp,tui]` extras required); the agent keeps only its own seams
157
+ (transcript tree, vault, signal translation, deck).
158
+ - **Behavioral parity with the TS lineage** — suites ported at full
159
+ strength (649 pytest cases vs the TS line's 332 vitest cases, never
160
+ weakened); versioning restarts at 0.1.0 while the TS line was at 0.1.62.
161
+ - **Library swaps** — `marked`/`highlight.js` → `markdown-it-py`/`pygments`;
162
+ `ulid` → `python-ulid`; Ink/React → Textual; jiti virtual modules →
163
+ plain `importlib` addon loading.
164
+ - **Packaging** — hatchling build; `py.typed` shipped; `NOTICE` and
165
+ `CREDITS.md` embedded via PEP 639 `license-files`; wheel gate = fresh
166
+ venv install (framework wheel first) + `--version`/`--help`/
167
+ `--list-models`/subsystem-import smokes.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: induscode
3
- Version: 0.1.1
3
+ Version: 0.1.4
4
4
  Summary: Indusagi coding agent — terminal-first AI coding agent on the indusagi framework (Python rebuild)
5
5
  Project-URL: Homepage, https://github.com/varunisrani/indusagi-ts
6
6
  Project-URL: Repository, https://github.com/varunisrani/indusagi-ts
@@ -22,7 +22,7 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
22
22
  Classifier: Topic :: Software Development :: Code Generators
23
23
  Classifier: Typing :: Typed
24
24
  Requires-Python: >=3.11
25
- Requires-Dist: indusagi[mcp,tui]>=0.1.1
25
+ Requires-Dist: indusagi[mcp,tui]>=0.1.3
26
26
  Requires-Dist: markdown-it-py>=3.0
27
27
  Requires-Dist: pygments>=2.18
28
28
  Requires-Dist: python-ulid>=2.7
@@ -8,7 +8,7 @@ name = "induscode"
8
8
  # it ports the TypeScript lineage `indusagi-coding-agent`, which was at VERSION
9
9
  # 0.1.62 (indus-code-rebuild/src/workspace/brand.ts) when this port began.
10
10
  # Runtime code reads this via importlib.metadata — never duplicate it.
11
- version = "0.1.1"
11
+ version = "0.1.4"
12
12
  description = "Indusagi coding agent — terminal-first AI coding agent on the indusagi framework (Python rebuild)"
13
13
  authors = [{ name = "Varun Israni" }, { name = "IndusAGI Team", email = "team@indusagi.ai" }]
14
14
  readme = "README.md"
@@ -32,10 +32,11 @@ dependencies = [
32
32
  # The framework, from PyPI. The [mcp,tui] extras are required: the framework's
33
33
  # shell_app barrel (which our workspace layer composes) imports `mcp` and
34
34
  # `textual` eagerly, and the agent's own MCP enrollment + Textual console need
35
- # them anyway. Pinned to the published framework (>=0.1.1).
35
+ # them anyway. Pinned to the published framework (>=0.1.3 — the
36
+ # background-free user turn lands in the framework's react_ink).
36
37
  # For local dev, editable-install the framework first:
37
38
  # pip install -e ../indusagi-python-rebuild (then pip install -e .)
38
- "indusagi[mcp,tui]>=0.1.1",
39
+ "indusagi[mcp,tui]>=0.1.3",
39
40
  "markdown-it-py>=3.0", # transcript-export markdown (replaces marked)
40
41
  "pygments>=2.18", # transcript-export highlighting (replaces highlight.js)
41
42
  "python-ulid>=2.7", # bridge-ledger ULID keys (capability_deck)
@@ -0,0 +1,376 @@
1
+ """Boot helper: a live :class:`DelegateRunner` that makes the ``task`` tool real.
2
+
3
+ Port of TS ``src/boot/runners/delegate-runner.ts``.
4
+
5
+ The ``task`` capability (:mod:`induscode.capability_deck.cards.task`) advertises
6
+ sub-agent delegation but only *runs* it when a :class:`DelegateRunner` is wired
7
+ into the deck context under :data:`DELEGATE_HANDLE_KEY`; absent one it degrades
8
+ to a typed stub. This module supplies that runner.
9
+
10
+ Each delegated objective spins a *fresh, isolated* framework :class:`~indusagi.agent.Agent`
11
+ (the same ``Agent`` the conductor already drives) bound to the parent's model id
12
+ and credential resolver, given a survey-style system prompt and a deck that
13
+ DELIBERATELY excludes the ``task`` card. That exclusion is the recursion guard:
14
+ a sub-agent with no ``task`` tool cannot spawn its own sub-agents. The runner
15
+ submits one prompt, lets the sub-agent run its own tool loop to completion, then
16
+ extracts the final assistant turn's text as the single report handed back to the
17
+ parent.
18
+
19
+ The runner never raises out of :meth:`run`: a model that does not resolve, a
20
+ sub-agent error, or an empty transcript all map to a ``DelegateResult(ok=False,
21
+ report=...)`` so a delegation failure surfaces to the parent agent as an
22
+ ordinary tool error rather than crashing the turn.
23
+
24
+ The ``spawn`` / ``tools`` options are pure test seams — they let a unit test
25
+ drive the runner with an in-memory fake instead of a real network round-trip.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ from collections.abc import Awaitable, Callable, Sequence
31
+ from typing import Any, Protocol
32
+
33
+ from induscode.capability_deck import AgentTool, DeckContext, provision_deck
34
+ from induscode.conductor import ModelCatalog, ModelMatcher
35
+
36
+ from ...capability_deck.cards.task import (
37
+ ActivityLine,
38
+ DelegateProgress,
39
+ DelegateRequest,
40
+ DelegateResult,
41
+ DelegateRunner,
42
+ )
43
+
44
+ __all__ = [
45
+ "DelegateRunnerOptions",
46
+ "DelegateSubAgent",
47
+ "create_delegate_runner",
48
+ ]
49
+
50
+
51
+ # ---------------------------------------------------------------------------
52
+ # The minimal sub-agent surface the runner drives
53
+ # ---------------------------------------------------------------------------
54
+
55
+
56
+ class DelegateSubAgent(Protocol):
57
+ """The minimal sub-agent surface the runner drives.
58
+
59
+ A real framework :class:`~indusagi.agent.Agent` satisfies this
60
+ structurally; a test passes a lightweight fake via
61
+ :attr:`DelegateRunnerOptions.spawn`. Only the pieces the runner touches are
62
+ named — submit a prompt, abort, and read the resulting transcript/error
63
+ afterward.
64
+ """
65
+
66
+ async def prompt(self, input: str) -> None: ...
67
+
68
+ def abort(self) -> None: ...
69
+
70
+ @property
71
+ def state(self) -> Any:
72
+ """The agent state: ``.messages`` (a sequence) and an optional
73
+ ``.error`` string."""
74
+ ...
75
+
76
+ # Optional event subscription (the real framework Agent provides it). The
77
+ # runner uses it to recompute live token spend as the sub-agent works; a
78
+ # test fake may omit it, in which case no progress is reported. (A Python
79
+ # Protocol cannot mark a method optional, so the runner probes with
80
+ # ``getattr`` instead of declaring it here.)
81
+
82
+
83
+ # ---------------------------------------------------------------------------
84
+ # Transcript projection helpers
85
+ # ---------------------------------------------------------------------------
86
+
87
+
88
+ def _field(obj: Any, name: str, default: Any = None) -> Any:
89
+ """Read ``name`` off a mapping key or an attribute, whichever the value
90
+ carries (framework messages are dataclasses; tests may pass dicts)."""
91
+ if isinstance(obj, dict):
92
+ return obj.get(name, default)
93
+ return getattr(obj, name, default)
94
+
95
+
96
+ def _sum_tokens(messages: Sequence[Any]) -> int:
97
+ """Sum the token spend across a sub-agent's assistant turns.
98
+
99
+ Each assistant message carries a ``usage`` of input/output token counts;
100
+ this totals input + output across the transcript so far, tolerating
101
+ messages that carry no usage (user/tool turns, or a turn still streaming).
102
+ """
103
+ total = 0
104
+ for message in messages:
105
+ usage = _field(message, "usage")
106
+ if usage is not None:
107
+ total += int(_field(usage, "input", 0) or 0)
108
+ total += int(_field(usage, "output", 0) or 0)
109
+ return total
110
+
111
+
112
+ def _one_line(text: str, max_len: int = 120) -> str:
113
+ """Collapse text to a single trimmed line clipped to ``max_len`` chars."""
114
+ flat = " ".join(text.split()).strip()
115
+ if len(flat) > max_len:
116
+ return f"{flat[: max_len - 1]}…"
117
+ return flat
118
+
119
+
120
+ def _arg_preview(args: Any) -> str:
121
+ """A compact ``(arg)`` preview from a tool call's first argument value."""
122
+ if not isinstance(args, dict) or len(args) == 0:
123
+ return ""
124
+ value = next(iter(args.values()))
125
+ if isinstance(value, str):
126
+ text = value
127
+ else:
128
+ import json
129
+
130
+ try:
131
+ text = json.dumps(value)
132
+ except Exception:
133
+ text = str(value)
134
+ return f" ({_one_line(str(text), 56)})"
135
+
136
+
137
+ def _build_activity(messages: Sequence[Any]) -> tuple[ActivityLine, ...]:
138
+ """Project a sub-agent's recent activity into compact display lines.
139
+
140
+ Splits assistant messages into reasoning/answer snippets and tool calls,
141
+ newest last, capped at the last ~14 so the stream stays light to ship over
142
+ the progress channel.
143
+ """
144
+ out: list[ActivityLine] = []
145
+ for message in messages:
146
+ if _field(message, "role") != "assistant":
147
+ continue
148
+ content = _field(message, "content")
149
+ if not isinstance(content, (list, tuple)):
150
+ continue
151
+ for part in content:
152
+ part_type = _field(part, "type")
153
+ if part_type == "text":
154
+ text = _field(part, "text")
155
+ if isinstance(text, str) and text.strip():
156
+ out.append(ActivityLine(tone="reason", text=_one_line(text)))
157
+ elif part_type == "thinking":
158
+ thinking = _field(part, "thinking")
159
+ if isinstance(thinking, str) and thinking.strip():
160
+ out.append(ActivityLine(tone="reason", text=_one_line(thinking)))
161
+ elif part_type == "toolCall":
162
+ name = _field(part, "name")
163
+ if isinstance(name, str):
164
+ label = name[:1].upper() + name[1:]
165
+ out.append(
166
+ ActivityLine(
167
+ tone="tool",
168
+ text=f"{label}{_arg_preview(_field(part, 'arguments'))}",
169
+ )
170
+ )
171
+ return tuple(out[-14:])
172
+
173
+
174
+ def _final_assistant_text(messages: Sequence[Any]) -> str:
175
+ """Extract the report text from a finished sub-agent transcript.
176
+
177
+ Walks backward to the last ``assistant`` message and joins its
178
+ ``type=='text'`` blocks; thinking and tool-call blocks carry no report and
179
+ are skipped. Returns ``''`` when there is no assistant turn (or it produced
180
+ no text), which the caller maps to a ``'(no output)'`` report.
181
+ """
182
+ for message in reversed(list(messages)):
183
+ if _field(message, "role") != "assistant":
184
+ continue
185
+ content = _field(message, "content")
186
+ if not isinstance(content, (list, tuple)):
187
+ return content if isinstance(content, str) else ""
188
+ parts: list[str] = []
189
+ for block in content:
190
+ if _field(block, "type") == "text":
191
+ text = _field(block, "text")
192
+ if isinstance(text, str):
193
+ parts.append(text)
194
+ return "".join(parts)
195
+ return ""
196
+
197
+
198
+ # ---------------------------------------------------------------------------
199
+ # Options
200
+ # ---------------------------------------------------------------------------
201
+
202
+
203
+ class DelegateRunnerOptions:
204
+ """Configuration for :func:`create_delegate_runner`.
205
+
206
+ A plain options bag (the TS interface). ``spawn`` and ``tools`` are pure
207
+ test seams.
208
+ """
209
+
210
+ def __init__(
211
+ self,
212
+ *,
213
+ modelId: str,
214
+ cwd: str,
215
+ system: str,
216
+ getApiKey: Callable[[str], Awaitable[str | None] | str | None] | None = None,
217
+ spawn: Callable[[str, str | None], DelegateSubAgent] | None = None,
218
+ tools: Callable[[], list[AgentTool]] | None = None,
219
+ ) -> None:
220
+ #: The model id the sub-agent runs under (the parent's resolved model).
221
+ self.modelId = modelId
222
+ #: The working directory the sub-agent's deck is scoped to.
223
+ self.cwd = cwd
224
+ #: The system prompt that shapes the sub-agent's behaviour.
225
+ self.system = system
226
+ #: Per-call credential resolver, forwarded to the framework Agent.
227
+ self.getApiKey = getApiKey
228
+ #: Test seam: build the sub-agent from the objective.
229
+ self.spawn = spawn
230
+ #: Test seam: the tool deck the sub-agent runs with.
231
+ self.tools = tools
232
+
233
+
234
+ # ---------------------------------------------------------------------------
235
+ # Runner factory
236
+ # ---------------------------------------------------------------------------
237
+
238
+
239
+ class _LiveDelegateRunner:
240
+ """A live :class:`DelegateRunner` bound to a resolved model + options."""
241
+
242
+ def __init__(self, options: DelegateRunnerOptions, model: Any) -> None:
243
+ self._options = options
244
+ # Resolved once up front; ``None`` means every run reports ok=False
245
+ # rather than raising (a misconfigured model can never crash a turn).
246
+ self._model = model
247
+
248
+ def _spawn_agent(self, request: DelegateRequest) -> DelegateSubAgent:
249
+ from indusagi.agent import Agent
250
+
251
+ opts = self._options
252
+ if opts.tools is not None:
253
+ tools = opts.tools()
254
+ else:
255
+ # The sub-agent's deck — the 'authoring' profile excludes the
256
+ # ``task`` card, so the sub-agent cannot recurse into delegation.
257
+ tools = provision_deck("authoring", DeckContext(cwd=opts.cwd)).tools()
258
+ initial_state: dict[str, Any] = {
259
+ # ``model`` is guaranteed non-None here: the run() guard returns
260
+ # early when no spawn seam was given and the model did not resolve.
261
+ "model": self._model,
262
+ "systemPrompt": opts.system,
263
+ "tools": list(tools),
264
+ }
265
+ kwargs: dict[str, Any] = {}
266
+ if opts.getApiKey is not None:
267
+ kwargs["get_api_key"] = opts.getApiKey
268
+ return Agent(initial_state=initial_state, **kwargs) # type: ignore[return-value]
269
+
270
+ async def run(
271
+ self,
272
+ request: DelegateRequest,
273
+ signal: object | None = None,
274
+ on_progress: Callable[[DelegateProgress], None] | None = None,
275
+ ) -> DelegateResult:
276
+ opts = self._options
277
+ if opts.spawn is None and self._model is None:
278
+ return DelegateResult(ok=False, report="no model resolved for delegation")
279
+ if signal is not None and bool(getattr(signal, "aborted", False)):
280
+ return DelegateResult(ok=False, report="delegation aborted")
281
+
282
+ # Fold any parent-supplied context into the objective so the otherwise
283
+ # history-less sub-agent starts with everything it needs.
284
+ objective = (
285
+ f"{request.objective}\n\nContext:\n{request.context}"
286
+ if request.context
287
+ else request.objective
288
+ )
289
+
290
+ agent: DelegateSubAgent = (
291
+ opts.spawn(request.objective, request.context)
292
+ if opts.spawn is not None
293
+ else self._spawn_agent(request)
294
+ )
295
+
296
+ # Forward cancellation: an abort raised mid-run calls agent.abort().
297
+ on_abort = agent.abort
298
+ add_listener = getattr(signal, "add_event_listener", None) or getattr(
299
+ signal, "addEventListener", None
300
+ )
301
+ if add_listener is not None:
302
+ try:
303
+ add_listener("abort", on_abort)
304
+ except Exception:
305
+ add_listener = None
306
+
307
+ # Stream live token spend while the sub-agent works: recompute the
308
+ # running total on every agent event and report it (only when it
309
+ # changes) so the host's background-agents panel shows a live count. The
310
+ # framework Agent provides ``subscribe``; a fake without it reports
311
+ # nothing.
312
+ last_signature = ""
313
+
314
+ def report_progress() -> None:
315
+ nonlocal last_signature
316
+ if on_progress is None:
317
+ return
318
+ messages = _field(agent.state, "messages") or ()
319
+ tokens = _sum_tokens(messages)
320
+ activity = _build_activity(messages)
321
+ signature = f"{tokens}:{len(activity)}"
322
+ if signature != last_signature:
323
+ last_signature = signature
324
+ on_progress(DelegateProgress(tokens=tokens, activity=activity))
325
+
326
+ off_progress: Callable[[], None] | None = None
327
+ subscribe = getattr(agent, "subscribe", None)
328
+ if on_progress is not None and callable(subscribe):
329
+ off_progress = subscribe(lambda _event: report_progress())
330
+
331
+ try:
332
+ await agent.prompt(objective)
333
+ report_progress()
334
+ except Exception as cause: # noqa: BLE001 — a sub-agent failure is a
335
+ # delegation failure, not a crash of the parent's tool call.
336
+ message = str(cause)
337
+ return DelegateResult(ok=False, report=message or "delegation failed")
338
+ finally:
339
+ if off_progress is not None:
340
+ try:
341
+ off_progress()
342
+ except Exception:
343
+ pass
344
+ remove_listener = getattr(signal, "remove_event_listener", None) or getattr(
345
+ signal, "removeEventListener", None
346
+ )
347
+ if add_listener is not None and remove_listener is not None:
348
+ try:
349
+ remove_listener("abort", on_abort)
350
+ except Exception:
351
+ pass
352
+
353
+ error = _field(agent.state, "error")
354
+ report = _final_assistant_text(_field(agent.state, "messages") or ())
355
+ return DelegateResult(
356
+ ok=error is None and len(report) > 0,
357
+ report=error if error is not None else (report or "(no output)"),
358
+ )
359
+
360
+
361
+ def create_delegate_runner(opts: DelegateRunnerOptions) -> DelegateRunner:
362
+ """Build a live :class:`DelegateRunner` the host wires into the deck context.
363
+
364
+ The model is resolved once up front; if the id resolves to nothing the
365
+ runner still builds but every :meth:`run` reports ``ok=False`` rather than
366
+ raising, so a misconfigured model can never crash the parent's turn.
367
+
368
+ :param opts: the model id, cwd, system prompt, and optional credential/test
369
+ seams
370
+ :returns: a runner satisfying the task card's :class:`DelegateRunner`
371
+ contract
372
+ """
373
+ # Resolve the framework Model once (same pattern the conductor uses).
374
+ card = ModelMatcher(ModelCatalog()).resolve_card(opts.modelId)
375
+ model = card.model if card is not None else None
376
+ return _LiveDelegateRunner(opts, model)