npcsh 1.1.19__py3-none-any.whl → 1.1.21__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 (173) hide show
  1. npcsh/_state.py +16 -78
  2. npcsh/diff_viewer.py +3 -3
  3. npcsh/npc_team/jinxs/lib/core/compress.jinx +373 -85
  4. npcsh/npc_team/jinxs/lib/core/search/db_search.jinx +18 -7
  5. npcsh/npc_team/jinxs/lib/core/search/file_search.jinx +18 -7
  6. npcsh/npc_team/jinxs/lib/core/search/kg_search.jinx +20 -9
  7. npcsh/npc_team/jinxs/lib/core/search/web_search.jinx +53 -15
  8. npcsh/npc_team/jinxs/{bin → lib/utils}/benchmark.jinx +2 -2
  9. npcsh/npc_team/jinxs/lib/utils/jinxs.jinx +393 -317
  10. npcsh/npc_team/jinxs/lib/utils/models.jinx +343 -0
  11. npcsh/npc_team/jinxs/{bin → lib/utils}/setup.jinx +8 -7
  12. npcsh/npc_team/jinxs/modes/alicanto.jinx +1573 -296
  13. npcsh/npc_team/jinxs/modes/arxiv.jinx +6 -6
  14. npcsh/npc_team/jinxs/modes/config_tui.jinx +300 -0
  15. npcsh/npc_team/jinxs/modes/corca.jinx +4 -4
  16. npcsh/npc_team/jinxs/modes/git.jinx +795 -0
  17. npcsh/npc_team/jinxs/modes/guac.jinx +4 -4
  18. npcsh/npc_team/jinxs/modes/kg.jinx +941 -0
  19. npcsh/npc_team/jinxs/modes/memories.jinx +414 -0
  20. npcsh/npc_team/jinxs/modes/nql.jinx +460 -0
  21. npcsh/npc_team/jinxs/modes/papers.jinx +578 -0
  22. npcsh/npc_team/jinxs/modes/plonk.jinx +490 -304
  23. npcsh/npc_team/jinxs/modes/pti.jinx +1 -1
  24. npcsh/npc_team/jinxs/modes/reattach.jinx +4 -4
  25. npcsh/npc_team/jinxs/modes/spool.jinx +4 -4
  26. npcsh/npc_team/jinxs/modes/team.jinx +504 -0
  27. npcsh/npc_team/jinxs/modes/vixynt.jinx +388 -0
  28. npcsh/npc_team/jinxs/modes/wander.jinx +455 -182
  29. npcsh/npc_team/jinxs/modes/yap.jinx +10 -3
  30. npcsh/npcsh.py +112 -47
  31. npcsh/routes.py +12 -3
  32. npcsh/salmon_simulation.py +0 -0
  33. npcsh-1.1.21.data/data/npcsh/npc_team/alicanto.jinx +1633 -0
  34. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/arxiv.jinx +6 -6
  35. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/benchmark.jinx +2 -2
  36. npcsh-1.1.21.data/data/npcsh/npc_team/compress.jinx +428 -0
  37. npcsh-1.1.21.data/data/npcsh/npc_team/config_tui.jinx +300 -0
  38. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/corca.jinx +4 -4
  39. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/db_search.jinx +18 -7
  40. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/file_search.jinx +18 -7
  41. npcsh-1.1.21.data/data/npcsh/npc_team/git.jinx +795 -0
  42. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/guac.jinx +4 -4
  43. npcsh-1.1.21.data/data/npcsh/npc_team/jinxs.jinx +407 -0
  44. npcsh-1.1.21.data/data/npcsh/npc_team/kg.jinx +941 -0
  45. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/kg_search.jinx +20 -9
  46. npcsh-1.1.21.data/data/npcsh/npc_team/memories.jinx +414 -0
  47. npcsh-1.1.21.data/data/npcsh/npc_team/models.jinx +343 -0
  48. npcsh-1.1.21.data/data/npcsh/npc_team/nql.jinx +460 -0
  49. npcsh-1.1.21.data/data/npcsh/npc_team/papers.jinx +578 -0
  50. npcsh-1.1.21.data/data/npcsh/npc_team/plonk.jinx +565 -0
  51. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/pti.jinx +1 -1
  52. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/reattach.jinx +4 -4
  53. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/setup.jinx +8 -7
  54. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/spool.jinx +4 -4
  55. npcsh-1.1.21.data/data/npcsh/npc_team/team.jinx +504 -0
  56. npcsh-1.1.21.data/data/npcsh/npc_team/vixynt.jinx +388 -0
  57. npcsh-1.1.21.data/data/npcsh/npc_team/wander.jinx +728 -0
  58. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/web_search.jinx +53 -15
  59. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/yap.jinx +10 -3
  60. {npcsh-1.1.19.dist-info → npcsh-1.1.21.dist-info}/METADATA +2 -2
  61. {npcsh-1.1.19.dist-info → npcsh-1.1.21.dist-info}/RECORD +147 -148
  62. npcsh-1.1.21.dist-info/entry_points.txt +11 -0
  63. npcsh/npc_team/jinxs/bin/config_tui.jinx +0 -299
  64. npcsh/npc_team/jinxs/bin/memories.jinx +0 -316
  65. npcsh/npc_team/jinxs/bin/nql.jinx +0 -141
  66. npcsh/npc_team/jinxs/bin/team_tui.jinx +0 -327
  67. npcsh/npc_team/jinxs/bin/vixynt.jinx +0 -122
  68. npcsh/npc_team/jinxs/lib/core/search/mem_review.jinx +0 -73
  69. npcsh/npc_team/jinxs/lib/core/search/mem_search.jinx +0 -388
  70. npcsh/npc_team/jinxs/lib/research/paper_search.jinx +0 -412
  71. npcsh/npc_team/jinxs/lib/research/semantic_scholar.jinx +0 -386
  72. npcsh/npc_team/plonkjr.npc +0 -23
  73. npcsh-1.1.19.data/data/npcsh/npc_team/alicanto.jinx +0 -356
  74. npcsh-1.1.19.data/data/npcsh/npc_team/compress.jinx +0 -140
  75. npcsh-1.1.19.data/data/npcsh/npc_team/config_tui.jinx +0 -299
  76. npcsh-1.1.19.data/data/npcsh/npc_team/jinxs.jinx +0 -331
  77. npcsh-1.1.19.data/data/npcsh/npc_team/mem_review.jinx +0 -73
  78. npcsh-1.1.19.data/data/npcsh/npc_team/mem_search.jinx +0 -388
  79. npcsh-1.1.19.data/data/npcsh/npc_team/memories.jinx +0 -316
  80. npcsh-1.1.19.data/data/npcsh/npc_team/nql.jinx +0 -141
  81. npcsh-1.1.19.data/data/npcsh/npc_team/paper_search.jinx +0 -412
  82. npcsh-1.1.19.data/data/npcsh/npc_team/plonk.jinx +0 -379
  83. npcsh-1.1.19.data/data/npcsh/npc_team/plonkjr.npc +0 -23
  84. npcsh-1.1.19.data/data/npcsh/npc_team/semantic_scholar.jinx +0 -386
  85. npcsh-1.1.19.data/data/npcsh/npc_team/team_tui.jinx +0 -327
  86. npcsh-1.1.19.data/data/npcsh/npc_team/vixynt.jinx +0 -122
  87. npcsh-1.1.19.data/data/npcsh/npc_team/wander.jinx +0 -455
  88. npcsh-1.1.19.dist-info/entry_points.txt +0 -22
  89. /npcsh/npc_team/jinxs/lib/{orchestration → core}/convene.jinx +0 -0
  90. /npcsh/npc_team/jinxs/lib/{orchestration → core}/delegate.jinx +0 -0
  91. /npcsh/npc_team/jinxs/{bin → lib/core}/sample.jinx +0 -0
  92. /npcsh/npc_team/jinxs/{bin → lib/utils}/sync.jinx +0 -0
  93. /npcsh/npc_team/jinxs/{bin → modes}/roll.jinx +0 -0
  94. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/add_tab.jinx +0 -0
  95. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/alicanto.npc +0 -0
  96. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/alicanto.png +0 -0
  97. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/browser_action.jinx +0 -0
  98. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/browser_screenshot.jinx +0 -0
  99. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/build.jinx +0 -0
  100. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/chat.jinx +0 -0
  101. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/click.jinx +0 -0
  102. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/close_browser.jinx +0 -0
  103. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/close_pane.jinx +0 -0
  104. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/close_tab.jinx +0 -0
  105. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/cmd.jinx +0 -0
  106. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/compile.jinx +0 -0
  107. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/confirm.jinx +0 -0
  108. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/convene.jinx +0 -0
  109. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/corca.npc +0 -0
  110. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/corca.png +0 -0
  111. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/corca_example.png +0 -0
  112. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/delegate.jinx +0 -0
  113. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/edit_file.jinx +0 -0
  114. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/focus_pane.jinx +0 -0
  115. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/frederic.npc +0 -0
  116. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/frederic4.png +0 -0
  117. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/guac.npc +0 -0
  118. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/guac.png +0 -0
  119. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/help.jinx +0 -0
  120. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/incognide.jinx +0 -0
  121. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/init.jinx +0 -0
  122. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/kadiefa.npc +0 -0
  123. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/kadiefa.png +0 -0
  124. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/key_press.jinx +0 -0
  125. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/launch_app.jinx +0 -0
  126. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/list_panes.jinx +0 -0
  127. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/load_file.jinx +0 -0
  128. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/navigate.jinx +0 -0
  129. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/notify.jinx +0 -0
  130. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/npcsh.ctx +0 -0
  131. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/npcsh_sibiji.png +0 -0
  132. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/open_browser.jinx +0 -0
  133. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/open_pane.jinx +0 -0
  134. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/ots.jinx +0 -0
  135. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/paste.jinx +0 -0
  136. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/plonk.npc +0 -0
  137. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/plonk.png +0 -0
  138. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/plonkjr.png +0 -0
  139. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/python.jinx +0 -0
  140. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/read_pane.jinx +0 -0
  141. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/roll.jinx +0 -0
  142. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/run_terminal.jinx +0 -0
  143. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sample.jinx +0 -0
  144. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/screenshot.jinx +0 -0
  145. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/search.jinx +0 -0
  146. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/send_message.jinx +0 -0
  147. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/serve.jinx +0 -0
  148. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/set.jinx +0 -0
  149. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sh.jinx +0 -0
  150. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/shh.jinx +0 -0
  151. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sibiji.npc +0 -0
  152. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sibiji.png +0 -0
  153. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sleep.jinx +0 -0
  154. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/split_pane.jinx +0 -0
  155. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/spool.png +0 -0
  156. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sql.jinx +0 -0
  157. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/switch.jinx +0 -0
  158. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/switch_npc.jinx +0 -0
  159. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/switch_tab.jinx +0 -0
  160. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/switches.jinx +0 -0
  161. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/sync.jinx +0 -0
  162. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/teamviz.jinx +0 -0
  163. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/trigger.jinx +0 -0
  164. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/type_text.jinx +0 -0
  165. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/usage.jinx +0 -0
  166. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/verbose.jinx +0 -0
  167. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/wait.jinx +0 -0
  168. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/write_file.jinx +0 -0
  169. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/yap.png +0 -0
  170. {npcsh-1.1.19.data → npcsh-1.1.21.data}/data/npcsh/npc_team/zen_mode.jinx +0 -0
  171. {npcsh-1.1.19.dist-info → npcsh-1.1.21.dist-info}/WHEEL +0 -0
  172. {npcsh-1.1.19.dist-info → npcsh-1.1.21.dist-info}/licenses/LICENSE +0 -0
  173. {npcsh-1.1.19.dist-info → npcsh-1.1.21.dist-info}/top_level.txt +0 -0
@@ -1,18 +1,15 @@
1
1
  jinx_name: alicanto
2
- description: Deep research mode - multi-perspective exploration with gold insights and cliff warnings
2
+ description: Deep research mode - multi-agent hypothesis exploration with approval-gated TUI pipeline
3
+ interactive: true
3
4
  npc: forenpc
4
5
  inputs:
5
6
  - query: null
6
- - num_npcs: 5
7
- - depth: 3
7
+ - num_npcs: 3
8
8
  - model: null
9
9
  - provider: null
10
- - max_steps: 20
11
- - skip_research: true
12
- - exploration: 0.3
13
- - creativity: 0.5
10
+ - max_steps: 10
11
+ - num_cycles: 3
14
12
  - format: report
15
- - browse: false
16
13
 
17
14
  steps:
18
15
  - name: alicanto_research
@@ -22,335 +19,1615 @@ steps:
22
19
  import sys
23
20
  import tty
24
21
  import termios
25
- from termcolor import colored
22
+ import select as _sel
23
+ import json
24
+ import random
25
+ import threading
26
+ import time
27
+ import textwrap
28
+ import csv
29
+ import subprocess
30
+ import hashlib
31
+ from datetime import datetime
32
+ from dataclasses import dataclass, asdict, field
33
+ from typing import List, Dict, Any, Tuple
34
+ from pathlib import Path
26
35
 
27
36
  from npcpy.llm_funcs import get_llm_response
28
- from npcpy.data.web import search_web
29
37
  from npcpy.npc_compiler import NPC
30
38
 
39
+ try:
40
+ from npcpy.data.web import search_web
41
+ WEB_AVAILABLE = True
42
+ except ImportError:
43
+ WEB_AVAILABLE = False
44
+
45
+ try:
46
+ from npcsh.wander import perform_single_wandering
47
+ WANDER_AVAILABLE = True
48
+ except ImportError:
49
+ WANDER_AVAILABLE = False
50
+
31
51
  npc = context.get('npc')
32
52
  team = context.get('team')
33
53
  messages = context.get('messages', [])
34
54
 
35
- # Resolve npc if it's a string (npc name) rather than NPC object
36
55
  if isinstance(npc, str) and team:
37
56
  npc = team.get(npc) if hasattr(team, 'get') else None
38
57
  elif isinstance(npc, str):
39
- npc = None # Can't use string npc without team to resolve it
58
+ npc = None
59
+
60
+ query = context.get('query')
61
+ num_npcs = int(context.get('num_npcs', 3))
62
+ max_steps = int(context.get('max_steps', 10))
63
+ num_cycles = int(context.get('num_cycles', 3))
64
+ output_format = context.get('format', 'report')
65
+
66
+ model = context.get('model') or (npc.model if npc and hasattr(npc, 'model') else None)
67
+ provider = context.get('provider') or (npc.provider if npc and hasattr(npc, 'provider') else None)
40
68
 
41
- # ========== TUI Helper Functions ==========
42
- def get_terminal_size():
69
+ # ========== Utility ==========
70
+ def get_size():
43
71
  try:
44
- size = os.get_terminal_size()
45
- return size.columns, size.lines
72
+ s = os.get_terminal_size()
73
+ return s.columns, s.lines
46
74
  except:
47
75
  return 80, 24
48
76
 
49
- def research_tui_browser(result):
50
- """Interactive TUI browser for research results"""
51
- perspectives = result.get('perspectives', '').split('\n')
52
- insights = result.get('insights', [])
53
- gold = result.get('gold', [])
54
- cliffs = result.get('cliffs', [])
55
- report = result.get('report', '')
77
+ def wrap_text(text, width):
78
+ lines = []
79
+ for line in str(text).split('\n'):
80
+ if len(line) <= width:
81
+ lines.append(line)
82
+ else:
83
+ lines.extend(textwrap.wrap(line, width) or [''])
84
+ return lines
56
85
 
57
- # Build tabs
58
- tabs = ['Gold', 'Cliffs', 'Insights', 'Report']
59
- current_tab = 0
86
+ def clamp(val, lo, hi):
87
+ return max(lo, min(val, hi))
60
88
 
61
- width, height = get_terminal_size()
62
- selected = 0
63
- scroll = 0
64
- list_height = height - 5
89
+ # ========== Data Classes (matching original) ==========
90
+ @dataclass
91
+ class ResearchStep:
92
+ step: int
93
+ thought: str
94
+ action: str
95
+ outcome: str
96
+
97
+ @dataclass
98
+ class SubAgentTrace:
99
+ hypothesis: str
100
+ agent_name: str
101
+ agent_persona: str
102
+ steps: List[ResearchStep] = field(default_factory=list)
103
+ final_files: Dict[str, str] = field(default_factory=dict)
104
+ was_successful: bool = False
65
105
 
66
- fd = sys.stdin.fileno()
67
- old_settings = termios.tcgetattr(fd)
106
+ @dataclass
107
+ class Paper:
108
+ title: str = ""
109
+ abstract: str = ""
110
+ introduction: List[str] = field(default_factory=list)
111
+ methods: List[str] = field(default_factory=list)
112
+ results: List[str] = field(default_factory=list)
113
+ discussion: List[str] = field(default_factory=list)
68
114
 
115
+ # ========== Tool Definitions (matching original) ==========
116
+ _work_dir = os.path.join(os.getcwd(), 'alicanto_output')
117
+ os.makedirs(_work_dir, exist_ok=True)
118
+ _orig_cwd = os.getcwd()
119
+ os.chdir(_work_dir)
120
+
121
+ def create_file(filename: str, content: str) -> str:
122
+ """Create a new file with the given content."""
123
+ filepath = os.path.abspath(filename)
124
+ if os.path.exists(filepath):
125
+ return f"Error: File '{filename}' already exists. Use append_to_file or replace_in_file to modify."
126
+ os.makedirs(os.path.dirname(filepath) if os.path.dirname(filepath) else '.', exist_ok=True)
127
+ with open(filepath, 'w') as f:
128
+ f.write(content)
129
+ return f"File '{filename}' created successfully."
130
+
131
+ def append_to_file(filename: str, content: str) -> str:
132
+ """Append content to an existing file."""
133
+ filepath = os.path.abspath(filename)
134
+ if not os.path.exists(filepath):
135
+ return f"Error: File '{filename}' not found. Use create_file first."
136
+ with open(filepath, 'a') as f:
137
+ f.write("\n" + content)
138
+ return f"Content appended to '{filename}'."
139
+
140
+ def replace_in_file(filename: str, old_content: str, new_content: str) -> str:
141
+ """Replace old_content with new_content in a file."""
142
+ filepath = os.path.abspath(filename)
143
+ if not os.path.exists(filepath):
144
+ return f"Error: File '{filename}' not found."
145
+ with open(filepath, 'r') as f:
146
+ file_contents = f.read()
147
+ file_contents = file_contents.replace(old_content, new_content)
148
+ with open(filepath, 'w') as f:
149
+ f.write(file_contents)
150
+ return f"Content in '{filename}' replaced."
151
+
152
+ def read_file(filename: str) -> str:
153
+ """Read and return the contents of a file."""
154
+ filepath = os.path.abspath(filename)
155
+ if not os.path.exists(filepath):
156
+ return f"Error: File '{filename}' not found."
157
+ with open(filepath, 'r') as f:
158
+ return f.read()
159
+
160
+ def list_files(directory: str = ".") -> str:
161
+ """List files in the current directory."""
69
162
  try:
70
- tty.setcbreak(fd)
71
- sys.stdout.write('\033[?25l')
72
- sys.stdout.write('\033[2J\033[H')
73
-
74
- while True:
75
- width, height = get_terminal_size()
76
- list_height = height - 5
77
-
78
- # Get current content
79
- if current_tab == 0: # Gold
80
- items = gold if gold else ['No gold insights marked']
81
- elif current_tab == 1: # Cliffs
82
- items = cliffs if cliffs else ['No cliff warnings marked']
83
- elif current_tab == 2: # Insights
84
- items = [i[:200] for i in insights] if insights else ['No insights yet']
85
- else: # Report
86
- items = report.split('\n') if report else ['No report generated']
87
-
88
- if selected >= len(items):
89
- selected = max(0, len(items) - 1)
90
-
91
- if selected < scroll:
92
- scroll = selected
93
- elif selected >= scroll + list_height:
94
- scroll = selected - list_height + 1
95
-
96
- sys.stdout.write('\033[H')
97
-
98
- # Tab bar
99
- tab_bar = " "
100
- for i, tab in enumerate(tabs):
101
- if i == current_tab:
102
- tab_bar += f'\033[43;30;1m {tab} \033[0m '
103
- else:
104
- tab_bar += f'\033[90m {tab} \033[0m '
105
- sys.stdout.write(f'{tab_bar.ljust(width)}\n')
106
- sys.stdout.write(f'\033[90m{"─" * width}\033[0m\n')
107
-
108
- # Content
109
- for i in range(list_height):
110
- idx = scroll + i
111
- sys.stdout.write(f'\033[{3+i};1H\033[K')
112
- if idx >= len(items):
113
- continue
114
-
115
- line = str(items[idx])[:width-2]
116
- if current_tab in [0, 1, 2] and idx == selected:
117
- sys.stdout.write(f'\033[47;30;1m>{line.ljust(width-2)}\033[0m')
118
- else:
119
- # Color gold/cliff markers
120
- if '[GOLD]' in line:
121
- sys.stdout.write(f'\033[33m {line}\033[0m')
122
- elif '[CLIFF]' in line:
123
- sys.stdout.write(f'\033[31m {line}\033[0m')
124
- else:
125
- sys.stdout.write(f' {line}')
126
-
127
- # Status bar
128
- sys.stdout.write(f'\033[{height-2};1H\033[K\033[90m{"─" * width}\033[0m')
129
- counts = f"Gold: {len(gold)} | Cliffs: {len(cliffs)} | Insights: {len(insights)}"
130
- sys.stdout.write(f'\033[{height-1};1H\033[K {counts}'.ljust(width)[:width])
131
- sys.stdout.write(f'\033[{height};1H\033[K\033[43;30m h/l:Tabs j/k:Nav Enter:View q:Quit [{selected+1}/{len(items)}] \033[0m')
132
-
133
- sys.stdout.flush()
134
-
135
- c = sys.stdin.read(1)
136
-
137
- if c == '\x1b':
138
- c2 = sys.stdin.read(1)
139
- if c2 == '[':
140
- c3 = sys.stdin.read(1)
141
- if c3 == 'A' and selected > 0:
142
- selected -= 1
143
- elif c3 == 'B' and selected < len(items) - 1:
144
- selected += 1
145
- elif c3 == 'C': # Right
146
- current_tab = (current_tab + 1) % len(tabs)
147
- selected = 0
148
- scroll = 0
149
- elif c3 == 'D': # Left
150
- current_tab = (current_tab - 1) % len(tabs)
151
- selected = 0
152
- scroll = 0
163
+ return "\n".join(os.listdir(directory))
164
+ except:
165
+ return "Error listing directory."
166
+
167
+ def execute_shell_command(command: str) -> str:
168
+ """Execute a shell command and return stdout/stderr."""
169
+ try:
170
+ result = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=60)
171
+ out = ""
172
+ if result.stdout:
173
+ out += result.stdout
174
+ if result.stderr:
175
+ out += result.stderr
176
+ return out[:3000] if out.strip() else "(no output)"
177
+ except subprocess.TimeoutExpired:
178
+ return "Command timed out (60s limit)"
179
+ except Exception as e:
180
+ return f"Error: {e}"
181
+
182
+ def _web_search_tool(query: str) -> str:
183
+ """Search the web for information."""
184
+ if not WEB_AVAILABLE:
185
+ return "Web search not available."
186
+ try:
187
+ results = search_web(query, num_results=5)
188
+ if not results:
189
+ return "No results found."
190
+ if isinstance(results, list):
191
+ out = []
192
+ for r in results[:5]:
193
+ if isinstance(r, dict):
194
+ out.append(f"- {r.get('title', 'N/A')}: {str(r.get('content', r.get('snippet', '')))[:300]}")
153
195
  else:
154
- return
155
- continue
156
-
157
- if c == 'q' or c == '\x03':
158
- return
159
- elif c == 'k' and selected > 0:
160
- selected -= 1
161
- elif c == 'j' and selected < len(items) - 1:
162
- selected += 1
163
- elif c == 'h':
164
- current_tab = (current_tab - 1) % len(tabs)
165
- selected = 0
166
- scroll = 0
167
- elif c == 'l':
168
- current_tab = (current_tab + 1) % len(tabs)
169
- selected = 0
170
- scroll = 0
171
- elif c in ('\r', '\n') and current_tab < 3:
172
- # Show full item
173
- item = items[selected] if selected < len(items) else ''
174
- print(f'\033[2J\033[H{item}\n\nPress any key to continue...')
175
- sys.stdout.flush()
176
- sys.stdin.read(1)
177
- sys.stdout.write('\033[2J\033[H')
178
-
179
- finally:
180
- termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
181
- sys.stdout.write('\033[?25h')
182
- sys.stdout.write('\033[2J\033[H')
183
- sys.stdout.flush()
196
+ out.append(f"- {str(r)[:300]}")
197
+ return "\n".join(out) if out else str(results)[:2000]
198
+ return str(results)[:2000]
199
+ except Exception as e:
200
+ return f"Search error: {e}"
184
201
 
185
- query = context.get('query')
186
- num_npcs = int(context.get('num_npcs', 5))
187
- depth = int(context.get('depth', 3))
188
- max_steps = int(context.get('max_steps', 20))
189
- skip_research = context.get('skip_research', True)
190
- exploration = float(context.get('exploration', 0.3))
191
- creativity = float(context.get('creativity', 0.5))
192
- output_format = context.get('format', 'report')
202
+ # ========== File Provenance (matching original) ==========
203
+ @dataclass
204
+ class FileProvenance:
205
+ filename: str
206
+ step_history: List[Tuple[int, str, str, str]] = field(default_factory=list)
207
+
208
+ def get_filesystem_state() -> Dict[str, str]:
209
+ files = {}
210
+ for f in os.listdir("."):
211
+ if os.path.isfile(f):
212
+ with open(f, 'rb') as fh:
213
+ content = fh.read()
214
+ files[f] = hashlib.md5(content).hexdigest()[:8]
215
+ return files
216
+
217
+ def summarize_step(thought, action, outcome, fs_before, fs_after,
218
+ file_provenance, step_num, _model, _provider, _npc):
219
+ """Advisor compresses a major step and suggests next action."""
220
+ current_files = {}
221
+ for f in os.listdir("."):
222
+ if os.path.isfile(f):
223
+ with open(f, 'rb') as fh:
224
+ content = fh.read()
225
+ current_files[f] = {'size': len(content), 'checksum': hashlib.md5(content).hexdigest()[:8]}
226
+
227
+ for f in fs_after:
228
+ if f not in file_provenance:
229
+ file_provenance[f] = FileProvenance(filename=f)
230
+ if f not in fs_before:
231
+ change = f"Created with {current_files.get(f, {}).get('size', '?')} bytes"
232
+ file_provenance[f].step_history.append((step_num, "CREATED", fs_after[f], change))
233
+ elif fs_before.get(f) != fs_after[f]:
234
+ change = f"Modified to {current_files.get(f, {}).get('size', '?')} bytes"
235
+ file_provenance[f].step_history.append((step_num, "MODIFIED", fs_after[f], change))
236
+
237
+ provenance_summary = []
238
+ for filename, prov in file_provenance.items():
239
+ history = "; ".join([f"Step {s}: {a} ({c}) - {ch}" for s, a, c, ch in prov.step_history])
240
+ provenance_summary.append(f"{filename}: {history}")
241
+
242
+ prompt = f"""AGENT'S REASONING: {str(thought)[:1500]}
243
+
244
+ AGENT'S ACTION: {str(action)[:1000]}
245
+ AGENT'S CLAIMED OUTCOME: {str(outcome)[:1000]}
246
+
247
+ COMPLETE FILE PROVENANCE:
248
+ {chr(10).join(provenance_summary)}
249
+
250
+ CURRENT FILESYSTEM:
251
+ Files: {list(current_files.keys())}
252
+ Details: {current_files}
253
+
254
+ Explain plainly what happened and whether the actions produced any measurable effects.
255
+ If the agent thinks then it is likely time to direct it to carry out a specific action.
256
+
257
+ Return JSON with "summary" and "next_step" keys."""
258
+
259
+ try:
260
+ response = get_llm_response(prompt, model=_model, provider=_provider, npc=_npc, format='json')
261
+ summary_data = response.get('response')
262
+ if isinstance(summary_data, str):
263
+ summary_data = json.loads(summary_data)
264
+ if not isinstance(summary_data, dict):
265
+ summary_data = {"summary": str(summary_data), "next_step": "Continue research."}
266
+ except:
267
+ summary_data = {"summary": "Step completed.", "next_step": "Continue research."}
268
+ return summary_data
269
+
270
+ # ========== Persona Generation (matching original) ==========
271
+ def save_persona_to_csv(persona_data):
272
+ csv_dir = os.path.expanduser("~/.npcsh/npc_team")
273
+ os.makedirs(csv_dir, exist_ok=True)
274
+ csv_path = os.path.join(csv_dir, "alicanto_personas.csv")
275
+ file_exists = os.path.exists(csv_path)
276
+ with open(csv_path, 'a', newline='') as csvfile:
277
+ fieldnames = ['name', 'birth_year', 'location', 'leader', 'interests',
278
+ 'worldview', 'approach', 'persona_text', 'created_at']
279
+ writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
280
+ if not file_exists:
281
+ writer.writeheader()
282
+ row = dict(persona_data)
283
+ if isinstance(row.get('interests'), list):
284
+ row['interests'] = json.dumps(row['interests'])
285
+ row['created_at'] = datetime.now().isoformat()
286
+ writer.writerow(row)
287
+
288
+ def generate_one_persona(birth_year, _model, _provider, _npc):
289
+ """Generate a single persona for a given birth year (original algorithm)."""
290
+ teen_year = birth_year + 16
291
+ json_template = (
292
+ '{\n'
293
+ ' "name": "culturally appropriate full name for someone born in ' + str(birth_year) + '",\n'
294
+ ' "location": "specific city/region where they were born in ' + str(birth_year) + '",\n'
295
+ ' "leader": "who ruled their region when they were 16 years old in ' + str(teen_year) + '",\n'
296
+ ' "interests": ["3-5 specific interests/obsessions they had as a teenager in ' + str(teen_year) + '"],\n'
297
+ ' "worldview": "one sentence describing their fundamental perspective shaped by growing up in that era",\n'
298
+ ' "approach": "how their historical background influences their way of thinking"\n'
299
+ '}'
300
+ )
301
+ prompt = (
302
+ f"Generate a unique persona for someone born in {birth_year}. Return JSON:\n"
303
+ f"{json_template}\n\n"
304
+ f"Make this person feel real and historically grounded. Consider: technological context, "
305
+ f"cultural movements, economic conditions, wars, discoveries happening in {teen_year}."
306
+ )
307
+ response = get_llm_response(prompt, model=_model, provider=_provider, npc=_npc, format='json')
308
+ new_persona = response.get('response')
309
+ if isinstance(new_persona, str):
310
+ raw = new_persona
311
+ start = raw.find('{')
312
+ end = raw.rfind('}') + 1
313
+ if start >= 0 and end > start:
314
+ raw = raw[start:end]
315
+ new_persona = json.loads(raw)
316
+
317
+ interests = new_persona.get('interests', [])
318
+ if isinstance(interests, list):
319
+ interests_str = ', '.join(interests)
320
+ else:
321
+ interests_str = str(interests)
322
+
323
+ persona_text = (
324
+ f"You are {new_persona.get('name')}, born {birth_year} in {new_persona.get('location')}, "
325
+ f"came of age under {new_persona.get('leader')}. "
326
+ f"Your interests were: {interests_str}. "
327
+ f"{new_persona.get('worldview')} {new_persona.get('approach')}"
328
+ )
329
+
330
+ persona_data = {
331
+ 'name': new_persona.get('name', f'Agent'),
332
+ 'birth_year': birth_year,
333
+ 'location': new_persona.get('location', 'Unknown'),
334
+ 'leader': new_persona.get('leader', 'Unknown'),
335
+ 'interests': new_persona.get('interests', []),
336
+ 'worldview': new_persona.get('worldview', ''),
337
+ 'approach': new_persona.get('approach', ''),
338
+ 'persona_text': persona_text,
339
+ }
340
+ try:
341
+ save_persona_to_csv(persona_data)
342
+ except:
343
+ pass
344
+ return persona_data
345
+
346
+ # ========== Sub-Agent Trace (matching original) ==========
347
+ def run_sub_agent_trace(hypothesis, persona_data, user_query, _model, _provider, _max_steps, ui_state):
348
+ """Run one sub-agent trace matching original algorithm."""
349
+ agent_name = persona_data.get('name', 'Agent')
350
+ agent_persona = persona_data.get('persona_text', '')
351
+
352
+ # wander wrapper for when agent is stuck
353
+ def wander_wrapper(problem_description: str) -> str:
354
+ """Get creative ideas when stuck using the wander exploration mode."""
355
+ if not WANDER_AVAILABLE:
356
+ return "Wander not available. Try a different approach."
357
+ try:
358
+ _, _, raw_brainstorm, _, _ = perform_single_wandering(
359
+ problem=problem_description, npc=agent, model=_model, provider=_provider
360
+ )
361
+ return str(raw_brainstorm)
362
+ except Exception as e:
363
+ return f"Wander failed: {e}"
364
+
365
+ tools = [create_file, append_to_file, replace_in_file, read_file,
366
+ list_files, execute_shell_command, _web_search_tool, wander_wrapper]
367
+
368
+ agent = NPC(
369
+ name=agent_name.replace(' ', '_').lower(),
370
+ model=_model,
371
+ provider=_provider,
372
+ primary_directive=agent_persona,
373
+ tools=tools
374
+ )
375
+
376
+ trace = SubAgentTrace(hypothesis=hypothesis, agent_name=agent_name, agent_persona=agent_persona)
377
+ summarized_history = []
378
+ file_provenance = {}
379
+ created_files = set()
380
+ summary = {}
381
+ major_step = 0
382
+
383
+ while major_step < _max_steps:
384
+ # Check for skip/quit
385
+ if ui_state.get('skip'):
386
+ ui_state['skip'] = False
387
+ ui_state['log'].append(f"\033[33m Skipped by user\033[0m")
388
+ break
389
+ while ui_state.get('paused'):
390
+ time.sleep(0.2)
391
+ if ui_state.get('skip'):
392
+ break
393
+
394
+ fs_before = get_filesystem_state()
395
+
396
+ provenance_summary = []
397
+ for filename, prov in file_provenance.items():
398
+ history = "; ".join([f"Step {s}: {a} ({c}) - {ch}" for s, a, c, ch in prov.step_history])
399
+ provenance_summary.append(f"{filename}: {history}")
400
+
401
+ history_str = "\n".join(summarized_history)
402
+ next_step_text = f"This is the next step suggested by your advisor. : BEGIN NEXT_STEP: {summary.get('next_step')} END NEXT STEP" if summary else ""
403
+
404
+ search_provider = os.environ.get('NPCSH_SEARCH_PROVIDER', 'duckduckgo')
405
+ initial_prompt = f"""Test the following hypothesis: '{hypothesis}' as related to the user query: '{user_query}'.
406
+ Only focus on your specific hypothesis, other agents are being tasked with other aspects of the problem.
407
+
408
+ Use bash commands to carry out research through the execute_shell_command.
409
+ Adjust files with `replace_in_file` and use `read_file` and `list_files` to verify file states and file creation.
410
+ Create files with create_file()
411
+
412
+ Test with execute_shell_command when needed
413
+ Get unstuck with wander_wrapper
414
+
415
+ When you have a definitive result, say RESEARCH_COMPLETE.
416
+
417
+ FILE PROVENANCE HISTORY:
418
+ {chr(10).join(provenance_summary)}
419
+
420
+ CURRENT FILES: {list(fs_before.keys())}
421
+
422
+ COMPLETE ACTION HISTORY:
423
+ BEGIN HISTORY
424
+ {history_str}
425
+ END HISTORY
426
+
427
+ What specific action will you take next to test your hypothesis?
428
+ AVAILABLE TOOLS: create_file, append_to_file, replace_in_file, read_file, list_files, execute_shell_command, wander_wrapper, _web_search_tool.
429
+
430
+ Do not repeat actions. Use `_web_search_tool` with provider of {search_provider} to look up items if you are struggling.
431
+
432
+ {next_step_text}
433
+
434
+ Your goal is to research. To set up experiments, create figures, and produce data outputs in csvs for verification and reproducibility."""
435
+
436
+ ui_state['log'].append(f"\033[90m Major step {major_step + 1}\033[0m")
437
+
438
+ agent_messages = []
439
+ all_thoughts = []
440
+ all_actions = []
441
+ all_outcomes = []
442
+
443
+ for micro_step in range(5):
444
+ if ui_state.get('skip'):
445
+ break
446
+
447
+ if micro_step == 0:
448
+ current_prompt = initial_prompt
449
+ else:
450
+ current_prompt = "Continue your work. What's your next action?"
451
+
452
+ try:
453
+ response = agent.get_llm_response(
454
+ current_prompt,
455
+ messages=agent_messages,
456
+ auto_process_tool_calls=True
457
+ )
458
+ except Exception as e:
459
+ ui_state['log'].append(f" \033[31mLLM error: {str(e)[:80]}\033[0m")
460
+ break
461
+
462
+ agent_messages = response.get('messages', [])
463
+ thought = response.get('response')
464
+ if thought is None:
465
+ thought = ''
466
+ else:
467
+ all_thoughts.append(thought)
468
+ preview = thought.replace('\n', ' ')[:120]
469
+ ui_state['log'].append(f" {preview}")
470
+
471
+ if thought and "RESEARCH_COMPLETE" in thought.upper():
472
+ ui_state['log'].append(f" \033[32mRESEARCH_COMPLETE\033[0m")
473
+ break
474
+
475
+ if response.get('tool_results'):
476
+ tool_results = response['tool_results']
477
+ tool_names = []
478
+ outcomes = []
479
+ for res in tool_results:
480
+ tname = res.get('tool_name', '?')
481
+ tool_names.append(tname)
482
+ args = res.get('arguments', {})
483
+ if tname in ('create_file', 'append_to_file', 'replace_in_file'):
484
+ fname = args.get('filename')
485
+ if fname:
486
+ created_files.add(fname)
487
+ trace.was_successful = True
488
+ outcomes.append(str(res.get('result', ''))[:200])
489
+
490
+ ui_state['log'].append(f" \033[36mTools: {', '.join(tool_names)}\033[0m")
491
+ all_actions.append(", ".join([f"{r.get('tool_name')}({r.get('arguments', {})})" for r in tool_results]))
492
+ all_outcomes.extend(outcomes)
493
+ elif micro_step > 0 and not response.get('tool_calls'):
494
+ break
495
+
496
+ fs_after = get_filesystem_state()
497
+ new_files = set(fs_after.keys()) - set(fs_before.keys())
498
+ if new_files:
499
+ ui_state['log'].append(f" \033[32mNew files: {list(new_files)}\033[0m")
500
+
501
+ combined_thought = " ".join(all_thoughts)
502
+ combined_action = " | ".join(filter(None, all_actions))
503
+ combined_outcome = " | ".join(filter(None, all_outcomes))
504
+
505
+ ui_state['log'].append(f" \033[90mCompressing step...\033[0m")
506
+ summary = summarize_step(
507
+ combined_thought, combined_action, combined_outcome,
508
+ fs_before, fs_after, file_provenance,
509
+ major_step + 1, _model, _provider, agent
510
+ )
511
+
512
+ summary_text = summary.get('summary', 'Step completed.')
513
+ next_text = summary.get('next_step', '')
514
+ ui_state['log'].append(f" \033[32mSummary: {str(summary_text)[:120]}\033[0m")
515
+ if next_text:
516
+ ui_state['log'].append(f" \033[33mNext: {str(next_text)[:100]}\033[0m")
517
+
518
+ summarized_history.append(f"Step {major_step + 1}: {summary_text}")
519
+ trace.steps.append(ResearchStep(
520
+ step=major_step + 1,
521
+ thought=combined_thought,
522
+ action=combined_action,
523
+ outcome=combined_outcome
524
+ ))
525
+
526
+ if combined_thought and "RESEARCH_COMPLETE" in combined_thought.upper():
527
+ break
528
+
529
+ major_step += 1
530
+
531
+ for filename in created_files:
532
+ if os.path.exists(filename):
533
+ try:
534
+ trace.final_files[filename] = read_file(filename)
535
+ except:
536
+ pass
537
+
538
+ return trace
539
+
540
+ # ========== Paper Writing (matching original) ==========
541
+ def write_paper(all_traces, user_query, _model, _provider, coordinator, ui_state):
542
+ """Iterative LaTeX paper building from compressed traces."""
543
+ ui_state['log'].append(f"\n\033[1;36m--- Writing Paper ---\033[0m")
544
+
545
+ # Compress traces for synthesis
546
+ compressed_summaries = []
547
+ for trace in all_traces:
548
+ steps_summary = []
549
+ for step in trace.steps[-3:]:
550
+ thought_short = (step.thought[:100] + "...") if step.thought and len(step.thought) > 100 else (step.thought or "No thought")
551
+ action_short = (step.action[:100] + "...") if step.action and len(step.action) > 100 else (step.action or "No action")
552
+ steps_summary.append(f"Step {step.step}: {thought_short} | {action_short}")
553
+ compressed_summaries.append({
554
+ "agent": trace.agent_name,
555
+ "hypothesis": trace.hypothesis,
556
+ "success": trace.was_successful,
557
+ "key_steps": steps_summary,
558
+ "files_created": list(trace.final_files.keys()),
559
+ })
560
+ compressed_research = json.dumps(compressed_summaries, indent=2)
561
+
562
+ author_list = [trace.agent_name for trace in all_traces]
563
+ author_string = ", ".join(author_list)
564
+
565
+ pct = chr(37) # percent sign - avoid bare % in YAML
566
+ todo = lambda s: f"{pct} TODO: {s}"
567
+ initial_latex = (
568
+ "\\documentclass{article}\n"
569
+ "\\title{" + todo("TITLE") + "}\n"
570
+ "\\author{" + author_string + "}\n"
571
+ "\\date{\\today}\n"
572
+ "\\begin{document}\n"
573
+ "\\maketitle\n\n"
574
+ "\\begin{abstract}\n"
575
+ + todo("ABSTRACT") + "\n"
576
+ "\\end{abstract}\n\n"
577
+ "\\section{Introduction}\n"
578
+ + todo("INTRODUCTION") + "\n\n"
579
+ "\\section{Methods}\n"
580
+ + todo("METHODS") + "\n\n"
581
+ "\\section{Results}\n"
582
+ + todo("RESULTS") + "\n\n"
583
+ "\\section{Discussion}\n"
584
+ + todo("DISCUSSION") + "\n\n"
585
+ "\\end{document}"
586
+ )
587
+
588
+ create_file("paper.tex", initial_latex)
589
+ ui_state['log'].append(f" Created paper.tex scaffold")
590
+
591
+ todo_sections = ["TITLE", "ABSTRACT", "INTRODUCTION", "METHODS", "RESULTS", "DISCUSSION"]
592
+
593
+ for section_round in range(len(todo_sections)):
594
+ current_paper = read_file("paper.tex")
595
+ sections_status = {s: "EMPTY" if (chr(37) + " TODO: " + s) in current_paper else "COMPLETE" for s in todo_sections}
596
+
597
+ next_section = None
598
+ for s in todo_sections:
599
+ if sections_status[s] == "EMPTY":
600
+ next_section = s
601
+ break
602
+ if not next_section:
603
+ ui_state['log'].append(f" \033[32mAll sections complete\033[0m")
604
+ break
605
+
606
+ ui_state['log'].append(f" Writing section: {next_section}")
607
+
608
+ coord_messages = []
609
+ section_prompt = f"""You are writing a research paper about: "{user_query}"
610
+
611
+ Research data from sub-agents: {compressed_research[:3000]}
612
+
613
+ Current paper content:
614
+ {current_paper}
615
+
616
+ Your task: Complete the {next_section} section by replacing the "{chr(37)} TODO: {next_section}" marker with actual content.
617
+
618
+ Use replace_in_file to update the paper. Use _web_search_tool if you need more information.
619
+
620
+ Focus ONLY on the {next_section} section. Write 2-4 paragraphs of substantial academic content.
621
+
622
+ Available tools: replace_in_file, read_file, _web_search_tool"""
623
+
624
+ for micro in range(5):
625
+ if micro == 0:
626
+ cprompt = section_prompt
627
+ else:
628
+ cprompt = f"Continue working on the {next_section} section. What's your next action?"
629
+ try:
630
+ resp = coordinator.get_llm_response(
631
+ cprompt, messages=coord_messages, auto_process_tool_calls=True
632
+ )
633
+ coord_messages = resp.get('messages', [])
634
+ if resp.get('tool_results'):
635
+ for tr in resp['tool_results']:
636
+ ui_state['log'].append(f" \033[36m{tr.get('tool_name', '?')}\033[0m")
637
+ except:
638
+ break
639
+
640
+ final_paper = read_file("paper.tex")
641
+ return final_paper
642
+
643
+ # ========== Sub-Agent Review Cycle ==========
644
+ def do_sub_agent_reviews(all_traces, paper_content, user_query, _model, _provider, cycle_num, ui_state):
645
+ """Each sub-agent reviews the paper and provides suggestions."""
646
+ ui_state['log'].append(f"\n\033[1;35m--- Cycle {cycle_num}: Sub-Agent Reviews ---\033[0m")
647
+ suggestions = []
648
+
649
+ for i, trace in enumerate(all_traces):
650
+ if ui_state.get('skip'):
651
+ ui_state['skip'] = False
652
+ break
653
+ while ui_state.get('paused'):
654
+ time.sleep(0.2)
655
+ if ui_state.get('skip'):
656
+ break
657
+
658
+ agent_name = trace.agent_name
659
+ ui_state['log'].append(f"\n\033[36m Reviewer: {agent_name}\033[0m")
660
+
661
+ # Build context of what this agent found
662
+ agent_findings = []
663
+ for step in trace.steps:
664
+ if step.thought:
665
+ agent_findings.append(f"Step {step.step}: {step.thought[:300]}")
666
+ findings_text = "\n".join(agent_findings[-5:])
667
+ files_text = ", ".join(trace.final_files.keys()) if trace.final_files else "(none)"
668
+
669
+ review_prompt = f"""You are {agent_name}, a research sub-agent. You previously investigated the hypothesis:
670
+ "{trace.hypothesis}"
671
+
672
+ Your key findings were:
673
+ {findings_text}
674
+
675
+ Files you created: {files_text}
676
+
677
+ Now review the following research paper written about the query: "{user_query}"
678
+
679
+ === PAPER ===
680
+ {paper_content[:4000]}
681
+ === END PAPER ===
682
+
683
+ Based on your expertise and findings, provide a critical review with specific suggestions:
684
+ 1. Are your findings accurately represented?
685
+ 2. What important findings or nuances are missing?
686
+ 3. What claims need stronger evidence or qualification?
687
+ 4. What additional analysis or experiments should be done?
688
+ 5. Specific text improvements (cite sections by name).
193
689
 
194
- # Handle case where npc might be a string (npc name) or NPC object
195
- model = context.get('model') or (npc.model if npc and hasattr(npc, 'model') else 'gemini-1.5-pro')
196
- provider = context.get('provider') or (npc.provider if npc and hasattr(npc, 'provider') else 'gemini')
690
+ Be concrete and specific. Reference sections (Introduction, Methods, Results, Discussion) directly."""
197
691
 
198
- if not query:
199
- context['output'] = """Usage: /alicanto <research query>
692
+ try:
693
+ resp = get_llm_response(review_prompt, model=_model, provider=_provider, npc=None)
694
+ review_text = resp.get('response', '')
695
+ if review_text:
696
+ suggestions.append({
697
+ 'agent': agent_name,
698
+ 'hypothesis': trace.hypothesis,
699
+ 'review': review_text,
700
+ })
701
+ preview = review_text.replace('\n', ' ')[:120]
702
+ ui_state['log'].append(f" {preview}")
703
+ else:
704
+ ui_state['log'].append(f" \033[90m(empty review)\033[0m")
705
+ except Exception as e:
706
+ ui_state['log'].append(f" \033[31mReview error: {str(e)[:80]}\033[0m")
707
+
708
+ return suggestions
709
+
710
+ def revise_paper(paper_content, suggestions, user_query, coordinator, cycle_num, ui_state):
711
+ """Coordinator revises the paper based on sub-agent suggestions."""
712
+ ui_state['log'].append(f"\n\033[1;36m--- Cycle {cycle_num}: Revising Paper ---\033[0m")
713
+
714
+ # Format all suggestions
715
+ suggestions_text = ""
716
+ for s in suggestions:
717
+ suggestions_text += f"\n--- Review by {s['agent']} (Hypothesis: {s['hypothesis'][:80]}) ---\n"
718
+ suggestions_text += s['review'][:1500] + "\n"
719
+
720
+ # Write current paper to file for coordinator to edit
721
+ paper_path = os.path.abspath("paper.tex")
722
+ with open(paper_path, 'w') as f:
723
+ f.write(paper_content)
724
+
725
+ revision_prompt = f"""You are revising a research paper about: "{user_query}"
726
+
727
+ This is revision cycle {cycle_num}. Your sub-agents have reviewed the paper and provided suggestions.
728
+
729
+ CURRENT PAPER:
730
+ {paper_content[:4000]}
731
+
732
+ SUB-AGENT REVIEWS AND SUGGESTIONS:
733
+ {suggestions_text[:4000]}
734
+
735
+ Your task:
736
+ 1. Read the reviews carefully
737
+ 2. Incorporate valid suggestions using replace_in_file on paper.tex
738
+ 3. Strengthen weak sections
739
+ 4. Add missing findings or nuances
740
+ 5. Improve clarity and academic rigor
741
+ 6. Do NOT remove existing good content - only improve and add
742
+
743
+ Use replace_in_file to make targeted improvements to paper.tex.
744
+ Use read_file to check current state.
745
+
746
+ Available tools: replace_in_file, read_file, append_to_file, _web_search_tool"""
747
+
748
+ coord_messages = []
749
+ for micro in range(8):
750
+ if ui_state.get('skip'):
751
+ break
752
+ if micro == 0:
753
+ cprompt = revision_prompt
754
+ else:
755
+ cprompt = "Continue revising the paper. What's your next improvement?"
756
+ try:
757
+ resp = coordinator.get_llm_response(
758
+ cprompt, messages=coord_messages, auto_process_tool_calls=True
759
+ )
760
+ coord_messages = resp.get('messages', [])
761
+ response_text = resp.get('response', '')
762
+ if resp.get('tool_results'):
763
+ for tr in resp['tool_results']:
764
+ ui_state['log'].append(f" \033[36m{tr.get('tool_name', '?')}\033[0m")
765
+ elif micro > 0 and not resp.get('tool_calls'):
766
+ break
767
+ except Exception as e:
768
+ ui_state['log'].append(f" \033[31mRevision error: {str(e)[:80]}\033[0m")
769
+ break
770
+
771
+ revised = read_file("paper.tex")
772
+ ui_state['log'].append(f" \033[32mRevision cycle {cycle_num} complete\033[0m")
773
+ return revised
774
+
775
+ # ========== Trace Saving (matching original) ==========
776
+ def save_traces_csv(all_traces):
777
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
778
+ trace_dir = os.path.join(_work_dir, "alicanto_traces")
779
+ os.makedirs(trace_dir, exist_ok=True)
780
+ filepath = os.path.join(trace_dir, f"trace_{timestamp}.csv")
781
+ rows = []
782
+ for trace in all_traces:
783
+ for step in trace.steps:
784
+ rows.append({
785
+ "hypothesis": trace.hypothesis,
786
+ "agent_name": trace.agent_name,
787
+ "agent_persona": trace.agent_persona,
788
+ "was_successful": trace.was_successful,
789
+ "step": step.step,
790
+ "thought": step.thought[:2000],
791
+ "action": step.action[:1000],
792
+ "outcome": step.outcome[:1000],
793
+ "final_files": json.dumps(list(trace.final_files.keys())),
794
+ })
795
+ if rows:
796
+ with open(filepath, 'w', newline='') as f:
797
+ writer = csv.DictWriter(f, fieldnames=rows[0].keys())
798
+ writer.writeheader()
799
+ writer.writerows(rows)
800
+ return filepath
801
+
802
+ # ========== Global State ==========
803
+ class AlicantoState:
804
+ def __init__(self):
805
+ self.phase = 0 # 0=query, 1=hypotheses, 2=personas, 3=execution, 4=review
806
+ self.auto_mode = False
807
+ self.query = query or ""
808
+ self.hypotheses = [] # list of hypothesis strings
809
+ self.personas = [] # list of persona dicts
810
+ self.traces = [] # list of SubAgentTrace
811
+ self.current_agent = -1
812
+ # UI state
813
+ self.sel = 0
814
+ self.scroll = 0
815
+ self.mode = 'normal' # normal, edit
816
+ self.input_buf = ""
817
+ self.input_target = ""
818
+ self.status = ""
819
+ self.generating = False
820
+ self.error = ""
821
+ # Review state
822
+ self.review_tab = 0 # 0=agents, 1=insights, 2=files, 3=paper
823
+ self.starred = set()
824
+ self.paper = ""
825
+ self.all_insights = []
826
+ self.all_files = [] # [{name, agent, content}]
827
+ # Execution display
828
+ self.exec_log = []
829
+ self.exec_scroll = 0
830
+ self.exec_paused = False
831
+ self.exec_skip = False
832
+ self.current_cycle = 0
833
+ self.total_cycles = num_cycles
834
+ self.end_cycles = False
835
+ # Shared state for sub-agent communication
836
+ self.agent_ui = {'log': [], 'skip': False, 'paused': False}
837
+
838
+ @property
839
+ def phase_name(self):
840
+ return ["Query", "Hypotheses", "Personas", "Execution", "Review"][self.phase]
841
+
842
+ ui = AlicantoState()
843
+
844
+ # ========== Query Entry ==========
845
+ if not ui.query:
846
+ if sys.stdin.isatty():
847
+ print("\033[1;36m ALICANTO - Multi-Agent Research System \033[0m")
848
+ print("\033[90mEnter your research query (or 'q' to quit):\033[0m")
849
+ try:
850
+ ui.query = input("\033[33m> \033[0m").strip()
851
+ except (EOFError, KeyboardInterrupt):
852
+ ui.query = ""
853
+ if not ui.query or ui.query.lower() == 'q':
854
+ os.chdir(_orig_cwd)
855
+ context['output'] = "Alicanto cancelled."
856
+ context['messages'] = messages
857
+ exit()
858
+ else:
859
+ os.chdir(_orig_cwd)
860
+ context['output'] = """Usage: /alicanto <research query>
200
861
 
201
862
  Options:
202
- --num-npcs N Number of research perspectives (default: 5)
203
- --depth N Research depth (default: 3)
204
- --max-steps N Maximum research steps (default: 20)
205
- --exploration F Exploration factor 0-1 (default: 0.3)
206
- --creativity F Creativity factor 0-1 (default: 0.5)
207
- --format FORMAT Output: report|summary|full (default: report)
863
+ --num-npcs N Number of sub-agents (default: 3)
864
+ --max-steps N Max major steps per agent (default: 10)
865
+ --num-cycles N Review/revision cycles (default: 3)
866
+ --model MODEL LLM model
867
+ --provider PROVIDER LLM provider
208
868
 
209
869
  Example: /alicanto What are the latest advances in quantum computing?"""
210
- context['messages'] = messages
211
- exit()
870
+ context['messages'] = messages
871
+ exit()
212
872
 
213
- print(f"""
214
- █████╗ ██╗ ██╗ ██████╗ █████╗ ███╗ ██╗████████╗ ██████╗
215
- ██╔══██╗██║ ██║██╔════╝██╔══██╗████╗ ██║╚══██╔══╝██╔═══██╗
216
- ███████║██║ ██║██║ ███████║██╔██╗ ██║ ██║ ██║ ██║
217
- ██╔══██║██║ ██║██║ ██╔══██║██║╚██╗██║ ██║ ██║ ██║
218
- ██║ ██║███████╗██║╚██████╗██║ ██║██║ ╚████║ ██║ ╚██████╔╝
219
- ╚═╝ ╚═╝╚══════╝╚═╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝
220
-
221
- Deep Research Mode
222
- Query: {query}
223
- Perspectives: {num_npcs} | Depth: {depth} | Max Steps: {max_steps}
224
- """)
225
-
226
- # Generate research perspectives
227
- perspectives_prompt = f"""Generate {num_npcs} distinct research perspectives for investigating: "{query}"
228
-
229
- For each perspective, provide:
230
- 1. Name (a descriptive title)
231
- 2. Approach (how this perspective would investigate)
232
- 3. Key questions to explore
233
-
234
- Return as a numbered list."""
235
-
236
- print(colored("Generating research perspectives...", "cyan"))
237
- resp = get_llm_response(
238
- perspectives_prompt,
239
- model=model,
240
- provider=provider,
241
- npc=npc
242
- )
243
- perspectives = str(resp.get('response', ''))
244
- print(perspectives)
245
-
246
- # Conduct web research if not skipped
247
- research_findings = ""
248
- if not skip_research:
249
- print(colored("\nConducting web research...", "cyan"))
873
+ # ========== Generation Functions ==========
874
+ def do_generate_hypotheses():
875
+ ui.status = "Generating hypotheses..."
876
+ ui.generating = True
877
+ ui.error = ""
250
878
  try:
251
- search_results = search_web(query, n_results=5)
252
- if search_results:
253
- research_findings = "\n\nWeb Research Findings:\n"
254
- for i, result in enumerate(search_results[:5], 1):
255
- title = result.get('title', 'No title')
256
- snippet = result.get('snippet', result.get('body', ''))[:200]
257
- research_findings += f"\n{i}. {title}\n {snippet}...\n"
258
- print(colored(f"Found {len(search_results)} sources", "green"))
259
- except Exception as e:
260
- print(colored(f"Web search error: {e}", "yellow"))
879
+ one_shot = '''
880
+ "example_input": "Investigate the impact of quantum annealing on protein folding.",
881
+ "example_output": {
882
+ "hypotheses": [
883
+ "Implementing a quantum annealer simulation for a small peptide chain will identify lower energy states faster than a classical simulated annealing approach.",
884
+ "The choice of qubit connectivity in the quantum annealer topology significantly impacts the final folded state accuracy for proteins with long-range interactions.",
885
+ "Encoding the protein residue interactions as a QUBO problem is feasible for structures up to 50 amino acids before qubit requirements become prohibitive."
886
+ ]
887
+ }'''
888
+ prompt = f"""Based on the following research topic, generate a list of {num_npcs} distinct, specific, and empirically testable hypotheses.
261
889
 
262
- # Multi-step exploration from each perspective
263
- all_insights = []
264
- gold_insights = [] # Key valuable findings
265
- cliff_warnings = [] # Potential pitfalls or caveats
890
+ TOPIC: "{ui.query}"
266
891
 
267
- for step in range(min(depth, max_steps)):
268
- print(colored(f"\n--- Research Depth {step + 1}/{depth} ---", "cyan"))
892
+ Return a JSON object with a single key "hypotheses" which is a list of strings.
269
893
 
270
- explore_prompt = f"""Research query: "{query}"
894
+ Here is an example of the expected input and output format:
895
+ {one_shot}
271
896
 
272
- Perspectives generated:
273
- {perspectives}
897
+ Return ONLY the JSON object."""
274
898
 
275
- {research_findings}
899
+ resp = get_llm_response(prompt, model=model, provider=provider, npc=npc, format='json')
900
+ result = resp.get('response')
901
+ if isinstance(result, str):
902
+ raw = result
903
+ start = raw.find('{')
904
+ end = raw.rfind('}') + 1
905
+ if start >= 0 and end > start:
906
+ raw = raw[start:end]
907
+ result = json.loads(raw)
908
+ if isinstance(result, dict):
909
+ ui.hypotheses = result.get('hypotheses', [])
910
+ elif isinstance(result, list):
911
+ ui.hypotheses = result
912
+ else:
913
+ ui.hypotheses = [str(result)]
914
+
915
+ if not ui.hypotheses:
916
+ ui.hypotheses = [f"Investigate: {ui.query}"]
917
+ except Exception as e:
918
+ ui.error = f"Hypothesis error: {e}"
919
+ ui.hypotheses = [f"Investigate: {ui.query}"]
920
+ ui.generating = False
921
+ ui.status = f"{len(ui.hypotheses)} hypotheses generated"
276
922
 
277
- Previous insights: {all_insights[-3:] if all_insights else 'None yet'}
923
+ def do_generate_personas():
924
+ ui.status = "Generating personas..."
925
+ ui.generating = True
926
+ ui.error = ""
927
+ n = len(ui.hypotheses)
928
+ ui.personas = []
929
+ for i in range(n):
930
+ birth_year = random.randint(-32665, 32665)
931
+ ui.status = f"Generating persona {i+1}/{n} (born {birth_year})..."
932
+ try:
933
+ persona = generate_one_persona(birth_year, model, provider, npc)
934
+ persona['hypothesis_idx'] = i
935
+ ui.personas.append(persona)
936
+ except Exception as e:
937
+ ui.personas.append({
938
+ 'name': f'Agent {i+1}', 'birth_year': birth_year,
939
+ 'location': 'Unknown', 'leader': 'Unknown',
940
+ 'interests': [], 'worldview': '', 'approach': '',
941
+ 'persona_text': f'You are Agent {i+1}, born {birth_year}.',
942
+ 'hypothesis_idx': i,
943
+ })
944
+ ui.error = f"Persona {i+1} error: {e}"
945
+ ui.generating = False
946
+ ui.status = f"{len(ui.personas)} personas generated"
278
947
 
279
- For depth level {step + 1}:
280
- 1. Explore deeper implications from each perspective
281
- 2. Identify GOLD insights (valuable, non-obvious findings) - mark with [GOLD]
282
- 3. Identify CLIFF warnings (pitfalls, caveats, risks) - mark with [CLIFF]
283
- 4. Connect insights across perspectives
948
+ def do_run_all_agents():
949
+ """Run all sub-agents serially, write paper, then iterate review cycles."""
950
+ ui.generating = True
951
+ ui.error = ""
952
+ ui.traces = []
953
+ ui.agent_ui = {'log': ui.exec_log, 'skip': False, 'paused': False}
284
954
 
285
- Exploration factor: {exploration} (higher = more diverse exploration)
286
- Creativity factor: {creativity} (higher = more novel connections)"""
955
+ # Create coordinator NPC for paper writing
956
+ def wander_wrapper_coord(problem_description: str) -> str:
957
+ """Get creative ideas when stuck."""
958
+ if not WANDER_AVAILABLE:
959
+ return "Wander not available."
960
+ try:
961
+ _, _, raw, _, _ = perform_single_wandering(
962
+ problem=problem_description, npc=coordinator, model=model, provider=provider
963
+ )
964
+ return str(raw)
965
+ except Exception as e:
966
+ return f"Wander failed: {e}"
287
967
 
288
- resp = get_llm_response(
289
- explore_prompt,
290
- model=model,
291
- provider=provider,
292
- temperature=creativity,
293
- npc=npc
968
+ coord_tools = [create_file, append_to_file, replace_in_file, read_file,
969
+ list_files, execute_shell_command, _web_search_tool, wander_wrapper_coord]
970
+
971
+ coordinator = NPC(
972
+ name="Alicanto",
973
+ model=model, provider=provider,
974
+ primary_directive="You are Alicanto the mythical bird. You research topics iteratively by writing to LaTeX files and searching for more information.",
975
+ tools=coord_tools
294
976
  )
295
977
 
296
- step_insights = str(resp.get('response', ''))
297
- print(step_insights)
298
-
299
- # Extract gold and cliff markers
300
- if '[GOLD]' in step_insights:
301
- gold_insights.extend([line.strip() for line in step_insights.split('\n') if '[GOLD]' in line])
302
- if '[CLIFF]' in step_insights:
303
- cliff_warnings.extend([line.strip() for line in step_insights.split('\n') if '[CLIFF]' in line])
304
-
305
- all_insights.append(step_insights)
306
-
307
- # Generate final synthesis
308
- print(colored("\n--- Synthesizing Research ---", "cyan"))
309
-
310
- synthesis_prompt = f"""Synthesize research on: "{query}"
311
-
312
- All insights gathered:
313
- {chr(10).join(all_insights)}
314
-
315
- Gold insights identified:
316
- {chr(10).join(gold_insights) if gold_insights else 'None explicitly marked'}
317
-
318
- Cliff warnings identified:
319
- {chr(10).join(cliff_warnings) if cliff_warnings else 'None explicitly marked'}
320
-
321
- Generate a {output_format} that:
322
- 1. Summarizes key findings
323
- 2. Highlights the most valuable insights (gold)
324
- 3. Notes important caveats and risks (cliffs)
325
- 4. Provides actionable conclusions"""
326
-
327
- resp = get_llm_response(
328
- synthesis_prompt,
329
- model=model,
330
- provider=provider,
331
- npc=npc
332
- )
333
-
334
- final_report = str(resp.get('response', ''))
335
- print("\n" + "="*60)
336
- print(colored("ALICANTO RESEARCH REPORT", "green", attrs=['bold']))
337
- print("="*60)
338
- print(final_report)
339
-
340
- alicanto_result = {
341
- 'query': query,
342
- 'perspectives': perspectives,
343
- 'insights': all_insights,
344
- 'gold': gold_insights,
345
- 'cliffs': cliff_warnings,
346
- 'report': final_report
347
- }
978
+ # ===== Cycle 1: Sub-agent experimentation =====
979
+ ui.current_cycle = 1
980
+ ui.exec_log.append(f"\n\033[1;33m{'='*50}\033[0m")
981
+ ui.exec_log.append(f"\033[1;33m CYCLE 1/{num_cycles}: Sub-Agent Experimentation\033[0m")
982
+ ui.exec_log.append(f"\033[1;33m{'='*50}\033[0m")
348
983
 
349
- context['output'] = final_report
350
- context['messages'] = messages
351
- context['alicanto_result'] = alicanto_result
984
+ for i in range(len(ui.hypotheses)):
985
+ hyp = ui.hypotheses[i]
986
+ persona = ui.personas[i % len(ui.personas)]
987
+ ui.current_agent = i
988
+ ui.exec_log.append(f"\n\033[1;36m--- Agent {i+1}/{len(ui.hypotheses)}: {persona['name']} ---\033[0m")
989
+ ui.exec_log.append(f" \033[90mHypothesis: {hyp[:100]}\033[0m")
990
+ ui.exec_log.append(f" \033[90mBorn {persona['birth_year']} in {persona.get('location', '?')}\033[0m")
991
+
992
+ ui.agent_ui['skip'] = ui.exec_skip
993
+ ui.agent_ui['paused'] = ui.exec_paused
994
+
995
+ trace = run_sub_agent_trace(hyp, persona, ui.query, model, provider, max_steps, ui.agent_ui)
996
+ ui.traces.append(trace)
997
+ ui.exec_log.append(f" \033[1;32mCompleted: {'SUCCESS' if trace.was_successful else 'no files'}, {len(trace.steps)} steps\033[0m")
998
+
999
+ # Save traces
1000
+ try:
1001
+ trace_path = save_traces_csv(ui.traces)
1002
+ ui.exec_log.append(f"\n\033[90mTraces saved: {trace_path}\033[0m")
1003
+ except Exception as e:
1004
+ ui.exec_log.append(f"\n\033[31mTrace save error: {e}\033[0m")
1005
+
1006
+ # ===== Cycle 1: Coordinator writes initial paper =====
1007
+ ui.exec_log.append(f"\n\033[1;36m--- Coordinator: Writing Initial Paper ---\033[0m")
1008
+ try:
1009
+ ui.paper = write_paper(ui.traces, ui.query, model, provider, coordinator, ui.agent_ui)
1010
+ except Exception as e:
1011
+ ui.paper = f"Paper writing error: {e}"
1012
+ ui.exec_log.append(f" \033[31m{e}\033[0m")
1013
+
1014
+ # ===== Cycles 2..N: Review and revise =====
1015
+ for cycle in range(2, num_cycles + 1):
1016
+ if ui.end_cycles:
1017
+ ui.exec_log.append(f"\n\033[33mCycles ended early by user at cycle {cycle-1}\033[0m")
1018
+ break
1019
+
1020
+ ui.current_cycle = cycle
1021
+ ui.exec_log.append(f"\n\033[1;33m{'='*50}\033[0m")
1022
+ ui.exec_log.append(f"\033[1;33m CYCLE {cycle}/{num_cycles}: Review & Revise\033[0m")
1023
+ ui.exec_log.append(f"\033[1;33m{'='*50}\033[0m")
1024
+
1025
+ # Sub-agents review the paper
1026
+ suggestions = do_sub_agent_reviews(
1027
+ ui.traces, ui.paper, ui.query, model, provider, cycle, ui.agent_ui
1028
+ )
1029
+
1030
+ if not suggestions:
1031
+ ui.exec_log.append(f"\n\033[90mNo suggestions received, skipping revision\033[0m")
1032
+ continue
1033
+
1034
+ if ui.end_cycles:
1035
+ break
1036
+
1037
+ # Coordinator revises based on suggestions
1038
+ ui.paper = revise_paper(
1039
+ ui.paper, suggestions, ui.query, coordinator, cycle, ui.agent_ui
1040
+ )
1041
+
1042
+ ui.exec_log.append(f"\n\033[1;32m{'='*50}\033[0m")
1043
+ ui.exec_log.append(f"\033[1;32m All {ui.current_cycle} cycle(s) complete\033[0m")
1044
+ ui.exec_log.append(f"\033[1;32m{'='*50}\033[0m")
1045
+
1046
+ # Collect insights and files
1047
+ ui.all_insights = []
1048
+ ui.all_files = []
1049
+ for trace in ui.traces:
1050
+ for step in trace.steps:
1051
+ if step.thought:
1052
+ ui.all_insights.append(f"[{trace.agent_name}] Step {step.step}: {step.thought[:300]}")
1053
+ for fname, content in trace.final_files.items():
1054
+ ui.all_files.append({"name": fname, "agent": trace.agent_name, "content": content[:1000]})
1055
+
1056
+ ui.generating = False
1057
+ ui.phase = 4
1058
+ ui.sel = 0
1059
+ ui.scroll = 0
1060
+ ui.status = f"Research complete ({ui.current_cycle} cycles). Review results."
1061
+
1062
+ # ========== TUI Rendering ==========
1063
+ def render_screen():
1064
+ width, height = get_size()
1065
+ out = []
1066
+ out.append("\033[2J\033[H")
1067
+
1068
+ auto_badge = " \033[32m[AUTO]\033[0m" if ui.auto_mode else ""
1069
+ phase_display = f" ALICANTO - Phase {ui.phase+1}: {ui.phase_name} "
1070
+ out.append(f"\033[1;1H\033[7;1m{phase_display.ljust(width)}\033[0m")
1071
+ out.append(f"\033[1;{width-20}H{auto_badge}")
1072
+
1073
+ if ui.phase == 0:
1074
+ render_phase_query(out, width, height)
1075
+ elif ui.phase == 1:
1076
+ render_phase_hypotheses(out, width, height)
1077
+ elif ui.phase == 2:
1078
+ render_phase_personas(out, width, height)
1079
+ elif ui.phase == 3:
1080
+ render_phase_execution(out, width, height)
1081
+ elif ui.phase == 4:
1082
+ render_phase_review(out, width, height)
1083
+
1084
+ status_color = "\033[33m" if ui.generating else "\033[90m"
1085
+ err_text = f" \033[31m{ui.error}\033[0m" if ui.error else ""
1086
+ out.append(f"\033[{height-1};1H\033[K{status_color}{ui.status}\033[0m{err_text}")
1087
+
1088
+ sys.stdout.write(''.join(out))
1089
+ sys.stdout.flush()
1090
+
1091
+ def render_phase_query(out, width, height):
1092
+ banner = [
1093
+ "\033[36m █████╗ ██╗ ██╗ ██████╗ █████╗ ███╗ ██╗████████╗ ██████╗\033[0m",
1094
+ "\033[36m██╔══██╗██║ ██║██╔════╝██╔══██╗████╗ ██║╚══██╔══╝██╔═══██╗\033[0m",
1095
+ "\033[36m███████║██║ ██║██║ ███████║██╔██╗ ██║ ██║ ██║ ██║\033[0m",
1096
+ "\033[36m██╔══██║██║ ██║██║ ██╔══██║██║╚██╗██║ ██║ ██║ ██║\033[0m",
1097
+ "\033[36m██║ ██║███████╗██║╚██████╗██║ ██║██║ ╚████║ ██║ ╚██████╔╝\033[0m",
1098
+ "\033[36m╚═╝ ╚═╝╚══════╝╚═╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝\033[0m",
1099
+ ]
1100
+ for i, line in enumerate(banner):
1101
+ out.append(f"\033[{3+i};3H{line}")
1102
+
1103
+ y = 3 + len(banner) + 2
1104
+ out.append(f"\033[{y};3H\033[1mQuery:\033[0m {ui.query}")
1105
+ out.append(f"\033[{y+1};3H\033[90mAgents: {num_npcs} | Max steps: {max_steps} | Cycles: {num_cycles} | Model: {model or 'default'} | Provider: {provider or 'default'}\033[0m")
1106
+ out.append(f"\033[{height};1H\033[K\033[7m Enter:Continue A:Auto-mode q:Quit \033[0m".ljust(width))
1107
+
1108
+ def render_phase_hypotheses(out, width, height):
1109
+ list_h = height - 6
1110
+ n = len(ui.hypotheses)
1111
+
1112
+ if ui.mode == 'edit':
1113
+ out.append(f"\033[3;1H\033[33mEditing hypothesis:\033[0m")
1114
+ out.append(f"\033[4;1H\033[7m {ui.input_buf} \033[0m")
1115
+ out.append(f"\033[{height};1H\033[K\033[7m Enter:Save Esc:Cancel \033[0m".ljust(width))
1116
+ return
1117
+
1118
+ out.append(f"\033[3;1H\033[1m Hypotheses ({n}) \033[90m{'─' * (width - 20)}\033[0m")
1119
+
1120
+ if ui.sel < ui.scroll:
1121
+ ui.scroll = ui.sel
1122
+ elif ui.sel >= ui.scroll + (list_h // 2):
1123
+ ui.scroll = ui.sel - (list_h // 2) + 1
1124
+
1125
+ y = 4
1126
+ for i in range(len(ui.hypotheses)):
1127
+ if i < ui.scroll:
1128
+ continue
1129
+ if y >= height - 3:
1130
+ break
1131
+ h = ui.hypotheses[i]
1132
+ selected = (i == ui.sel)
1133
+ prefix = "\033[7m>" if selected else " "
1134
+ out.append(f"\033[{y};1H\033[K{prefix} \033[1mH{i+1}\033[0m: {str(h)[:width-10]}")
1135
+ if selected:
1136
+ out.append("\033[0m")
1137
+ y += 1
1138
+ if selected:
1139
+ for dl in wrap_text(h, width - 6)[1:4]:
1140
+ if y >= height - 3:
1141
+ break
1142
+ out.append(f"\033[{y};5H\033[K\033[90m{dl}\033[0m")
1143
+ y += 1
1144
+ y += 1
1145
+
1146
+ while y < height - 2:
1147
+ out.append(f"\033[{y};1H\033[K")
1148
+ y += 1
1149
+
1150
+ gen = " (generating...)" if ui.generating else ""
1151
+ out.append(f"\033[{height};1H\033[K\033[7m j/k:Nav e:Edit d:Delete +:Add r:Regen Enter:Approve A:Auto q:Quit{gen} \033[0m".ljust(width))
1152
+
1153
+ def render_phase_personas(out, width, height):
1154
+ list_h = height - 6
1155
+ n = len(ui.personas)
1156
+
1157
+ if ui.mode == 'edit':
1158
+ out.append(f"\033[3;1H\033[33mEditing {ui.input_target}:\033[0m")
1159
+ out.append(f"\033[4;1H\033[7m {ui.input_buf} \033[0m")
1160
+ out.append(f"\033[{height};1H\033[K\033[7m Enter:Save Esc:Cancel \033[0m".ljust(width))
1161
+ return
352
1162
 
353
- # Launch interactive browser automatically
354
- if gold_insights or cliff_warnings or all_insights:
355
- print(colored("\nLaunching results browser...", "cyan"))
356
- research_tui_browser(alicanto_result)
1163
+ out.append(f"\033[3;1H\033[1m Personas ({n}) \033[90m{'─' * (width - 20)}\033[0m")
1164
+
1165
+ if ui.sel < ui.scroll:
1166
+ ui.scroll = ui.sel
1167
+ elif ui.sel >= ui.scroll + (list_h // 5):
1168
+ ui.scroll = ui.sel - (list_h // 5) + 1
1169
+
1170
+ y = 4
1171
+ for i in range(len(ui.personas)):
1172
+ if i < ui.scroll:
1173
+ continue
1174
+ if y >= height - 3:
1175
+ break
1176
+ p = ui.personas[i]
1177
+ selected = (i == ui.sel)
1178
+ prefix = "\033[7m>" if selected else " "
1179
+ hyp_idx = p.get('hypothesis_idx', 0)
1180
+ hyp_text = ui.hypotheses[hyp_idx][:60] if hyp_idx < len(ui.hypotheses) else "?"
1181
+
1182
+ out.append(f"\033[{y};1H\033[K{prefix} \033[1m{p['name']}\033[0m \033[90m(b.{p['birth_year']}, {p.get('location', '?')})\033[0m")
1183
+ y += 1
1184
+ if selected and y < height - 3:
1185
+ out.append(f"\033[{y};5H\033[K\033[90mLeader: {p.get('leader', '?')}\033[0m")
1186
+ y += 1
1187
+ interests = p.get('interests', [])
1188
+ if isinstance(interests, list):
1189
+ interests = ', '.join(interests)
1190
+ if y < height - 3:
1191
+ out.append(f"\033[{y};5H\033[K\033[90mInterests: {str(interests)[:width-20]}\033[0m")
1192
+ y += 1
1193
+ if y < height - 3:
1194
+ out.append(f"\033[{y};5H\033[K\033[90mWorldview: {p.get('worldview', '')[:width-20]}\033[0m")
1195
+ y += 1
1196
+ if y < height - 3:
1197
+ out.append(f"\033[{y};5H\033[K\033[36mHypothesis: {hyp_text}\033[0m")
1198
+ y += 1
1199
+ y += 1
1200
+
1201
+ while y < height - 2:
1202
+ out.append(f"\033[{y};1H\033[K")
1203
+ y += 1
1204
+
1205
+ gen = " (generating...)" if ui.generating else ""
1206
+ out.append(f"\033[{height};1H\033[K\033[7m j/k:Nav e:Edit r:Regen s:Swap hyp Enter:Launch A:Auto q:Quit{gen} \033[0m".ljust(width))
1207
+
1208
+ def render_phase_execution(out, width, height):
1209
+ left_w = max(30, width // 3)
1210
+ right_w = width - left_w - 1
1211
+ panel_h = height - 4
1212
+
1213
+ out.append(f"\033[3;1H\033[36;1m Agent Info \033[90m{'─' * (left_w - 13)}\033[0m")
1214
+
1215
+ if 0 <= ui.current_agent < len(ui.personas):
1216
+ p = ui.personas[ui.current_agent]
1217
+ hi = p.get('hypothesis_idx', 0)
1218
+ hyp = ui.hypotheses[hi] if hi < len(ui.hypotheses) else "?"
1219
+ y = 4
1220
+ out.append(f"\033[{y};2H\033[1m{p['name']}\033[0m"); y += 1
1221
+ out.append(f"\033[{y};2H\033[90mb.{p['birth_year']}, {p.get('location', '?')[:left_w-10]}\033[0m"); y += 1
1222
+ out.append(f"\033[{y};2H\033[90mLeader: {p.get('leader', '?')[:left_w-12]}\033[0m"); y += 1
1223
+ interests = p.get('interests', [])
1224
+ if isinstance(interests, list):
1225
+ interests = ', '.join(interests)
1226
+ out.append(f"\033[{y};2H\033[90m{str(interests)[:left_w-4]}\033[0m"); y += 1
1227
+ y += 1
1228
+ out.append(f"\033[{y};2H\033[36mHypothesis:\033[0m"); y += 1
1229
+ for hl in wrap_text(str(hyp), left_w - 4)[:3]:
1230
+ out.append(f"\033[{y};3H{hl[:left_w-4]}"); y += 1
1231
+ y += 1
1232
+ done = sum(1 for t in ui.traces if t)
1233
+ out.append(f"\033[{y};2H\033[90mAgent {ui.current_agent+1}/{len(ui.hypotheses)}\033[0m"); y += 1
1234
+ out.append(f"\033[{y};2H\033[90mDone: {done}\033[0m")
1235
+ else:
1236
+ out.append(f"\033[4;2H\033[90mWaiting...\033[0m")
1237
+
1238
+ out.append(f"\033[3;{left_w+1}H\033[33;1m Live Output \033[90m{'─' * (right_w - 14)}\033[0m")
1239
+
1240
+ log_h = panel_h - 1
1241
+ total = len(ui.exec_log)
1242
+ visible_start = max(0, total - log_h - ui.exec_scroll)
1243
+
1244
+ for i in range(log_h):
1245
+ idx = visible_start + i
1246
+ row = 4 + i
1247
+ out.append(f"\033[{row};{left_w+1}H\033[K")
1248
+ if 0 <= idx < total:
1249
+ out.append(f"\033[{row};{left_w+2}H{ui.exec_log[idx][:right_w-2]}")
1250
+
1251
+ pause_text = " \033[31m[PAUSED]\033[0m" if ui.exec_paused else ""
1252
+ done_count = len(ui.traces)
1253
+ cycle_text = f"[Cycle {ui.current_cycle}/{ui.total_cycles}] " if ui.current_cycle > 0 else ""
1254
+ progress = f"{cycle_text}[Agent {ui.current_agent+1}/{len(ui.hypotheses)}] [Done: {done_count}]"
1255
+ out.append(f"\033[{height-1};1H\033[K\033[90m{progress}\033[0m")
1256
+ out.append(f"\033[{height};1H\033[K\033[7m s:Skip p:Pause x:End cycles j/k:Scroll q:Quit{pause_text} \033[0m".ljust(width))
1257
+
1258
+ def render_phase_review(out, width, height):
1259
+ tabs = ['Agents', 'Insights', 'Files', 'Paper']
1260
+ tab_bar = " "
1261
+ for i, tab in enumerate(tabs):
1262
+ if i == ui.review_tab:
1263
+ tab_bar += f'\033[43;30;1m {tab} \033[0m '
1264
+ else:
1265
+ tab_bar += f'\033[90m {tab} \033[0m '
1266
+ out.append(f"\033[3;1H{tab_bar}")
1267
+ out.append(f"\033[4;1H\033[90m{'─' * width}\033[0m")
1268
+
1269
+ content_h = height - 7
1270
+ content_lines = []
1271
+
1272
+ if ui.review_tab == 0:
1273
+ for i, trace in enumerate(ui.traces):
1274
+ sel = (i == ui.sel)
1275
+ prefix = "\033[7m>" if sel else " "
1276
+ status = "\033[32mSUCCESS\033[0m" if trace.was_successful else "\033[90mno files\033[0m"
1277
+ content_lines.append(f"{prefix} \033[1m{trace.agent_name}\033[0m [{status}] Steps: {len(trace.steps)} Files: {len(trace.final_files)}")
1278
+ if sel:
1279
+ content_lines.append(f" \033[36mHypothesis: {trace.hypothesis[:width-20]}\033[0m")
1280
+ content_lines.append(f" \033[90mPersona: {trace.agent_persona[:width-14]}\033[0m")
1281
+ if trace.final_files:
1282
+ content_lines.append(f" \033[90mFiles: {', '.join(trace.final_files.keys())}\033[0m")
1283
+ content_lines.append("")
1284
+
1285
+ elif ui.review_tab == 1:
1286
+ for i, insight in enumerate(ui.all_insights):
1287
+ sel = (i == ui.sel)
1288
+ starred = "★ " if i in ui.starred else " "
1289
+ prefix = "\033[7m>" if sel else " "
1290
+ star_color = "\033[33m" if i in ui.starred else ""
1291
+ content_lines.append(f"{prefix}{star_color}{starred}{insight[:width-8]}\033[0m")
1292
+
1293
+ elif ui.review_tab == 2:
1294
+ if not ui.all_files:
1295
+ content_lines.append(" No files created during research.")
1296
+ for i, finfo in enumerate(ui.all_files):
1297
+ sel = (i == ui.sel)
1298
+ prefix = "\033[7m>" if sel else " "
1299
+ content_lines.append(f"{prefix} \033[1m{finfo['name']}\033[0m \033[90m(by {finfo['agent']})\033[0m")
1300
+ if sel and finfo.get('content'):
1301
+ for pl in wrap_text(finfo['content'], width - 8)[:6]:
1302
+ content_lines.append(f" \033[90m{pl}\033[0m")
1303
+ content_lines.append("")
1304
+
1305
+ elif ui.review_tab == 3:
1306
+ if not ui.paper:
1307
+ content_lines.append(" No paper generated yet.")
1308
+ else:
1309
+ content_lines = wrap_text(ui.paper, width - 4)
1310
+
1311
+ if ui.scroll > max(0, len(content_lines) - 1):
1312
+ ui.scroll = max(0, len(content_lines) - 1)
1313
+
1314
+ for i in range(content_h):
1315
+ idx = ui.scroll + i
1316
+ row = 5 + i
1317
+ out.append(f"\033[{row};1H\033[K")
1318
+ if idx < len(content_lines):
1319
+ out.append(f"\033[{row};2H{content_lines[idx][:width-2]}")
1320
+
1321
+ if len(content_lines) > content_h and content_h > 0:
1322
+ pct = ui.scroll / max(1, len(content_lines) - content_h)
1323
+ pos = int(pct * (content_h - 1))
1324
+ out.append(f"\033[{5+pos};{width}H\033[33m▐\033[0m")
1325
+
1326
+ gen = " (generating...)" if ui.generating else ""
1327
+ out.append(f"\033[{height};1H\033[K\033[7m Tab/h/l:Tabs j/k:Nav s:Star Enter:View q:Quit{gen} \033[0m".ljust(width))
1328
+
1329
+ # ========== Input Handling ==========
1330
+ def handle_input(c, fd):
1331
+ if ui.mode == 'edit':
1332
+ return handle_edit_input(c, fd)
1333
+
1334
+ if c == '\x1b':
1335
+ if _sel.select([fd], [], [], 0.05)[0]:
1336
+ c2 = os.read(fd, 1).decode('latin-1')
1337
+ if c2 == '[':
1338
+ c3 = os.read(fd, 1).decode('latin-1')
1339
+ if c3 == 'A': nav_up()
1340
+ elif c3 == 'B': nav_down()
1341
+ elif c3 == 'C': nav_right()
1342
+ elif c3 == 'D': nav_left()
1343
+ else:
1344
+ return False
1345
+ return True
1346
+
1347
+ if c == 'q' or c == '\x03':
1348
+ return False
1349
+
1350
+ if c == 'A' and ui.phase < 3:
1351
+ ui.auto_mode = True
1352
+ ui.status = "Auto mode enabled"
1353
+ advance_phase()
1354
+ return True
1355
+
1356
+ if ui.phase == 0:
1357
+ return handle_phase0(c, fd)
1358
+ elif ui.phase == 1:
1359
+ return handle_phase1(c, fd)
1360
+ elif ui.phase == 2:
1361
+ return handle_phase2(c, fd)
1362
+ elif ui.phase == 3:
1363
+ return handle_phase3(c, fd)
1364
+ elif ui.phase == 4:
1365
+ return handle_phase4(c, fd)
1366
+ return True
1367
+
1368
+ def handle_edit_input(c, fd):
1369
+ if c == '\x1b':
1370
+ if _sel.select([fd], [], [], 0.05)[0]:
1371
+ os.read(fd, 2)
1372
+ ui.mode = 'normal'
1373
+ ui.input_buf = ""
1374
+ return True
1375
+ if c in ('\r', '\n'):
1376
+ save_edit()
1377
+ ui.mode = 'normal'
1378
+ return True
1379
+ if c == '\x7f' or c == '\x08':
1380
+ ui.input_buf = ui.input_buf[:-1]
1381
+ return True
1382
+ if c >= ' ' and c <= '~':
1383
+ ui.input_buf += c
1384
+ return True
1385
+
1386
+ def save_edit():
1387
+ buf = ui.input_buf.strip()
1388
+ if not buf:
1389
+ return
1390
+ target = ui.input_target
1391
+ if target.startswith("hyp:"):
1392
+ idx = int(target.split(":")[1])
1393
+ if idx < len(ui.hypotheses):
1394
+ ui.hypotheses[idx] = buf
1395
+ elif target == "new_hyp":
1396
+ ui.hypotheses.append(buf)
1397
+ ui.sel = len(ui.hypotheses) - 1
1398
+ elif target.startswith("persona_name:"):
1399
+ idx = int(target.split(":")[1])
1400
+ if idx < len(ui.personas):
1401
+ ui.personas[idx]['name'] = buf
1402
+
1403
+ def nav_up():
1404
+ if ui.sel > 0:
1405
+ ui.sel -= 1
1406
+ def nav_down():
1407
+ mx = get_max_sel()
1408
+ if ui.sel < mx:
1409
+ ui.sel += 1
1410
+ def nav_right():
1411
+ if ui.phase == 4:
1412
+ ui.review_tab = (ui.review_tab + 1) % 4
1413
+ ui.sel = 0; ui.scroll = 0
1414
+ def nav_left():
1415
+ if ui.phase == 4:
1416
+ ui.review_tab = (ui.review_tab - 1) % 4
1417
+ ui.sel = 0; ui.scroll = 0
1418
+
1419
+ def get_max_sel():
1420
+ if ui.phase == 1: return max(0, len(ui.hypotheses) - 1)
1421
+ elif ui.phase == 2: return max(0, len(ui.personas) - 1)
1422
+ elif ui.phase == 4:
1423
+ if ui.review_tab == 0: return max(0, len(ui.traces) - 1)
1424
+ elif ui.review_tab == 1: return max(0, len(ui.all_insights) - 1)
1425
+ elif ui.review_tab == 2: return max(0, len(ui.all_files) - 1)
1426
+ return 0
1427
+
1428
+ def handle_phase0(c, fd):
1429
+ if c in ('\r', '\n'):
1430
+ advance_phase()
1431
+ return True
1432
+
1433
+ def handle_phase1(c, fd):
1434
+ if c == 'j': nav_down()
1435
+ elif c == 'k': nav_up()
1436
+ elif c == 'e' and not ui.generating and ui.hypotheses:
1437
+ ui.mode = 'edit'
1438
+ ui.input_target = f"hyp:{ui.sel}"
1439
+ ui.input_buf = ui.hypotheses[ui.sel] if ui.sel < len(ui.hypotheses) else ""
1440
+ elif c == 'd' and not ui.generating and len(ui.hypotheses) > 1:
1441
+ del ui.hypotheses[ui.sel]
1442
+ ui.sel = clamp(ui.sel, 0, len(ui.hypotheses) - 1)
1443
+ elif c == '+' and not ui.generating:
1444
+ ui.mode = 'edit'
1445
+ ui.input_target = "new_hyp"
1446
+ ui.input_buf = ""
1447
+ elif c == 'r' and not ui.generating:
1448
+ threading.Thread(target=do_generate_hypotheses, daemon=True).start()
1449
+ elif c in ('\r', '\n') and not ui.generating:
1450
+ advance_phase()
1451
+ return True
1452
+
1453
+ def handle_phase2(c, fd):
1454
+ if c == 'j': nav_down()
1455
+ elif c == 'k': nav_up()
1456
+ elif c == 'e' and not ui.generating and ui.personas:
1457
+ ui.mode = 'edit'
1458
+ ui.input_target = f"persona_name:{ui.sel}"
1459
+ ui.input_buf = ui.personas[ui.sel]['name'] if ui.sel < len(ui.personas) else ""
1460
+ elif c == 'r' and not ui.generating:
1461
+ threading.Thread(target=do_generate_personas, daemon=True).start()
1462
+ elif c == 's' and not ui.generating and ui.personas:
1463
+ p = ui.personas[ui.sel]
1464
+ p['hypothesis_idx'] = (p['hypothesis_idx'] + 1) % len(ui.hypotheses)
1465
+ elif c in ('\r', '\n') and not ui.generating:
1466
+ advance_phase()
1467
+ return True
1468
+
1469
+ def handle_phase3(c, fd):
1470
+ if c == 's':
1471
+ ui.exec_skip = True
1472
+ ui.agent_ui['skip'] = True
1473
+ elif c == 'p':
1474
+ ui.exec_paused = not ui.exec_paused
1475
+ ui.agent_ui['paused'] = ui.exec_paused
1476
+ ui.status = "Paused" if ui.exec_paused else "Resumed"
1477
+ elif c == 'x':
1478
+ ui.end_cycles = True
1479
+ ui.status = "Ending review cycles after current operation..."
1480
+ elif c == 'j':
1481
+ ui.exec_scroll = max(0, ui.exec_scroll - 1)
1482
+ elif c == 'k':
1483
+ ui.exec_scroll += 1
1484
+ return True
1485
+
1486
+ def handle_phase4(c, fd):
1487
+ if c == 'j': nav_down()
1488
+ elif c == 'k': nav_up()
1489
+ elif c == '\t' or c == 'l': nav_right()
1490
+ elif c == 'h': nav_left()
1491
+ elif c == 's' and ui.review_tab == 1 and ui.sel < len(ui.all_insights):
1492
+ if ui.sel in ui.starred: ui.starred.discard(ui.sel)
1493
+ else: ui.starred.add(ui.sel)
1494
+ elif c in ('\r', '\n'):
1495
+ show_full_item(fd)
1496
+ return True
1497
+
1498
+ def show_full_item(fd):
1499
+ content = ""
1500
+ if ui.review_tab == 0 and ui.sel < len(ui.traces):
1501
+ t = ui.traces[ui.sel]
1502
+ content = f"Agent: {t.agent_name}\nHypothesis: {t.hypothesis}\nSuccess: {t.was_successful}\nPersona: {t.agent_persona}\n\n"
1503
+ for s in t.steps:
1504
+ content += f"--- Step {s.step} ---\nThought: {s.thought[:500]}\nAction: {s.action[:300]}\nOutcome: {s.outcome[:300]}\n\n"
1505
+ if t.final_files:
1506
+ content += f"Files: {', '.join(t.final_files.keys())}\n"
1507
+ elif ui.review_tab == 1 and ui.sel < len(ui.all_insights):
1508
+ content = ui.all_insights[ui.sel]
1509
+ elif ui.review_tab == 2 and ui.sel < len(ui.all_files):
1510
+ f = ui.all_files[ui.sel]
1511
+ content = f"File: {f['name']}\nAgent: {f['agent']}\n\n{f.get('content', 'N/A')}"
1512
+ elif ui.review_tab == 3:
1513
+ content = ui.paper
1514
+
1515
+ if not content:
1516
+ return
1517
+
1518
+ width, height = get_size()
1519
+ lines = wrap_text(content, width - 4)
1520
+ scroll = 0
1521
+
1522
+ sys.stdout.write('\033[2J\033[H')
1523
+ while True:
1524
+ sys.stdout.write('\033[H')
1525
+ for i in range(height - 2):
1526
+ idx = scroll + i
1527
+ sys.stdout.write(f'\033[{i+1};1H\033[K')
1528
+ if idx < len(lines):
1529
+ sys.stdout.write(f' {lines[idx][:width-4]}')
1530
+ sys.stdout.write(f'\033[{height};1H\033[K\033[7m j/k:Scroll q/Enter:Back [{scroll+1}-{min(scroll+height-2, len(lines))}/{len(lines)}] \033[0m')
1531
+ sys.stdout.flush()
1532
+
1533
+ c = os.read(fd, 1).decode('latin-1')
1534
+ if c in ('q', '\r', '\n'):
1535
+ break
1536
+ if c == '\x1b':
1537
+ if _sel.select([fd], [], [], 0.05)[0]:
1538
+ c2 = os.read(fd, 1).decode('latin-1')
1539
+ if c2 == '[':
1540
+ c3 = os.read(fd, 1).decode('latin-1')
1541
+ if c3 == 'A': scroll = max(0, scroll - 1)
1542
+ elif c3 == 'B': scroll = min(max(0, len(lines) - (height - 2)), scroll + 1)
1543
+ else:
1544
+ break
1545
+ elif c == 'k': scroll = max(0, scroll - 1)
1546
+ elif c == 'j': scroll = min(max(0, len(lines) - (height - 2)), scroll + 1)
1547
+
1548
+ def advance_phase():
1549
+ if ui.phase == 0:
1550
+ ui.phase = 1; ui.sel = 0; ui.scroll = 0; ui.error = ""
1551
+ threading.Thread(target=do_generate_hypotheses, daemon=True).start()
1552
+ if ui.auto_mode:
1553
+ threading.Thread(target=_auto_wait_then_advance, args=(1,), daemon=True).start()
1554
+ elif ui.phase == 1:
1555
+ ui.phase = 2; ui.sel = 0; ui.scroll = 0; ui.error = ""
1556
+ threading.Thread(target=do_generate_personas, daemon=True).start()
1557
+ if ui.auto_mode:
1558
+ threading.Thread(target=_auto_wait_then_advance, args=(2,), daemon=True).start()
1559
+ elif ui.phase == 2:
1560
+ ui.phase = 3; ui.sel = 0; ui.scroll = 0; ui.error = ""
1561
+ threading.Thread(target=do_run_all_agents, daemon=True).start()
1562
+
1563
+ def _auto_wait_then_advance(expected_phase):
1564
+ while ui.generating:
1565
+ time.sleep(0.3)
1566
+ if ui.phase == expected_phase and ui.auto_mode:
1567
+ time.sleep(0.5)
1568
+ advance_phase()
1569
+
1570
+ # ========== Main TUI Loop ==========
1571
+ if not sys.stdin.isatty():
1572
+ os.chdir(_orig_cwd)
1573
+ context['output'] = "Alicanto requires an interactive terminal."
1574
+ context['messages'] = messages
1575
+ exit()
1576
+
1577
+ ui.phase = 0
1578
+ ui.status = "Press Enter to begin, A for auto mode"
1579
+
1580
+ fd = sys.stdin.fileno()
1581
+ old_settings = termios.tcgetattr(fd)
1582
+
1583
+ try:
1584
+ tty.setcbreak(fd)
1585
+ sys.stdout.write('\033[?25l')
1586
+ sys.stdout.flush()
1587
+ render_screen()
1588
+
1589
+ running = True
1590
+ while running:
1591
+ if ui.generating:
1592
+ if _sel.select([fd], [], [], 0.3)[0]:
1593
+ c = os.read(fd, 1).decode('latin-1')
1594
+ running = handle_input(c, fd)
1595
+ else:
1596
+ if _sel.select([fd], [], [], 0.5)[0]:
1597
+ c = os.read(fd, 1).decode('latin-1')
1598
+ running = handle_input(c, fd)
1599
+ # Sync agent_ui state
1600
+ ui.agent_ui['skip'] = ui.exec_skip
1601
+ ui.agent_ui['paused'] = ui.exec_paused
1602
+ render_screen()
1603
+
1604
+ finally:
1605
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
1606
+ sys.stdout.write('\033[?25h')
1607
+ sys.stdout.write('\033[2J\033[H')
1608
+ sys.stdout.flush()
1609
+ os.chdir(_orig_cwd)
1610
+
1611
+ if ui.traces:
1612
+ print("\033[1;36m=== ALICANTO RESEARCH COMPLETE ===\033[0m\n")
1613
+ print(f"Query: {ui.query}")
1614
+ print(f"Agents: {len(ui.traces)}")
1615
+ for i, trace in enumerate(ui.traces):
1616
+ status = "SUCCESS" if trace.was_successful else "no files"
1617
+ print(f"\n{i+1}. \033[1m{trace.agent_name}\033[0m [{status}]")
1618
+ print(f" Hypothesis: {trace.hypothesis[:100]}")
1619
+ print(f" Steps: {len(trace.steps)} Files: {list(trace.final_files.keys())}")
1620
+ if ui.paper:
1621
+ print(f"\n\033[1;32m--- Paper ---\033[0m")
1622
+ print(ui.paper[:2000])
1623
+ print(f"\n\033[90mOutput directory: {_work_dir}\033[0m")
1624
+
1625
+ context['output'] = ui.paper or "Alicanto session ended."
1626
+ context['messages'] = messages
1627
+ context['alicanto_result'] = {
1628
+ 'query': ui.query,
1629
+ 'hypotheses': ui.hypotheses,
1630
+ 'personas': [{'name': p.get('name'), 'birth_year': p.get('birth_year'), 'persona_text': p.get('persona_text')} for p in ui.personas],
1631
+ 'traces': [{'agent': t.agent_name, 'hypothesis': t.hypothesis, 'success': t.was_successful, 'steps': len(t.steps), 'files': list(t.final_files.keys())} for t in ui.traces],
1632
+ 'paper': ui.paper,
1633
+ }