@nosserb/prmt 0.0.0-stage → 2.0.0

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 (166) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +191 -2
  3. package/bin/prmt.mjs +174 -0
  4. package/dist/assets/AuditModule-BOO_iRy8.js +1 -0
  5. package/dist/assets/Audits-yUBm0EpW.js +1 -0
  6. package/dist/assets/BrainPage-FIWO_9Nc.js +3 -0
  7. package/dist/assets/BrainPage-XQCEGRc5.css +1 -0
  8. package/dist/assets/Carte-CPuiR1Ha.css +1 -0
  9. package/dist/assets/Carte-xycu1mQn.js +1 -0
  10. package/dist/assets/Dashboard-DKvCQcX8.js +2 -0
  11. package/dist/assets/Dashboard-XdhC6_hM.css +1 -0
  12. package/dist/assets/Field-C7c2mCbb.js +2 -0
  13. package/dist/assets/Journal-BLIIlh80.css +1 -0
  14. package/dist/assets/Journal-BaelFjwV.js +1 -0
  15. package/dist/assets/Landing-B2jxV3zs.css +1 -0
  16. package/dist/assets/Landing-Dr612Zdu.js +26 -0
  17. package/dist/assets/Register-DA_Aq5f8.js +1 -0
  18. package/dist/assets/Reports-8wfDNSxH.js +1 -0
  19. package/dist/assets/Reports-B8GXN95N.css +1 -0
  20. package/dist/assets/SavoirPage-BTxbH3LY.css +1 -0
  21. package/dist/assets/SavoirPage-Dp3eUCHp.js +1 -0
  22. package/dist/assets/SettingsPage-BElIP46c.css +1 -0
  23. package/dist/assets/SettingsPage-BiQbnw3o.js +1 -0
  24. package/dist/assets/Shell-BAyHVljp.css +1 -0
  25. package/dist/assets/Shell-CQRlx9-V.js +2 -0
  26. package/dist/assets/Table-DUmGel5e.js +1 -0
  27. package/dist/assets/Tabs-CfhbELZk.js +1 -0
  28. package/dist/assets/Terminal-Bv1IJfi9.js +3 -0
  29. package/dist/assets/Terminal-CEfj38NV.css +1 -0
  30. package/dist/assets/arrow-right-Cx3pAEhl.js +1 -0
  31. package/dist/assets/auditViews-BMza2Sdz.js +1 -0
  32. package/dist/assets/auditViews-BePWj26h.css +1 -0
  33. package/dist/assets/bricolage-grotesque-latin-ext-opsz-normal-IcJDqblK.woff2 +0 -0
  34. package/dist/assets/bricolage-grotesque-latin-opsz-normal-Cre6nC2_.woff2 +0 -0
  35. package/dist/assets/bricolage-grotesque-vietnamese-opsz-normal-D9N6E8K1.woff2 +0 -0
  36. package/dist/assets/charts-BcijSJ4J.js +1 -0
  37. package/dist/assets/circle-stop-C33BAcKW.js +1 -0
  38. package/dist/assets/command-ChA49Xk7.js +1 -0
  39. package/dist/assets/command-Ck4pdQCv.css +1 -0
  40. package/dist/assets/download-CeuKOenD.js +1 -0
  41. package/dist/assets/eye-CXXHBBpr.js +1 -0
  42. package/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  43. package/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  44. package/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  45. package/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  46. package/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  47. package/dist/assets/habillage-B5mMBadn.js +1 -0
  48. package/dist/assets/index-BH1etemR.css +1 -0
  49. package/dist/assets/index-COSiM-R9.js +14 -0
  50. package/dist/assets/jetbrains-mono-cyrillic-ext-wght-normal-EocZY2iu.woff2 +0 -0
  51. package/dist/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  52. package/dist/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  53. package/dist/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  54. package/dist/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  55. package/dist/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  56. package/dist/assets/layers-2x-Bpkbi35X.png +0 -0
  57. package/dist/assets/layers-BWBAp2CZ.png +0 -0
  58. package/dist/assets/markdown-DF87LQh1.css +1 -0
  59. package/dist/assets/markdown-o5sMhPHN.js +4 -0
  60. package/dist/assets/marker-icon-hN30_KVU.png +0 -0
  61. package/dist/assets/play-Daw4K_73.js +1 -0
  62. package/dist/assets/plus-D_QHm8ud.js +1 -0
  63. package/dist/assets/trash-DLpHM-s8.js +1 -0
  64. package/dist/assets/web-6j1-aY67.js +1 -0
  65. package/dist/favicon.svg +1 -0
  66. package/dist/index.html +18 -0
  67. package/docs/ANALYSE.md +87 -0
  68. package/docs/SECURITE.md +188 -0
  69. package/lib/scripts/lier.js +15 -0
  70. package/lib/scripts/searxng.js +91 -0
  71. package/lib/server/app.js +444 -0
  72. package/lib/server/audits/modules/local.js +429 -0
  73. package/lib/server/audits/modules/network.js +331 -0
  74. package/lib/server/audits/modules/web.js +302 -0
  75. package/lib/server/audits/net.js +141 -0
  76. package/lib/server/audits/registry.js +202 -0
  77. package/lib/server/audits/types.js +58 -0
  78. package/lib/server/brain/brain.js +624 -0
  79. package/lib/server/brain/conversations.js +82 -0
  80. package/lib/server/brain/ollama.js +197 -0
  81. package/lib/server/brain/prompt.js +188 -0
  82. package/lib/server/brain/provider.js +1 -0
  83. package/lib/server/cli/bridge.js +292 -0
  84. package/lib/server/cli/pont.js +62 -0
  85. package/lib/server/config.js +68 -0
  86. package/lib/server/engine/analyzer.js +205 -0
  87. package/lib/server/engine/catalog.js +229 -0
  88. package/lib/server/engine/decoder.js +91 -0
  89. package/lib/server/engine/engine.js +194 -0
  90. package/lib/server/engine/paths.js +129 -0
  91. package/lib/server/engine/policy.js +397 -0
  92. package/lib/server/engine/relax.js +10 -0
  93. package/lib/server/engine/runner.js +193 -0
  94. package/lib/server/errors.js +23 -0
  95. package/lib/server/http/guard.js +75 -0
  96. package/lib/server/http/router.js +154 -0
  97. package/lib/server/http/static.js +76 -0
  98. package/lib/server/index.js +40 -0
  99. package/lib/server/journal.js +139 -0
  100. package/lib/server/lien.js +33 -0
  101. package/lib/server/savoir/base.js +284 -0
  102. package/lib/server/savoir/lecture.js +406 -0
  103. package/lib/server/savoir/memoire.js +115 -0
  104. package/lib/server/search/categories.js +179 -0
  105. package/lib/server/search/fiche.js +326 -0
  106. package/lib/server/search/lieux.js +134 -0
  107. package/lib/server/search/overpass.js +216 -0
  108. package/lib/server/search/searxng.js +223 -0
  109. package/lib/server/search/tiles.js +103 -0
  110. package/lib/server/status.js +157 -0
  111. package/lib/server/stores.js +141 -0
  112. package/lib/server/telephone/acces.js +457 -0
  113. package/lib/server/telephone/certificat.js +79 -0
  114. package/lib/server/telephone/qr.js +308 -0
  115. package/lib/shared/analysis.js +40 -0
  116. package/lib/shared/schemas.js +105 -0
  117. package/lib/shared/types.js +10 -0
  118. package/package.json +81 -4
  119. package/searxng/settings.yml +35 -0
  120. package/tui/Modelfile +23 -0
  121. package/tui/README.md +103 -0
  122. package/tui/prmt_tui.py +65 -0
  123. package/tui/promethee/__init__.py +1 -0
  124. package/tui/promethee/__main__.py +4 -0
  125. package/tui/promethee/accueil.py +66 -0
  126. package/tui/promethee/agents.py +220 -0
  127. package/tui/promethee/app/__init__.py +4 -0
  128. package/tui/promethee/app/application.py +299 -0
  129. package/tui/promethee/app/clavier.py +99 -0
  130. package/tui/promethee/app/commandes_slash.py +164 -0
  131. package/tui/promethee/app/contexte.py +188 -0
  132. package/tui/promethee/app/definitions.py +518 -0
  133. package/tui/promethee/app/ecran.py +177 -0
  134. package/tui/promethee/app/envoi.py +141 -0
  135. package/tui/promethee/app/historique.py +373 -0
  136. package/tui/promethee/app/modele.py +108 -0
  137. package/tui/promethee/app/notes.py +63 -0
  138. package/tui/promethee/app/outils.py +589 -0
  139. package/tui/promethee/app/plateforme.py +409 -0
  140. package/tui/promethee/app/saisie.py +100 -0
  141. package/tui/promethee/app/taches.py +92 -0
  142. package/tui/promethee/app/tour.py +539 -0
  143. package/tui/promethee/app/validation.py +210 -0
  144. package/tui/promethee/appels.py +35 -0
  145. package/tui/promethee/commandes.py +154 -0
  146. package/tui/promethee/completion.py +140 -0
  147. package/tui/promethee/config.py +12 -0
  148. package/tui/promethee/consignes.py +278 -0
  149. package/tui/promethee/conversation.py +94 -0
  150. package/tui/promethee/etat.py +253 -0
  151. package/tui/promethee/fichiers.py +354 -0
  152. package/tui/promethee/journal.py +116 -0
  153. package/tui/promethee/logo.py +132 -0
  154. package/tui/promethee/memoire.py +50 -0
  155. package/tui/promethee/ollama.py +52 -0
  156. package/tui/promethee/plan.py +91 -0
  157. package/tui/promethee/politique.py +429 -0
  158. package/tui/promethee/preferences.py +65 -0
  159. package/tui/promethee/presse_papiers.py +44 -0
  160. package/tui/promethee/recherche.py +581 -0
  161. package/tui/promethee/securite.py +204 -0
  162. package/tui/promethee/shell.py +190 -0
  163. package/tui/promethee/textes.py +190 -0
  164. package/tui/promethee/theme.py +92 -0
  165. package/tui/promethee/widgets.py +493 -0
  166. package/tui/requirements.txt +1 -0
@@ -0,0 +1,177 @@
1
+ """Barre d'état, spinner et affichage dans la conversation."""
2
+ from __future__ import annotations
3
+
4
+ import random
5
+ import time
6
+
7
+ from rich.text import Text
8
+ from textual.widgets import Markdown, Static
9
+
10
+ from ..accueil import Accueil
11
+ from ..textes import SPINNER, VERBS
12
+ from ..theme import AGENT_COLOR, CODE_COLOR, DIM, MEMORY_COLOR, ORANGE, PLAN_COLOR, RED, SHELL_COLOR, YELLOW
13
+ from ..widgets import AgentView, AssistantMessage, SearchView, ToolCallView
14
+
15
+
16
+ class Ecran:
17
+ """Partie de PrometheeApp : barre d'état, spinner et affichage dans la conversation."""
18
+
19
+ # ------------------------------------------------------------------ barre d'état
20
+ def set_hints(self, text: str | None = None, warn: bool = False) -> None:
21
+ hints = self.query_one("#hints", Static)
22
+ if text:
23
+ hints.update(Text(text, style=f"bold {YELLOW}" if warn else DIM))
24
+ elif self._perm is not None:
25
+ hints.update(Text(self._perm.hint, style=DIM))
26
+ elif self._comp_items:
27
+ what = "lancer" if self._comp_kind == "/" else "joindre"
28
+ hints.update(Text(f"↑↓ choisir · Tab compléter · Entrée {what} · Esc fermer", style=DIM))
29
+ elif self.input.text.startswith("!"):
30
+ hints.update(Text("! mode commande : une lecture part directement, le reste te demande une confirmation",
31
+ style=SHELL_COLOR))
32
+ elif self._agent_draft is not None:
33
+ what = ("Nouvel agent : décris son rôle et quand l'utiliser" if self._draft_kind == "agent"
34
+ else "Nouvelle commande : décris ce qu'elle doit faire")
35
+ hints.update(Text(f"{what}, puis Entrée · Esc annuler", style=AGENT_COLOR))
36
+ elif self.input.text.startswith("#"):
37
+ hints.update(Text("# mode mémoire : Entrée enregistre cette note (mémoire globale ou du dossier)",
38
+ style=MEMORY_COLOR))
39
+ elif self.plan_mode:
40
+ hints.update(Text("⏸ mode plan : Prométhée propose un plan sans rien exécuter · Shift+Tab pour quitter",
41
+ style=PLAN_COLOR))
42
+ else:
43
+ hints.update(Text("/aide · Shift+Tab plan · Esc interrompre · Ctrl+C ×2 quitter", style=DIM))
44
+
45
+ def update_status(self) -> None:
46
+ parts = Text(style=DIM)
47
+ if self.generating and self._turn_model and self._turn_model != self.model:
48
+ parts.append(f"{self._turn_model} (code)", style=CODE_COLOR)
49
+ else:
50
+ parts.append(self.model, style=ORANGE)
51
+ if self.model_state:
52
+ parts.append(f" · {self.model_state}")
53
+ elif not self.supports_tools:
54
+ parts.append(" · sans commandes")
55
+ # Politique : « stricte » au repos, « souple » et « validation manuelle » signalées (un mot, pas seulement une couleur)
56
+ politique = self.politique
57
+ relaxed = politique.libelle() != "politique stricte"
58
+ parts.append(" · ")
59
+ parts.append(("▲ " if relaxed else "") + politique.libelle(), style=YELLOW if relaxed else DIM)
60
+ if self.num_ctx:
61
+ pct = min(100, round(100 * self.ctx_used / self.num_ctx))
62
+ style = f"bold {RED}" if pct >= 90 else YELLOW if pct >= 75 else DIM
63
+ parts.append(" · ")
64
+ if self.size.width >= 132:
65
+ filled = min(8, round(8 * pct / 100))
66
+ parts.append("▰" * filled, style=RED if pct >= 90 else YELLOW if pct >= 75 else ORANGE)
67
+ parts.append("▱" * (8 - filled) + " ", style=DIM)
68
+ parts.append(f"contexte {pct} %", style=style)
69
+ if pct >= 90:
70
+ parts.append(" (/compact conseillé)", style=f"bold {RED}")
71
+ if self.last_tps:
72
+ parts.append(f" · {self.last_tps:.0f} tok/s")
73
+ self.query_one("#status", Static).update(parts)
74
+ for panel in self.query(Accueil):
75
+ panel.rafraichir() # l'accueil montre le même état : modèle, politique
76
+
77
+ def _tick_spinner(self) -> None:
78
+ if not self.generating or self._perm is not None:
79
+ return
80
+ self._follow()
81
+ now = time.monotonic()
82
+ frame = SPINNER[int(now * 8) % len(SPINNER)]
83
+ elapsed = int(now - self.spin_start)
84
+ if self.spin_mode == "command":
85
+ verb, detail, stop = "Exécution de la commande", f"{elapsed}s", "Esc pour l'arrêter"
86
+ elif self.spin_mode == "check":
87
+ verb, detail, stop = self.spin_verb, f"{elapsed}s", "Esc pour interrompre"
88
+ elif self.spin_mode == "agent":
89
+ verb = self.spin_verb
90
+ detail = f"{elapsed}s" + (f" · ↓ {self.spin_tokens} tokens" if self.spin_tokens else "")
91
+ stop = "Esc pour interrompre"
92
+ else:
93
+ verb = "Réflexion" if self.spin_thinking and not self.spin_tokens else self.spin_verb
94
+ detail = f"{elapsed}s" + (f" · ↓ {self.spin_tokens} tokens" if self.spin_tokens else "")
95
+ stop = "Esc pour interrompre"
96
+ self.query_one("#spinner", Static).update(
97
+ Text.assemble((f"{frame} ", ORANGE), (f"{verb}…", f"bold {ORANGE}"), (f" ({detail} · {stop})", DIM))
98
+ )
99
+
100
+ def _spinner(self, active: bool, mode: str | None = None, verb: str = "") -> None:
101
+ if mode:
102
+ self.spin_mode = mode
103
+ self.spin_start = time.monotonic()
104
+ if mode == "model":
105
+ self.spin_tokens = 0
106
+ self.spin_thinking = False
107
+ self.spin_verb = random.choice(VERBS)
108
+ elif mode == "check":
109
+ self.spin_verb = verb or "Vérification"
110
+ elif mode == "agent":
111
+ self.spin_tokens = 0
112
+ self.spin_verb = verb or "Agent au travail"
113
+ spinner = self.query_one("#spinner", Static)
114
+ spinner.set_class(active, "-active")
115
+ if active:
116
+ self._tick_spinner()
117
+
118
+ # ------------------------------------------------------------------ affichage
119
+ @staticmethod
120
+ def user_widget(text: str) -> Static:
121
+ lines = text.splitlines() or [text]
122
+ shown = lines if len(lines) <= 12 else lines[:8]
123
+ body = Text()
124
+ for i, line in enumerate(shown):
125
+ body.append("> " if i == 0 else " ", style=f"bold {DIM}")
126
+ body.append(line + ("\n" if i < len(shown) - 1 else ""), style=DIM)
127
+ if len(lines) > len(shown):
128
+ body.append(f"\n … [+{len(lines) - len(shown)} lignes]", style=f"italic {DIM}")
129
+ return Static(body, classes="user")
130
+
131
+ def add_user(self, text: str) -> None:
132
+ self.chat.mount(self.user_widget(text))
133
+ self.call_after_refresh(self._follow)
134
+
135
+ def add_notice(self, text: str, style: str = DIM) -> None:
136
+ self.chat.mount(Static(Text.assemble(("⎿ ", DIM), (text, style)), classes="notice"))
137
+ self.call_after_refresh(self._follow)
138
+
139
+ def add_notice_md(self, markdown: str) -> None:
140
+ self.chat.mount(Markdown(markdown, classes="notice-md"))
141
+ self.call_after_refresh(self._follow)
142
+ self.set_timer(0.2, self._follow) # un tableau Markdown s'affiche un peu après : suivre sa vraie hauteur
143
+
144
+ async def _new_bubble(self, text: str = "") -> AssistantMessage:
145
+ bubble = AssistantMessage(text)
146
+ await self.chat.mount(bubble)
147
+ self.call_after_refresh(self._follow)
148
+ return bubble
149
+
150
+ async def _new_tool_view(self, command: str, label: str = "PowerShell",
151
+ waiting: str = "En attente de ta validation…") -> ToolCallView:
152
+ view = ToolCallView(command, label, waiting)
153
+ # Pendant le travail d'un agent, ses outils s'affichent en retrait dans son cadre
154
+ await (self._tool_parent or self.chat).mount(view)
155
+ self.call_after_refresh(self._follow)
156
+ return view
157
+
158
+ async def _new_search_view(self, requete: str) -> SearchView:
159
+ view = SearchView(requete)
160
+ await self.chat.mount(view)
161
+ self.call_after_refresh(self._follow)
162
+ return view
163
+
164
+ async def _new_agent_view(self, name: str, task: str) -> AgentView:
165
+ view = AgentView(name, task)
166
+ await self.chat.mount(view)
167
+ self.call_after_refresh(self._follow)
168
+ return view
169
+
170
+ def action_toggle_details(self) -> None:
171
+ """Ctrl+O : afficher ou replier le détail du travail des agents."""
172
+ views = [v for v in self.query(AgentView) if v.done]
173
+ if not views:
174
+ return
175
+ self.show_details = not self.show_details
176
+ for view in views:
177
+ view.set_expanded(self.show_details)
@@ -0,0 +1,141 @@
1
+ """Envoi d'un message, mode « ! », message système et estimation du contexte."""
2
+ from __future__ import annotations
3
+
4
+ import datetime as dt
5
+ import json
6
+ import time
7
+
8
+ from textual import work
9
+
10
+ from ..agents import agent_tool_def, agents_guide, list_agents
11
+ from ..completion import expand_attachments
12
+ from ..consignes import TOOLS, TOOL_GUIDE
13
+ from ..memoire import MEMORY_INTRO, read_memory
14
+ from ..recherche import search_answer_rules
15
+ from ..securite import is_dangerous
16
+ from ..shell import shell_message
17
+ from ..theme import DIM, YELLOW
18
+ from ..widgets import ToolCallView
19
+
20
+
21
+ class Envoi:
22
+ """Partie de PrometheeApp : envoi d'un message, mode « ! », message système et estimation du contexte."""
23
+
24
+ def send_message(self, text: str, content: str | None = None, intent: str | None = None,
25
+ mode: str | None = None) -> None:
26
+ """Envoie un message (text = ce qui s'affiche ; content = ce que reçoit le modèle, si différent ;
27
+ intent = ce que lit l'interpréteur à la place du texte affiché ; mode = « action » imposé)."""
28
+ if self.generating:
29
+ self.notify("Prométhée travaille encore. Esc pour l'interrompre.", severity="warning")
30
+ return
31
+ if self.tasks and all(t["state"] == "done" for t in self.tasks):
32
+ self.tasks = [] # la tâche précédente est finie
33
+ self.add_user(text)
34
+ if not self.session_title:
35
+ self.session_title = " ".join(text.split())[:70]
36
+ expanded, notes = expand_attachments(content or text, self.cwd)
37
+ for shown, summary, ok in notes:
38
+ self.add_notice(f"Lu {shown} ({summary})" if ok else f"@{shown} : {summary}", DIM if ok else YELLOW)
39
+ message = {"role": "user", "content": expanded}
40
+ if expanded != text:
41
+ message["display"] = text # affiché (et utilisé par l'interpréteur) à la place du contenu complet
42
+ if intent:
43
+ message["intent"] = intent # commande personnalisée, /init : l'interpréteur lit la vraie demande
44
+ if mode:
45
+ message["mode"] = mode
46
+ self.messages.append(message)
47
+ self.generating = True
48
+ self.cancel_event.clear()
49
+ self._run_plan = self.plan_mode
50
+ self._spinner(True, "model")
51
+ self.render_tasks()
52
+ self.run_agent(list(self.messages), self.model, self.plan_mode)
53
+
54
+ @work(thread=True, exclusive=True, group="generate")
55
+ def run_shell(self, command: str, view: ToolCallView) -> None:
56
+ """« ! commande » : tapée par toi. Une lecture part tout de suite ; une commande sensible, risquée ou hors
57
+ politique te demande une confirmation ; un refus de la politique vaut aussi pour toi."""
58
+ verdict = self._verdict(command)
59
+ if verdict is not None and verdict.refusee:
60
+ self.call_from_thread(view.set_refused, verdict.resume(), verdict.conseils())
61
+ self.noter_refus(command, verdict, "toi")
62
+ self.call_from_thread(self.finish_shell, command, None, "", "", False)
63
+ return
64
+ if is_dangerous(command) or (verdict is not None and not verdict.lecture):
65
+ decision = self._wait_choice(lambda done: self.show_permission(command, "", done, verdict, direct=True))
66
+ if decision != "yes":
67
+ self.call_from_thread(view.set_denied)
68
+ self.noter_commande(command, None, "", "annulée par toi", time.monotonic(), self.cwd, verdict, "refus", "toi")
69
+ self.call_from_thread(self.finish_shell, command, None, "", "", False)
70
+ return
71
+ self.call_from_thread(view.set_running)
72
+ self.call_from_thread(self._spinner, True, "command")
73
+ started, where = time.monotonic(), self.cwd
74
+ code, output, note = self._execute(command, verdict)
75
+ self.noter_commande(command, code, output, note, started, where, verdict, "toi", "toi")
76
+ self.last_output = output
77
+ self.call_from_thread(view.set_result, code, output, note)
78
+ self.call_from_thread(self.finish_shell, command, code, output, note)
79
+
80
+ def finish_shell(self, command: str, code: int | None, output: str, note: str, ran: bool = True) -> None:
81
+ self.generating = False
82
+ self._spinner(False)
83
+ if self._perm is not None:
84
+ self.hide_permission()
85
+ if not ran:
86
+ return # refusée par la politique ou annulée : rien n'a tourné, rien n'entre dans la conversation
87
+ self.messages.append({"role": "user", "content": shell_message(command, code, output, note)})
88
+ if not self.session_title:
89
+ self.session_title = f"! {command}"[:70]
90
+ self.ctx_used = max(self.ctx_used, self.estimate_tokens())
91
+ self.update_status()
92
+ self.save_session()
93
+ self.call_after_refresh(self._follow)
94
+ self._maybe_auto_compact()
95
+
96
+ def memory_text(self) -> str:
97
+ memory = read_memory(self.cwd)
98
+ if not memory:
99
+ return ""
100
+ return MEMORY_INTRO + "".join(f"\n\n### Mémoire {label}\n{text}" for label, _, text in memory)
101
+
102
+ def _system_extra(self) -> str:
103
+ """Mémoire et résumé de la conversation compactée, ajoutés aux consignes du modèle."""
104
+ extra = self.memory_text()
105
+ if self.summary:
106
+ extra += f"\n\n## Résumé de la conversation précédente (compactée)\n{self.summary}"
107
+ return extra
108
+
109
+ def search_system(self, search: dict) -> dict:
110
+ """Consignes de la réponse qui suit une recherche web, comme la console : celles du modèle sans le guide des
111
+ outils (aucune commande dans ce tour), puis les consignes de la recherche."""
112
+ base = (self.model_system or "") + self._system_extra()
113
+ rules = search_answer_rules(search)
114
+ return {"role": "system", "content": f"{base}\n\n{rules}" if base else rules}
115
+
116
+ def system_message(self) -> dict | None:
117
+ extra = self._system_extra()
118
+ if not self.supports_tools:
119
+ if not extra:
120
+ return None # le SYSTEM du Modelfile s'applique
121
+ return {"role": "system", "content": (self.model_system or "") + extra}
122
+ guide = TOOL_GUIDE.format(cwd=self.cwd, date=dt.date.today().strftime("%d/%m/%Y"))
123
+ agents = list_agents(self.cwd)
124
+ if agents:
125
+ guide += agents_guide(agents)
126
+ return {"role": "system", "content": (self.model_system or "") + guide + extra}
127
+
128
+ def request_tools(self) -> list[dict]:
129
+ """Outils envoyés au modèle principal : commandes, fichiers, et lancer_agent s'il y a des agents."""
130
+ agents = list_agents(self.cwd)
131
+ return TOOLS + ([agent_tool_def(agents)] if agents else [])
132
+
133
+ def estimate_tokens(self) -> int:
134
+ """Estimation de la taille du contexte (≈ 3 caractères par jeton + définitions des outils)."""
135
+ system = self.system_message() or {}
136
+ chars = len(system.get("content", ""))
137
+ for m in self.messages:
138
+ chars += len(str(m.get("content") or ""))
139
+ if m.get("tool_calls"):
140
+ chars += len(json.dumps(m["tool_calls"], ensure_ascii=False))
141
+ return int(chars / 3.0) + (1250 if self.supports_tools else 0)