npcsh 1.1.20__py3-none-any.whl → 1.1.22__py3-none-any.whl

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 (186) hide show
  1. npcsh/_state.py +15 -76
  2. npcsh/benchmark/npcsh_agent.py +22 -14
  3. npcsh/benchmark/templates/install-npcsh.sh.j2 +2 -2
  4. npcsh/diff_viewer.py +3 -3
  5. npcsh/mcp_server.py +9 -1
  6. npcsh/npc_team/alicanto.npc +12 -6
  7. npcsh/npc_team/corca.npc +0 -1
  8. npcsh/npc_team/frederic.npc +2 -3
  9. npcsh/npc_team/jinxs/lib/core/compress.jinx +373 -85
  10. npcsh/npc_team/jinxs/lib/core/edit_file.jinx +83 -61
  11. npcsh/npc_team/jinxs/lib/core/search/db_search.jinx +17 -6
  12. npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +17 -6
  13. npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +52 -14
  14. npcsh/npc_team/jinxs/{bin → lib/utils}/benchmark.jinx +2 -2
  15. npcsh/npc_team/jinxs/{bin → lib/utils}/jinxs.jinx +12 -12
  16. npcsh/npc_team/jinxs/{bin → lib/utils}/models.jinx +7 -7
  17. npcsh/npc_team/jinxs/{bin → lib/utils}/setup.jinx +6 -6
  18. npcsh/npc_team/jinxs/modes/alicanto.jinx +1633 -295
  19. npcsh/npc_team/jinxs/modes/arxiv.jinx +5 -5
  20. npcsh/npc_team/jinxs/modes/build.jinx +378 -0
  21. npcsh/npc_team/jinxs/modes/config_tui.jinx +300 -0
  22. npcsh/npc_team/jinxs/modes/convene.jinx +597 -0
  23. npcsh/npc_team/jinxs/modes/corca.jinx +777 -387
  24. npcsh/npc_team/jinxs/modes/git.jinx +795 -0
  25. {npcsh-1.1.20.data/data/npcsh/npc_team → npcsh/npc_team/jinxs/modes}/kg.jinx +82 -15
  26. npcsh/npc_team/jinxs/modes/memories.jinx +414 -0
  27. npcsh/npc_team/jinxs/{bin → modes}/nql.jinx +10 -21
  28. npcsh/npc_team/jinxs/modes/papers.jinx +578 -0
  29. npcsh/npc_team/jinxs/modes/plonk.jinx +503 -308
  30. npcsh/npc_team/jinxs/modes/reattach.jinx +3 -3
  31. npcsh/npc_team/jinxs/modes/spool.jinx +3 -3
  32. npcsh/npc_team/jinxs/{bin → modes}/team.jinx +12 -12
  33. npcsh/npc_team/jinxs/modes/vixynt.jinx +388 -0
  34. npcsh/npc_team/jinxs/modes/wander.jinx +454 -181
  35. npcsh/npc_team/jinxs/modes/yap.jinx +630 -182
  36. npcsh/npc_team/kadiefa.npc +2 -1
  37. npcsh/npc_team/sibiji.npc +3 -3
  38. npcsh/npcsh.py +112 -47
  39. npcsh/routes.py +4 -1
  40. npcsh/salmon_simulation.py +0 -0
  41. npcsh-1.1.22.data/data/npcsh/npc_team/alicanto.jinx +1694 -0
  42. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/alicanto.npc +12 -6
  43. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/arxiv.jinx +5 -5
  44. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/benchmark.jinx +2 -2
  45. npcsh-1.1.22.data/data/npcsh/npc_team/build.jinx +378 -0
  46. npcsh-1.1.22.data/data/npcsh/npc_team/compress.jinx +428 -0
  47. npcsh-1.1.22.data/data/npcsh/npc_team/config_tui.jinx +300 -0
  48. npcsh-1.1.22.data/data/npcsh/npc_team/corca.jinx +820 -0
  49. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/corca.npc +0 -1
  50. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/db_search.jinx +17 -6
  51. npcsh-1.1.22.data/data/npcsh/npc_team/edit_file.jinx +119 -0
  52. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/file_search.jinx +17 -6
  53. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/frederic.npc +2 -3
  54. npcsh-1.1.22.data/data/npcsh/npc_team/git.jinx +795 -0
  55. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/jinxs.jinx +12 -12
  56. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/kadiefa.npc +2 -1
  57. {npcsh/npc_team/jinxs/bin → npcsh-1.1.22.data/data/npcsh/npc_team}/kg.jinx +82 -15
  58. npcsh-1.1.22.data/data/npcsh/npc_team/memories.jinx +414 -0
  59. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/models.jinx +7 -7
  60. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/nql.jinx +10 -21
  61. npcsh-1.1.22.data/data/npcsh/npc_team/papers.jinx +578 -0
  62. npcsh-1.1.22.data/data/npcsh/npc_team/plonk.jinx +574 -0
  63. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/reattach.jinx +3 -3
  64. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/setup.jinx +6 -6
  65. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sibiji.npc +3 -3
  66. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/spool.jinx +3 -3
  67. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/team.jinx +12 -12
  68. npcsh-1.1.22.data/data/npcsh/npc_team/vixynt.jinx +388 -0
  69. npcsh-1.1.22.data/data/npcsh/npc_team/wander.jinx +728 -0
  70. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/web_search.jinx +52 -14
  71. npcsh-1.1.22.data/data/npcsh/npc_team/yap.jinx +716 -0
  72. {npcsh-1.1.20.dist-info → npcsh-1.1.22.dist-info}/METADATA +246 -281
  73. npcsh-1.1.22.dist-info/RECORD +240 -0
  74. npcsh-1.1.22.dist-info/entry_points.txt +11 -0
  75. npcsh/npc_team/jinxs/bin/config_tui.jinx +0 -300
  76. npcsh/npc_team/jinxs/bin/memories.jinx +0 -317
  77. npcsh/npc_team/jinxs/bin/vixynt.jinx +0 -122
  78. npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +0 -418
  79. npcsh/npc_team/jinxs/lib/core/search/mem_review.jinx +0 -73
  80. npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +0 -388
  81. npcsh/npc_team/jinxs/lib/core/search.jinx +0 -54
  82. npcsh/npc_team/jinxs/lib/research/paper_search.jinx +0 -412
  83. npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +0 -386
  84. npcsh/npc_team/jinxs/lib/utils/build.jinx +0 -65
  85. npcsh/npc_team/plonkjr.npc +0 -23
  86. npcsh-1.1.20.data/data/npcsh/npc_team/alicanto.jinx +0 -356
  87. npcsh-1.1.20.data/data/npcsh/npc_team/build.jinx +0 -65
  88. npcsh-1.1.20.data/data/npcsh/npc_team/compress.jinx +0 -140
  89. npcsh-1.1.20.data/data/npcsh/npc_team/config_tui.jinx +0 -300
  90. npcsh-1.1.20.data/data/npcsh/npc_team/corca.jinx +0 -430
  91. npcsh-1.1.20.data/data/npcsh/npc_team/edit_file.jinx +0 -97
  92. npcsh-1.1.20.data/data/npcsh/npc_team/kg_search.jinx +0 -418
  93. npcsh-1.1.20.data/data/npcsh/npc_team/mem_review.jinx +0 -73
  94. npcsh-1.1.20.data/data/npcsh/npc_team/mem_search.jinx +0 -388
  95. npcsh-1.1.20.data/data/npcsh/npc_team/memories.jinx +0 -317
  96. npcsh-1.1.20.data/data/npcsh/npc_team/paper_search.jinx +0 -412
  97. npcsh-1.1.20.data/data/npcsh/npc_team/plonk.jinx +0 -379
  98. npcsh-1.1.20.data/data/npcsh/npc_team/plonkjr.npc +0 -23
  99. npcsh-1.1.20.data/data/npcsh/npc_team/search.jinx +0 -54
  100. npcsh-1.1.20.data/data/npcsh/npc_team/semantic_scholar.jinx +0 -386
  101. npcsh-1.1.20.data/data/npcsh/npc_team/vixynt.jinx +0 -122
  102. npcsh-1.1.20.data/data/npcsh/npc_team/wander.jinx +0 -455
  103. npcsh-1.1.20.data/data/npcsh/npc_team/yap.jinx +0 -268
  104. npcsh-1.1.20.dist-info/RECORD +0 -248
  105. npcsh-1.1.20.dist-info/entry_points.txt +0 -25
  106. /npcsh/npc_team/jinxs/lib/{orchestration → core}/convene.jinx +0 -0
  107. /npcsh/npc_team/jinxs/lib/{orchestration → core}/delegate.jinx +0 -0
  108. /npcsh/npc_team/jinxs/{bin → lib/core}/sample.jinx +0 -0
  109. /npcsh/npc_team/jinxs/lib/{core → utils}/chat.jinx +0 -0
  110. /npcsh/npc_team/jinxs/lib/{core → utils}/cmd.jinx +0 -0
  111. /npcsh/npc_team/jinxs/{bin → lib/utils}/sync.jinx +0 -0
  112. /npcsh/npc_team/jinxs/{bin → modes}/roll.jinx +0 -0
  113. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/add_tab.jinx +0 -0
  114. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/alicanto.png +0 -0
  115. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
  116. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
  117. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/chat.jinx +0 -0
  118. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/click.jinx +0 -0
  119. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  120. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/close_pane.jinx +0 -0
  121. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/close_tab.jinx +0 -0
  122. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  123. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/compile.jinx +0 -0
  124. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/confirm.jinx +0 -0
  125. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/convene.jinx +0 -0
  126. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/corca.png +0 -0
  127. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/corca_example.png +0 -0
  128. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/delegate.jinx +0 -0
  129. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/focus_pane.jinx +0 -0
  130. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/frederic4.png +0 -0
  131. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/guac.jinx +0 -0
  132. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/guac.npc +0 -0
  133. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/guac.png +0 -0
  134. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/help.jinx +0 -0
  135. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/incognide.jinx +0 -0
  136. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/init.jinx +0 -0
  137. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  138. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/key_press.jinx +0 -0
  139. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
  140. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/list_panes.jinx +0 -0
  141. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  142. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/navigate.jinx +0 -0
  143. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/notify.jinx +0 -0
  144. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  145. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  146. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
  147. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/open_pane.jinx +0 -0
  148. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/ots.jinx +0 -0
  149. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/paste.jinx +0 -0
  150. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/plonk.npc +0 -0
  151. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/plonk.png +0 -0
  152. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  153. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/pti.jinx +0 -0
  154. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/python.jinx +0 -0
  155. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/read_pane.jinx +0 -0
  156. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/roll.jinx +0 -0
  157. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/run_terminal.jinx +0 -0
  158. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sample.jinx +0 -0
  159. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
  160. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/send_message.jinx +0 -0
  161. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/serve.jinx +0 -0
  162. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/set.jinx +0 -0
  163. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sh.jinx +0 -0
  164. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/shh.jinx +0 -0
  165. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sibiji.png +0 -0
  166. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sleep.jinx +0 -0
  167. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/split_pane.jinx +0 -0
  168. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/spool.png +0 -0
  169. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sql.jinx +0 -0
  170. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/switch.jinx +0 -0
  171. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/switch_npc.jinx +0 -0
  172. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/switch_tab.jinx +0 -0
  173. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/switches.jinx +0 -0
  174. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/sync.jinx +0 -0
  175. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
  176. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  177. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/type_text.jinx +0 -0
  178. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/usage.jinx +0 -0
  179. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  180. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/wait.jinx +0 -0
  181. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/write_file.jinx +0 -0
  182. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/yap.png +0 -0
  183. {npcsh-1.1.20.data → npcsh-1.1.22.data}/data/npcsh/npc_team/zen_mode.jinx +0 -0
  184. {npcsh-1.1.20.dist-info → npcsh-1.1.22.dist-info}/WHEEL +0 -0
  185. {npcsh-1.1.20.dist-info → npcsh-1.1.22.dist-info}/licenses/LICENSE +0 -0
  186. {npcsh-1.1.20.dist-info → npcsh-1.1.22.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,597 @@
1
+ jinx_name: convene
2
+ description: Multi-NPC structured discussion with live TUI showing trains of thought
3
+ interactive: true
4
+ inputs:
5
+ - topic: ""
6
+ - npcs: "alicanto,corca,guac"
7
+ - rounds: 3
8
+ - model: null
9
+ - provider: null
10
+ steps:
11
+ - name: convene_tui
12
+ engine: python
13
+ code: |
14
+ import os
15
+ import sys
16
+ import tty
17
+ import termios
18
+ import select as _sel
19
+ import random
20
+ import threading
21
+ import time
22
+ import textwrap
23
+
24
+ from npcpy.llm_funcs import get_llm_response
25
+
26
+ npc = context.get('npc')
27
+ team = context.get('team')
28
+ messages = context.get('messages', [])
29
+
30
+ model = context.get('model') or (npc.model if npc and hasattr(npc, 'model') else None)
31
+ provider = context.get('provider') or (npc.provider if npc and hasattr(npc, 'provider') else None)
32
+
33
+ topic = context.get('topic', '')
34
+ npcs_str = context.get('npcs', 'alicanto,corca,guac')
35
+ num_rounds = int(context.get('rounds', 3))
36
+
37
+ if not topic:
38
+ if sys.stdin.isatty():
39
+ print('\033[1;36m CONVENE - Multi-NPC Discussion \033[0m')
40
+ print('\033[90mEnter discussion topic (or q to quit):\033[0m')
41
+ try:
42
+ topic = input('\033[33m> \033[0m').strip()
43
+ except (EOFError, KeyboardInterrupt):
44
+ topic = ''
45
+ if not topic or topic.lower() == 'q':
46
+ context['output'] = 'Convene cancelled.'
47
+ context['messages'] = messages
48
+ exit()
49
+ else:
50
+ context['output'] = 'Usage: /convene "topic" npcs=name1,name2 rounds=3'
51
+ context['messages'] = messages
52
+ exit()
53
+
54
+ _interactive = sys.stdin.isatty()
55
+
56
+ if not _interactive:
57
+ # ========== Text-based (non-interactive) mode ==========
58
+ try:
59
+ from termcolor import colored
60
+ except ImportError:
61
+ def colored(t, *a, **kw): return t
62
+
63
+ npc_names = [n.strip() for n in npcs_str.split(',') if n.strip()]
64
+ participants = []
65
+ for name in npc_names:
66
+ if team and hasattr(team, 'npcs') and name in team.npcs:
67
+ target_npc = team.npcs[name]
68
+ persona = getattr(target_npc, 'primary_directive', f'{name} specialist')
69
+ participants.append({'name': name, 'persona': persona, 'npc': target_npc})
70
+ else:
71
+ participants.append({'name': name, 'persona': f'{name} - general assistant', 'npc': None})
72
+
73
+ print(f"\n Convening Discussion\n Topic: {topic}\n Participants: {', '.join(npc_names)}\n Rounds: {num_rounds}\n")
74
+
75
+ discussion_log = []
76
+ for round_num in range(1, num_rounds + 1):
77
+ print(colored(f"\n{'='*60}", "cyan"))
78
+ print(colored(f" ROUND {round_num}/{num_rounds}", "cyan", attrs=["bold"]))
79
+ print(colored(f"{'='*60}", "cyan"))
80
+
81
+ round_contributions = []
82
+ for participant in participants:
83
+ name = participant['name']
84
+ persona = participant['persona']
85
+ prev_context = ""
86
+ if discussion_log:
87
+ prev_context = "\n\nPrevious discussion:\n"
88
+ for entry in discussion_log[-len(participants)*2:]:
89
+ prev_context += f"[{entry['speaker']}]: {entry['contribution'][:200]}...\n"
90
+ if round_contributions:
91
+ prev_context += "\nThis round so far:\n"
92
+ for entry in round_contributions:
93
+ prev_context += f"[{entry['speaker']}]: {entry['contribution'][:200]}...\n"
94
+
95
+ prompt = f"""You are {name}. {persona}
96
+
97
+ Topic under discussion: "{topic}"
98
+ {prev_context}
99
+
100
+ Provide your perspective. Be concise but insightful. Build on what others have said. If you disagree, explain why constructively."""
101
+
102
+ print(colored(f"\n[{name}]:", "yellow", attrs=["bold"]))
103
+ resp = get_llm_response(prompt, model=model, provider=provider,
104
+ npc=participant.get('npc') or npc, temperature=0.7)
105
+ contribution = str(resp.get('response', ''))
106
+ print(contribution)
107
+ entry = {'round': round_num, 'speaker': name, 'contribution': contribution}
108
+ round_contributions.append(entry)
109
+ discussion_log.append(entry)
110
+
111
+ other_participants = [p for p in participants if p['name'] != name]
112
+ if other_participants:
113
+ fp = random.choice(other_participants)
114
+ fn = fp['name']
115
+ fp_prompt = f"""You are {fn}. {fp['persona']}
116
+
117
+ Topic: "{topic}"
118
+ {name} just said: "{contribution[:500]}"
119
+
120
+ Respond briefly - agree, disagree, build on it, or ask a clarifying question. Keep it to 2-3 sentences."""
121
+
122
+ print(colored(f"\n [{fn} responds]:", "cyan"))
123
+ fresp = get_llm_response(fp_prompt, model=model, provider=provider,
124
+ npc=fp.get('npc') or npc, temperature=0.7)
125
+ fcontrib = str(fresp.get('response', ''))
126
+ print(f" {fcontrib}")
127
+ discussion_log.append({'round': round_num, 'speaker': fn, 'contribution': fcontrib, 'type': 'followup'})
128
+
129
+ if random.random() < 0.6:
130
+ rp = random.choice(other_participants) if random.random() >= 0.4 else participant
131
+ rn = rp['name']
132
+ cp = f"""You are {rn}. {rp['persona']}
133
+
134
+ Topic: "{topic}"
135
+ {fn} responded: "{fcontrib}"
136
+
137
+ Brief reaction (1-2 sentences). Move the discussion forward."""
138
+
139
+ print(colored(f"\n [{rn}]:", "magenta"))
140
+ cresp = get_llm_response(cp, model=model, provider=provider,
141
+ npc=rp.get('npc') or npc, temperature=0.7)
142
+ ccontrib = str(cresp.get('response', ''))
143
+ print(f" {ccontrib}")
144
+ discussion_log.append({'round': round_num, 'speaker': rn, 'contribution': ccontrib, 'type': 'counter'})
145
+
146
+ print(colored(f"\n{'='*60}", "green"))
147
+ print(colored(" SYNTHESIS", "green", attrs=["bold"]))
148
+ print(colored(f"{'='*60}", "green"))
149
+
150
+ all_contribs = "\n".join([f"[{e['speaker']} - Round {e['round']}]: {e['contribution']}" for e in discussion_log])
151
+ synth_prompt = f"""As the convener of this discussion on "{topic}", synthesize the key points:
152
+
153
+ Full discussion:
154
+ {all_contribs}
155
+
156
+ Provide:
157
+ 1. Key agreements and consensus points
158
+ 2. Areas of disagreement or tension
159
+ 3. Novel ideas that emerged
160
+ 4. Recommended next steps or actions"""
161
+
162
+ sresp = get_llm_response(synth_prompt, model=model, provider=provider, npc=npc, temperature=0.4)
163
+ synthesis = str(sresp.get('response', ''))
164
+ print(synthesis)
165
+
166
+ context['output'] = synthesis
167
+ context['messages'] = messages
168
+ context['convene_result'] = {
169
+ 'topic': topic,
170
+ 'participants': [n.strip() for n in npcs_str.split(',')],
171
+ 'rounds': num_rounds,
172
+ 'discussion': discussion_log,
173
+ 'synthesis': synthesis,
174
+ }
175
+ exit()
176
+
177
+ # ========== Interactive TUI mode ==========
178
+
179
+ # ========== Helpers ==========
180
+ def get_size():
181
+ try:
182
+ s = os.get_terminal_size()
183
+ return s.columns, s.lines
184
+ except:
185
+ return 80, 24
186
+
187
+ def wrap(text, w):
188
+ lines = []
189
+ for line in str(text).split('\n'):
190
+ if len(line) <= w:
191
+ lines.append(line)
192
+ else:
193
+ lines.extend(textwrap.wrap(line, w) or [''])
194
+ return lines
195
+
196
+ # ========== Load Participants ==========
197
+ npc_names = [n.strip() for n in npcs_str.split(',') if n.strip()]
198
+ participants = []
199
+ for name in npc_names:
200
+ if team and hasattr(team, 'npcs') and name in team.npcs:
201
+ target_npc = team.npcs[name]
202
+ persona = getattr(target_npc, 'primary_directive', f'{name} specialist')
203
+ participants.append({'name': name, 'persona': persona, 'npc': target_npc})
204
+ else:
205
+ participants.append({'name': name, 'persona': f'{name} - general assistant', 'npc': None})
206
+
207
+ # ========== Speaker Colors ==========
208
+ COLORS = ['\033[36m', '\033[33m', '\033[35m', '\033[32m', '\033[34m', '\033[91m', '\033[96m', '\033[93m']
209
+ def speaker_color(idx):
210
+ return COLORS[idx % len(COLORS)]
211
+
212
+ # ========== State ==========
213
+ class ConveneState:
214
+ def __init__(self):
215
+ self.phase = 0 # 0=overview, 1=running, 2=synthesis, 3=review
216
+ self.round = 0
217
+ self.total_rounds = num_rounds
218
+ self.current_speaker = -1
219
+ self.speaker_status = {} # name -> 'idle'|'speaking'|'done'
220
+ self.discussion_log = [] # [{round, speaker, contribution, type}]
221
+ self.display_lines = [] # formatted lines for the discussion panel
222
+ self.scroll = 0
223
+ self.synthesis = ""
224
+ self.generating = False
225
+ self.paused = False
226
+ self.skip_round = False
227
+ self.status = ""
228
+ self.done = False
229
+ # review state
230
+ self.review_sel = 0
231
+ self.review_scroll = 0
232
+
233
+ ui = ConveneState()
234
+ for p in participants:
235
+ ui.speaker_status[p['name']] = 'idle'
236
+
237
+ # ========== Discussion Logic ==========
238
+ def run_discussion():
239
+ ui.generating = True
240
+
241
+ for round_num in range(1, num_rounds + 1):
242
+ if ui.done:
243
+ break
244
+ ui.round = round_num
245
+ ui.display_lines.append(f'\033[90m{"="*50}\033[0m')
246
+ ui.display_lines.append(f'\033[1;37m ROUND {round_num}/{num_rounds}\033[0m')
247
+ ui.display_lines.append(f'\033[90m{"="*50}\033[0m')
248
+
249
+ round_contributions = []
250
+
251
+ for pi, participant in enumerate(participants):
252
+ if ui.done:
253
+ break
254
+ while ui.paused:
255
+ time.sleep(0.2)
256
+ if ui.done:
257
+ break
258
+
259
+ name = participant['name']
260
+ persona = participant['persona']
261
+ ui.current_speaker = pi
262
+ ui.speaker_status[name] = 'speaking'
263
+
264
+ # Build context
265
+ prev_context = ""
266
+ if ui.discussion_log:
267
+ prev_context = "\n\nPrevious discussion:\n"
268
+ for entry in ui.discussion_log[-len(participants)*2:]:
269
+ prev_context += f"[{entry['speaker']}]: {entry['contribution'][:200]}...\n"
270
+ if round_contributions:
271
+ prev_context += "\nThis round so far:\n"
272
+ for entry in round_contributions:
273
+ prev_context += f"[{entry['speaker']}]: {entry['contribution'][:200]}...\n"
274
+
275
+ prompt = f"""You are {name}. {persona}
276
+
277
+ Topic under discussion: "{topic}"
278
+ {prev_context}
279
+
280
+ Provide your perspective. Be concise but insightful. Build on what others have said. If you disagree, explain why constructively."""
281
+
282
+ color = speaker_color(pi)
283
+ ui.display_lines.append(f'')
284
+ ui.display_lines.append(f'{color}\033[1m[{name}]:\033[0m')
285
+
286
+ try:
287
+ resp = get_llm_response(prompt, model=model, provider=provider,
288
+ npc=participant.get('npc') or npc, temperature=0.7)
289
+ contribution = str(resp.get('response', ''))
290
+ except Exception as e:
291
+ contribution = f'(Error: {e})'
292
+
293
+ # Add contribution lines
294
+ for line in wrap(contribution, 70):
295
+ ui.display_lines.append(f' {line}')
296
+
297
+ entry = {'round': round_num, 'speaker': name, 'contribution': contribution, 'type': 'main'}
298
+ round_contributions.append(entry)
299
+ ui.discussion_log.append(entry)
300
+ ui.speaker_status[name] = 'done'
301
+
302
+ # Followup from another participant
303
+ others = [p for p in participants if p['name'] != name]
304
+ if others and not ui.done:
305
+ followup_p = random.choice(others)
306
+ fn = followup_p['name']
307
+ fi = npc_names.index(fn) if fn in npc_names else 0
308
+ ui.speaker_status[fn] = 'speaking'
309
+
310
+ followup_prompt = f"""You are {fn}. {followup_p['persona']}
311
+
312
+ Topic: "{topic}"
313
+
314
+ {name} just said: "{contribution[:500]}"
315
+
316
+ Respond briefly - agree, disagree, build on it, or ask a clarifying question. Keep it to 2-3 sentences."""
317
+
318
+ fcolor = speaker_color(fi)
319
+ ui.display_lines.append(f'')
320
+ ui.display_lines.append(f' {fcolor}[{fn} responds]:\033[0m')
321
+
322
+ try:
323
+ fresp = get_llm_response(followup_prompt, model=model, provider=provider,
324
+ npc=followup_p.get('npc') or npc, temperature=0.7)
325
+ fcontrib = str(fresp.get('response', ''))
326
+ except Exception as e:
327
+ fcontrib = f'(Error: {e})'
328
+
329
+ for line in wrap(fcontrib, 66):
330
+ ui.display_lines.append(f' {line}')
331
+
332
+ ui.discussion_log.append({'round': round_num, 'speaker': fn,
333
+ 'contribution': fcontrib, 'type': 'followup'})
334
+ ui.speaker_status[fn] = 'done'
335
+
336
+ # Counter-response (60% chance)
337
+ if random.random() < 0.6 and not ui.done:
338
+ if random.random() < 0.4:
339
+ resp_p = participant
340
+ else:
341
+ resp_p = random.choice(others)
342
+ rn = resp_p['name']
343
+ ri = npc_names.index(rn) if rn in npc_names else 0
344
+ ui.speaker_status[rn] = 'speaking'
345
+
346
+ counter_prompt = f"""You are {rn}. {resp_p['persona']}
347
+
348
+ Topic: "{topic}"
349
+
350
+ {fn} responded: "{fcontrib}"
351
+
352
+ Brief reaction (1-2 sentences). Move the discussion forward."""
353
+
354
+ rcolor = speaker_color(ri)
355
+ ui.display_lines.append(f'')
356
+ ui.display_lines.append(f' {rcolor}[{rn}]:\033[0m')
357
+
358
+ try:
359
+ cresp = get_llm_response(counter_prompt, model=model, provider=provider,
360
+ npc=resp_p.get('npc') or npc, temperature=0.7)
361
+ ccontrib = str(cresp.get('response', ''))
362
+ except Exception as e:
363
+ ccontrib = f'(Error: {e})'
364
+
365
+ for line in wrap(ccontrib, 66):
366
+ ui.display_lines.append(f' {line}')
367
+
368
+ ui.discussion_log.append({'round': round_num, 'speaker': rn,
369
+ 'contribution': ccontrib, 'type': 'counter'})
370
+ ui.speaker_status[rn] = 'done'
371
+
372
+ # Reset statuses for next round
373
+ for p in participants:
374
+ ui.speaker_status[p['name']] = 'idle'
375
+
376
+ # Synthesis
377
+ if not ui.done:
378
+ ui.display_lines.append(f'')
379
+ ui.display_lines.append(f'\033[1;32m{"="*50}\033[0m')
380
+ ui.display_lines.append(f'\033[1;32m SYNTHESIS\033[0m')
381
+ ui.display_lines.append(f'\033[1;32m{"="*50}\033[0m')
382
+
383
+ all_contribs = "\n".join([f"[{e['speaker']} - Round {e['round']}]: {e['contribution']}"
384
+ for e in ui.discussion_log])
385
+
386
+ synth_prompt = f"""As the convener of this discussion on "{topic}", synthesize the key points:
387
+
388
+ Full discussion:
389
+ {all_contribs[:6000]}
390
+
391
+ Provide:
392
+ 1. Key agreements and consensus points
393
+ 2. Areas of disagreement or tension
394
+ 3. Novel ideas that emerged
395
+ 4. Recommended next steps or actions"""
396
+
397
+ try:
398
+ sresp = get_llm_response(synth_prompt, model=model, provider=provider, npc=npc, temperature=0.4)
399
+ ui.synthesis = str(sresp.get('response', ''))
400
+ except Exception as e:
401
+ ui.synthesis = f'Synthesis error: {e}'
402
+
403
+ for line in wrap(ui.synthesis, 70):
404
+ ui.display_lines.append(f' {line}')
405
+
406
+ ui.generating = False
407
+ ui.phase = 3
408
+ ui.status = "Discussion complete. Review results."
409
+
410
+ # ========== Rendering ==========
411
+ def render():
412
+ width, height = get_size()
413
+ out = []
414
+ out.append('\033[2J\033[H')
415
+
416
+ # Header
417
+ phase_label = ['Overview', 'Discussion', 'Synthesizing', 'Review'][min(ui.phase, 3)]
418
+ header = f' CONVENE - {phase_label} '
419
+ out.append(f'\033[1;1H\033[7;1m{header.ljust(width)}\033[0m')
420
+
421
+ if ui.phase == 0:
422
+ render_overview(out, width, height)
423
+ else:
424
+ render_discussion(out, width, height)
425
+
426
+ # Status bar
427
+ pause_text = ' \033[31m[PAUSED]\033[0m' if ui.paused else ''
428
+ gen_text = ' \033[33m(generating...)\033[0m' if ui.generating else ''
429
+ out.append(f'\033[{height-1};1H\033[K\033[90m Round {ui.round}/{ui.total_rounds} | {len(ui.discussion_log)} contributions{gen_text}{pause_text}\033[0m')
430
+
431
+ if ui.phase == 0:
432
+ out.append(f'\033[{height};1H\033[K\033[7m Enter:Start q:Quit \033[0m'.ljust(width))
433
+ elif ui.phase < 3:
434
+ out.append(f'\033[{height};1H\033[K\033[7m p:Pause s:Skip round j/k:Scroll q:Quit \033[0m'.ljust(width))
435
+ else:
436
+ out.append(f'\033[{height};1H\033[K\033[7m j/k:Scroll q:Quit \033[0m'.ljust(width))
437
+
438
+ sys.stdout.write(''.join(out))
439
+ sys.stdout.flush()
440
+
441
+ def render_overview(out, width, height):
442
+ banner = [
443
+ '\033[36m ██████╗ ██████╗ ███╗ ██╗██╗ ██╗███████╗███╗ ██╗███████╗\033[0m',
444
+ '\033[36m██╔════╝██╔═══██╗████╗ ██║██║ ██║██╔════╝████╗ ██║██╔════╝\033[0m',
445
+ '\033[36m██║ ██║ ██║██╔██╗ ██║██║ ██║█████╗ ██╔██╗ ██║█████╗ \033[0m',
446
+ '\033[36m██║ ██║ ██║██║╚██╗██║╚██╗ ██╔╝██╔══╝ ██║╚██╗██║██╔══╝ \033[0m',
447
+ '\033[36m╚██████╗╚██████╔╝██║ ╚████║ ╚████╔╝ ███████╗██║ ╚████║███████╗\033[0m',
448
+ '\033[36m ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═══╝ ╚══════╝╚═╝ ╚═══╝╚══════╝\033[0m',
449
+ ]
450
+ for i, line in enumerate(banner):
451
+ out.append(f'\033[{3+i};2H{line}')
452
+
453
+ y = 3 + len(banner) + 1
454
+ out.append(f'\033[{y};3H\033[1mTopic:\033[0m {topic}')
455
+ y += 1
456
+ out.append(f'\033[{y};3H\033[1mRounds:\033[0m {num_rounds}')
457
+ y += 2
458
+ out.append(f'\033[{y};3H\033[1mParticipants:\033[0m')
459
+ y += 1
460
+ for i, p in enumerate(participants):
461
+ color = speaker_color(i)
462
+ pname = p['name']
463
+ pdesc = p['persona'][:60] if len(p['persona']) > 60 else p['persona']
464
+ pdesc = pdesc.split('\n')[0]
465
+ out.append(f'\033[{y};5H{color}{pname}\033[0m \033[90m- {pdesc}\033[0m')
466
+ y += 1
467
+
468
+ def render_discussion(out, width, height):
469
+ left_w = max(22, width // 5)
470
+ right_w = width - left_w - 1
471
+
472
+ # Left panel: participants
473
+ out.append(f'\033[3;1H\033[36;1m Participants \033[90m{"_" * (left_w - 15)}\033[0m')
474
+ y = 4
475
+ for i, p in enumerate(participants):
476
+ name = p['name']
477
+ status = ui.speaker_status.get(name, 'idle')
478
+ color = speaker_color(i)
479
+ if status == 'speaking':
480
+ icon = '\033[33m>\033[0m'
481
+ elif status == 'done':
482
+ icon = '\033[32m+\033[0m'
483
+ else:
484
+ icon = '\033[90m-\033[0m'
485
+ out.append(f'\033[{y};2H{icon} {color}{name[:left_w-4]}\033[0m')
486
+ y += 1
487
+
488
+ y += 1
489
+ out.append(f'\033[{y};2H\033[90mRound {ui.round}/{ui.total_rounds}\033[0m')
490
+ y += 1
491
+ contribs = len(ui.discussion_log)
492
+ out.append(f'\033[{y};2H\033[90m{contribs} contributions\033[0m')
493
+
494
+ # Right panel: discussion log
495
+ out.append(f'\033[3;{left_w+1}H\033[33;1m Discussion \033[90m{"_" * (right_w - 13)}\033[0m')
496
+
497
+ panel_h = height - 6
498
+ total = len(ui.display_lines)
499
+ # Auto-scroll to bottom during generation
500
+ if ui.generating:
501
+ ui.scroll = max(0, total - panel_h)
502
+ vis_start = max(0, ui.scroll)
503
+
504
+ for i in range(panel_h):
505
+ idx = vis_start + i
506
+ row = 4 + i
507
+ out.append(f'\033[{row};{left_w+1}H\033[K')
508
+ if 0 <= idx < total:
509
+ line = ui.display_lines[idx]
510
+ # Truncate visible portion
511
+ out.append(f'\033[{row};{left_w+2}H{line[:right_w-2]}')
512
+
513
+ # ========== Input ==========
514
+ def handle_input(c, fd):
515
+ if c == '\x1b':
516
+ if _sel.select([fd], [], [], 0.05)[0]:
517
+ c2 = os.read(fd, 1).decode('latin-1')
518
+ if c2 == '[':
519
+ c3 = os.read(fd, 1).decode('latin-1')
520
+ if c3 == 'A': scroll_up()
521
+ elif c3 == 'B': scroll_down()
522
+ return True
523
+
524
+ if c == 'q' or c == '\x03':
525
+ ui.done = True
526
+ return False
527
+
528
+ if ui.phase == 0:
529
+ if c in ('\r', '\n'):
530
+ ui.phase = 1
531
+ threading.Thread(target=run_discussion, daemon=True).start()
532
+ elif ui.phase < 3:
533
+ if c == 'p':
534
+ ui.paused = not ui.paused
535
+ elif c == 's':
536
+ ui.skip_round = True
537
+ elif c == 'j': scroll_down()
538
+ elif c == 'k': scroll_up()
539
+ else:
540
+ if c == 'j': scroll_down()
541
+ elif c == 'k': scroll_up()
542
+
543
+ return True
544
+
545
+ def scroll_up():
546
+ ui.scroll = max(0, ui.scroll - 1)
547
+
548
+ def scroll_down():
549
+ total = len(ui.display_lines)
550
+ _, height = get_size()
551
+ panel_h = height - 6
552
+ ui.scroll = min(max(0, total - panel_h), ui.scroll + 1)
553
+
554
+ # ========== Main Loop ==========
555
+ fd = sys.stdin.fileno()
556
+ old_settings = termios.tcgetattr(fd)
557
+
558
+ try:
559
+ tty.setcbreak(fd)
560
+ sys.stdout.write('\033[?25l')
561
+ render()
562
+
563
+ running = True
564
+ while running:
565
+ if ui.generating:
566
+ if _sel.select([fd], [], [], 0.3)[0]:
567
+ c = os.read(fd, 1).decode('latin-1')
568
+ running = handle_input(c, fd)
569
+ else:
570
+ if _sel.select([fd], [], [], 0.5)[0]:
571
+ c = os.read(fd, 1).decode('latin-1')
572
+ running = handle_input(c, fd)
573
+ render()
574
+ finally:
575
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
576
+ sys.stdout.write('\033[?25h\033[2J\033[H')
577
+ sys.stdout.flush()
578
+
579
+ # Print summary
580
+ if ui.discussion_log:
581
+ print(f'\033[1;36m=== CONVENE: {topic} ===\033[0m\n')
582
+ print(f'Participants: {", ".join(npc_names)}')
583
+ print(f'Rounds: {ui.round}/{ui.total_rounds}')
584
+ print(f'Contributions: {len(ui.discussion_log)}')
585
+ if ui.synthesis:
586
+ print(f'\n\033[1;32m--- Synthesis ---\033[0m')
587
+ print(ui.synthesis[:2000])
588
+
589
+ context['output'] = ui.synthesis or 'Convene session ended.'
590
+ context['messages'] = messages
591
+ context['convene_result'] = {
592
+ 'topic': topic,
593
+ 'participants': npc_names,
594
+ 'rounds': ui.round,
595
+ 'discussion': ui.discussion_log,
596
+ 'synthesis': ui.synthesis,
597
+ }