oats-coder 1.0.2__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 (242) hide show
  1. oats/AGENT.dir.python.tools.json +1 -0
  2. oats/AGENT.python.tools.md +131 -0
  3. oats/agent/AGENT.dir.python.tools.json +1 -0
  4. oats/agent/AGENT.python.tools.md +19 -0
  5. oats/agent/agent.py +176 -0
  6. oats/agent/agent.py.AGENT.python.tools.json +7 -0
  7. oats/agent_get_tool_choices_for_prompt.py +32 -0
  8. oats/agent_get_tool_choices_for_prompt.py.AGENT.python.tools.json +7 -0
  9. oats/call_tool_with_loader1.py +430 -0
  10. oats/call_tool_with_loader1.py.AGENT.python.tools.json +7 -0
  11. oats/cli/AGENT.dir.python.tools.json +1 -0
  12. oats/cli/AGENT.python.tools.md +33 -0
  13. oats/cli/approval.py +154 -0
  14. oats/cli/approval.py.AGENT.python.tools.json +7 -0
  15. oats/cli/check_providers.py +25 -0
  16. oats/cli/check_providers.py.AGENT.python.tools.json +7 -0
  17. oats/cli/interactive.py +550 -0
  18. oats/cli/interactive.py.AGENT.python.tools.json +7 -0
  19. oats/cli/process_message.py +153 -0
  20. oats/cli/process_message.py.AGENT.python.tools.json +7 -0
  21. oats/cli/tui/AGENT.dir.python.tools.json +1 -0
  22. oats/cli/tui/AGENT.python.tools.md +19 -0
  23. oats/cli/tui/tui_banner.py +114 -0
  24. oats/cli/tui/tui_banner.py.AGENT.python.tools.json +7 -0
  25. oats/cli/tui/tui_consts.py +120 -0
  26. oats/cli/tui/tui_consts.py.AGENT.python.tools.json +7 -0
  27. oats/cli/tui/tui_utils.py +492 -0
  28. oats/cli/tui/tui_utils.py.AGENT.python.tools.json +7 -0
  29. oats/config/coder.json +27 -0
  30. oats/core/AGENT.dir.python.tools.json +1 -0
  31. oats/core/AGENT.python.tools.md +117 -0
  32. oats/core/__init__.py +19 -0
  33. oats/core/bus.py +170 -0
  34. oats/core/bus.py.AGENT.python.tools.json +7 -0
  35. oats/core/config.py +270 -0
  36. oats/core/config.py.AGENT.python.tools.json +7 -0
  37. oats/core/features.py +118 -0
  38. oats/core/features.py.AGENT.python.tools.json +7 -0
  39. oats/core/id.py +17 -0
  40. oats/core/id.py.AGENT.python.tools.json +7 -0
  41. oats/core/offline.py +93 -0
  42. oats/core/offline.py.AGENT.python.tools.json +7 -0
  43. oats/core/profiles.py +179 -0
  44. oats/core/profiles.py.AGENT.python.tools.json +7 -0
  45. oats/core/storage.py +207 -0
  46. oats/core/storage.py.AGENT.python.tools.json +7 -0
  47. oats/core/tokens.py +80 -0
  48. oats/core/tokens.py.AGENT.python.tools.json +7 -0
  49. oats/date.py +83 -0
  50. oats/date.py.AGENT.python.tools.json +7 -0
  51. oats/determine_best_tools1.py +311 -0
  52. oats/determine_best_tools1.py.AGENT.python.tools.json +7 -0
  53. oats/get_oat_config.py +12 -0
  54. oats/get_oat_config.py.AGENT.python.tools.json +3 -0
  55. oats/git/AGENT.dir.python.tools.json +1 -0
  56. oats/git/AGENT.python.tools.md +89 -0
  57. oats/git/__init__.py +6 -0
  58. oats/git/build_git_repo_to_dataset.py +185 -0
  59. oats/git/build_git_repo_to_dataset.py.AGENT.python.tools.json +7 -0
  60. oats/git/coauthor.py +75 -0
  61. oats/git/coauthor.py.AGENT.python.tools.json +7 -0
  62. oats/git/git_commit_search1.py +537 -0
  63. oats/git/git_commit_search1.py.AGENT.python.tools.json +7 -0
  64. oats/git/git_diff_extractor.py +221 -0
  65. oats/git/git_diff_extractor.py.AGENT.python.tools.json +7 -0
  66. oats/git/git_to_df_converter.py +115 -0
  67. oats/git/git_to_df_converter.py.AGENT.python.tools.json +7 -0
  68. oats/git/repo_to_parquet.py +81 -0
  69. oats/git/repo_to_parquet.py.AGENT.python.tools.json +7 -0
  70. oats/git/walk_up_dir_path_to_find_git_config.py +56 -0
  71. oats/git/walk_up_dir_path_to_find_git_config.py.AGENT.python.tools.json +7 -0
  72. oats/git/worktree.py +184 -0
  73. oats/git/worktree.py.AGENT.python.tools.json +7 -0
  74. oats/hook/AGENT.dir.python.tools.json +1 -0
  75. oats/hook/AGENT.python.tools.md +19 -0
  76. oats/hook/__init__.py +23 -0
  77. oats/hook/engine.py +225 -0
  78. oats/hook/engine.py.AGENT.python.tools.json +7 -0
  79. oats/load_tools_from_source1.py +594 -0
  80. oats/load_tools_from_source1.py.AGENT.python.tools.json +7 -0
  81. oats/log.py +233 -0
  82. oats/log.py.AGENT.python.tools.json +7 -0
  83. oats/lsp/AGENT.dir.python.tools.json +1 -0
  84. oats/lsp/AGENT.python.tools.md +19 -0
  85. oats/lsp/__init__.py +1 -0
  86. oats/lsp/client.py +381 -0
  87. oats/lsp/client.py.AGENT.python.tools.json +7 -0
  88. oats/mcp/AGENT.dir.python.tools.json +1 -0
  89. oats/mcp/AGENT.python.tools.md +159 -0
  90. oats/mcp/config.py +201 -0
  91. oats/mcp/config.py.AGENT.python.tools.json +7 -0
  92. oats/mcp/example_mcp_config.json +58 -0
  93. oats/mcp/fetch.py +405 -0
  94. oats/mcp/fetch.py.AGENT.python.tools.json +7 -0
  95. oats/mcp/index.py +381 -0
  96. oats/mcp/index.py.AGENT.python.tools.json +7 -0
  97. oats/mcp/intent.py +427 -0
  98. oats/mcp/intent.py.AGENT.python.tools.json +7 -0
  99. oats/mcp/models.py +304 -0
  100. oats/mcp/models.py.AGENT.python.tools.json +7 -0
  101. oats/mcp/orchestrator.py +653 -0
  102. oats/mcp/orchestrator.py.AGENT.python.tools.json +7 -0
  103. oats/mcp/ranking.py +243 -0
  104. oats/mcp/ranking.py.AGENT.python.tools.json +7 -0
  105. oats/mcp/registry.py +567 -0
  106. oats/mcp/registry.py.AGENT.python.tools.json +7 -0
  107. oats/mcp/resolver.py +588 -0
  108. oats/mcp/resolver.py.AGENT.python.tools.json +7 -0
  109. oats/mcp/tools.py +574 -0
  110. oats/mcp/tools.py.AGENT.python.tools.json +7 -0
  111. oats/mcp/tracker.py +254 -0
  112. oats/mcp/tracker.py.AGENT.python.tools.json +7 -0
  113. oats/memory/AGENT.dir.python.tools.json +1 -0
  114. oats/memory/AGENT.python.tools.md +19 -0
  115. oats/memory/__init__.py +14 -0
  116. oats/memory/manager.py +180 -0
  117. oats/memory/manager.py.AGENT.python.tools.json +7 -0
  118. oats/memory/models.py +97 -0
  119. oats/memory/models.py.AGENT.python.tools.json +7 -0
  120. oats/models.py +332 -0
  121. oats/models.py.AGENT.python.tools.json +7 -0
  122. oats/oweb/AGENT.dir.python.tools.json +1 -0
  123. oats/oweb/AGENT.python.tools.md +33 -0
  124. oats/oweb/get_auth.py +56 -0
  125. oats/oweb/get_auth.py.AGENT.python.tools.json +7 -0
  126. oats/oweb/login.py +72 -0
  127. oats/oweb/login.py.AGENT.python.tools.json +7 -0
  128. oats/plugins/AGENT.dir.python.tools.json +1 -0
  129. oats/plugins/AGENT.python.tools.md +33 -0
  130. oats/plugins/__init__.py +24 -0
  131. oats/plugins/loader.py +278 -0
  132. oats/plugins/loader.py.AGENT.python.tools.json +7 -0
  133. oats/plugins/manifest.py +171 -0
  134. oats/plugins/manifest.py.AGENT.python.tools.json +7 -0
  135. oats/pp.py +8 -0
  136. oats/pp.py.AGENT.python.tools.json +7 -0
  137. oats/provider/AGENT.dir.python.tools.json +1 -0
  138. oats/provider/AGENT.python.tools.md +33 -0
  139. oats/provider/models.py +249 -0
  140. oats/provider/models.py.AGENT.python.tools.json +7 -0
  141. oats/provider/provider.py +822 -0
  142. oats/provider/provider.py.AGENT.python.tools.json +7 -0
  143. oats/session/AGENT.dir.python.tools.json +1 -0
  144. oats/session/AGENT.python.tools.md +201 -0
  145. oats/session/__init__.py +35 -0
  146. oats/session/build_system_prompt.py +184 -0
  147. oats/session/build_system_prompt.py.AGENT.python.tools.json +7 -0
  148. oats/session/caveman.py +177 -0
  149. oats/session/caveman.py.AGENT.python.tools.json +7 -0
  150. oats/session/compaction.py +463 -0
  151. oats/session/compaction.py.AGENT.python.tools.json +7 -0
  152. oats/session/debug_trace.py +41 -0
  153. oats/session/debug_trace.py.AGENT.python.tools.json +7 -0
  154. oats/session/file_cache.py +108 -0
  155. oats/session/file_cache.py.AGENT.python.tools.json +7 -0
  156. oats/session/message.py +214 -0
  157. oats/session/message.py.AGENT.python.tools.json +7 -0
  158. oats/session/metrics.py +52 -0
  159. oats/session/metrics.py.AGENT.python.tools.json +7 -0
  160. oats/session/models.py +43 -0
  161. oats/session/models.py.AGENT.python.tools.json +5 -0
  162. oats/session/modes.py +107 -0
  163. oats/session/modes.py.AGENT.python.tools.json +7 -0
  164. oats/session/processor.py +1600 -0
  165. oats/session/processor.py.AGENT.python.tools.json +7 -0
  166. oats/session/screenshot_store.py +157 -0
  167. oats/session/screenshot_store.py.AGENT.python.tools.json +7 -0
  168. oats/session/session.py +224 -0
  169. oats/session/session.py.AGENT.python.tools.json +7 -0
  170. oats/session/skill_selector.py +156 -0
  171. oats/session/skill_selector.py.AGENT.python.tools.json +7 -0
  172. oats/session/task_budget.py +159 -0
  173. oats/session/task_budget.py.AGENT.python.tools.json +7 -0
  174. oats/session/token_budget.py +90 -0
  175. oats/session/token_budget.py.AGENT.python.tools.json +7 -0
  176. oats/session/tool_retention.py +80 -0
  177. oats/session/tool_retention.py.AGENT.python.tools.json +7 -0
  178. oats/session/usage.py +139 -0
  179. oats/session/usage.py.AGENT.python.tools.json +7 -0
  180. oats/tool/AGENT.dir.python.tools.json +1 -0
  181. oats/tool/AGENT.python.tools.md +299 -0
  182. oats/tool/agent_tool.py +447 -0
  183. oats/tool/agent_tool.py.AGENT.python.tools.json +7 -0
  184. oats/tool/aws_safety.py +189 -0
  185. oats/tool/aws_safety.py.AGENT.python.tools.json +7 -0
  186. oats/tool/bash.py +188 -0
  187. oats/tool/bash.py.AGENT.python.tools.json +7 -0
  188. oats/tool/edit.py +437 -0
  189. oats/tool/generate_readme.py +280 -0
  190. oats/tool/generate_readme.py.AGENT.python.tools.json +7 -0
  191. oats/tool/glob_tool.py +183 -0
  192. oats/tool/glob_tool.py.AGENT.python.tools.json +7 -0
  193. oats/tool/grep.py +337 -0
  194. oats/tool/grep.py.AGENT.python.tools.json +7 -0
  195. oats/tool/init_tools.py +152 -0
  196. oats/tool/init_tools.py.AGENT.python.tools.json +7 -0
  197. oats/tool/lsp_tool.py +315 -0
  198. oats/tool/lsp_tool.py.AGENT.python.tools.json +7 -0
  199. oats/tool/memory_tool.py +241 -0
  200. oats/tool/memory_tool.py.AGENT.python.tools.json +7 -0
  201. oats/tool/multiedit.py +198 -0
  202. oats/tool/multiedit.py.AGENT.python.tools.json +7 -0
  203. oats/tool/patch.py +343 -0
  204. oats/tool/patch.py.AGENT.python.tools.json +7 -0
  205. oats/tool/plan.py +318 -0
  206. oats/tool/plan.py.AGENT.python.tools.json +7 -0
  207. oats/tool/playwright_search.py +227 -0
  208. oats/tool/playwright_search.py.AGENT.python.tools.json +7 -0
  209. oats/tool/question.py +245 -0
  210. oats/tool/question.py.AGENT.python.tools.json +7 -0
  211. oats/tool/read.py +199 -0
  212. oats/tool/read.py.AGENT.python.tools.json +7 -0
  213. oats/tool/registry.py +184 -0
  214. oats/tool/registry.py.AGENT.python.tools.json +7 -0
  215. oats/tool/todowrite.py +224 -0
  216. oats/tool/todowrite.py.AGENT.python.tools.json +7 -0
  217. oats/tool/tool_search.py +176 -0
  218. oats/tool/tool_search.py.AGENT.python.tools.json +7 -0
  219. oats/tool/webfetch.py +200 -0
  220. oats/tool/webfetch.py.AGENT.python.tools.json +7 -0
  221. oats/tool/websearch.py +277 -0
  222. oats/tool/websearch.py.AGENT.python.tools.json +7 -0
  223. oats/tool/write.py +154 -0
  224. oats/tool/write.py.AGENT.python.tools.json +7 -0
  225. oats/trajectory/AGENT.dir.python.tools.json +1 -0
  226. oats/trajectory/AGENT.python.tools.md +61 -0
  227. oats/trajectory/__init__.py +17 -0
  228. oats/trajectory/logger.py +119 -0
  229. oats/trajectory/logger.py.AGENT.python.tools.json +7 -0
  230. oats/trajectory/metrics.py +222 -0
  231. oats/trajectory/metrics.py.AGENT.python.tools.json +7 -0
  232. oats/trajectory/report.py +37 -0
  233. oats/trajectory/report.py.AGENT.python.tools.json +7 -0
  234. oats/trajectory/retrieval.py +140 -0
  235. oats/trajectory/retrieval.py.AGENT.python.tools.json +7 -0
  236. oats/trajectory/store.py +366 -0
  237. oats/trajectory/store.py.AGENT.python.tools.json +7 -0
  238. oats_coder-1.0.2.dist-info/METADATA +271 -0
  239. oats_coder-1.0.2.dist-info/RECORD +242 -0
  240. oats_coder-1.0.2.dist-info/WHEEL +4 -0
  241. oats_coder-1.0.2.dist-info/entry_points.txt +4 -0
  242. oats_coder-1.0.2.dist-info/licenses/LICENSE +1 -0
@@ -0,0 +1,159 @@
1
+ """
2
+ Lightweight task-budget tracking for agent loops.
3
+
4
+ This helps local-model sessions avoid runaway tool churn by monitoring turn
5
+ count, tool-call volume, and repeated tool patterns.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ from dataclasses import dataclass, field
12
+ from typing import Any
13
+
14
+
15
+ def _env_int(name: str, default: int) -> int:
16
+ try:
17
+ return int(os.getenv(name, str(default)))
18
+ except (TypeError, ValueError):
19
+ return default
20
+
21
+
22
+ @dataclass
23
+ class TaskBudgetSnapshot:
24
+ iteration: int
25
+ max_iterations: int
26
+ tool_calls: int
27
+ max_tool_calls: int
28
+ repeated_tool_streak: int
29
+ pressure: str
30
+ should_stop: bool
31
+ guidance: str | None = None
32
+
33
+
34
+ @dataclass
35
+ class SessionTaskBudget:
36
+ max_iterations: int = field(default_factory=lambda: _env_int("CODER_MAX_ITERATIONS", 150))
37
+ max_tool_calls: int = field(default_factory=lambda: _env_int("CODER_MAX_TOOL_CALLS", 300))
38
+ repeated_tool_limit: int = 3
39
+ commit_extension_iterations: int = 8
40
+ _tool_calls: int = 0
41
+ _history: list[tuple[str, str]] = field(default_factory=list)
42
+ _committed: bool = False
43
+ _commit_iteration: int = 0
44
+ _commit_tool_calls: int = 0
45
+
46
+ def record_tool_call(self, tool_name: str, arguments: dict[str, Any]) -> None:
47
+ normalized = json.dumps(arguments, sort_keys=True, ensure_ascii=True)
48
+ self._tool_calls += 1
49
+ self._history.append((tool_name, normalized))
50
+ if len(self._history) > 24:
51
+ self._history.pop(0)
52
+
53
+ def commit(self, iteration: int) -> None:
54
+ """Switch to commit mode: stop the discovery loop but allow the model
55
+ to continue calling tools needed to finalize work (edits, writes, etc.).
56
+ Budget limits are extended by commit_extension_iterations as a safety
57
+ net so a stuck model still terminates eventually."""
58
+ if self._committed:
59
+ return
60
+ self._committed = True
61
+ self._commit_iteration = iteration
62
+ self._commit_tool_calls = self._tool_calls
63
+ self._history.clear()
64
+
65
+ def snapshot(self, iteration: int) -> TaskBudgetSnapshot:
66
+ repeated_streak = self._repeated_streak()
67
+
68
+ if self._committed:
69
+ iters_since = iteration - self._commit_iteration
70
+ calls_since = self._tool_calls - self._commit_tool_calls
71
+ hard_stop = (
72
+ iters_since >= self.commit_extension_iterations
73
+ or calls_since >= self.commit_extension_iterations * 2
74
+ or repeated_streak >= self.repeated_tool_limit + 2
75
+ )
76
+ return TaskBudgetSnapshot(
77
+ iteration=iteration,
78
+ max_iterations=self.max_iterations,
79
+ tool_calls=self._tool_calls,
80
+ max_tool_calls=self.max_tool_calls,
81
+ repeated_tool_streak=repeated_streak,
82
+ pressure="critical",
83
+ should_stop=hard_stop,
84
+ guidance=self._build_commit_guidance(iters_since, repeated_streak),
85
+ )
86
+
87
+ iter_ratio = iteration / max(1, self.max_iterations)
88
+ tool_ratio = self._tool_calls / max(1, self.max_tool_calls)
89
+
90
+ if (
91
+ iteration >= self.max_iterations
92
+ or self._tool_calls >= self.max_tool_calls
93
+ or repeated_streak >= self.repeated_tool_limit + 2
94
+ ):
95
+ pressure = "critical"
96
+ should_stop = True
97
+ elif (
98
+ iter_ratio >= 0.85
99
+ or tool_ratio >= 0.85
100
+ or repeated_streak >= self.repeated_tool_limit
101
+ ):
102
+ pressure = "high"
103
+ should_stop = False
104
+ elif (
105
+ iter_ratio >= 0.65
106
+ or tool_ratio >= 0.65
107
+ or repeated_streak >= max(2, self.repeated_tool_limit - 2)
108
+ ):
109
+ pressure = "medium"
110
+ should_stop = False
111
+ else:
112
+ pressure = "low"
113
+ should_stop = False
114
+
115
+ guidance = None
116
+ if pressure in {"medium", "high", "critical"}:
117
+ guidance = self._build_guidance(pressure, repeated_streak)
118
+
119
+ return TaskBudgetSnapshot(
120
+ iteration=iteration,
121
+ max_iterations=self.max_iterations,
122
+ tool_calls=self._tool_calls,
123
+ max_tool_calls=self.max_tool_calls,
124
+ repeated_tool_streak=repeated_streak,
125
+ pressure=pressure,
126
+ should_stop=should_stop,
127
+ guidance=guidance,
128
+ )
129
+
130
+ def _repeated_streak(self) -> int:
131
+ if not self._history:
132
+ return 0
133
+ streak = 1
134
+ last = self._history[-1]
135
+ for item in reversed(self._history[:-1]):
136
+ if item == last:
137
+ streak += 1
138
+ else:
139
+ break
140
+ return streak
141
+
142
+ def _build_guidance(self, pressure: str, repeated_streak: int) -> str:
143
+ line = f"# Task: pressure={pressure}, calls={self._tool_calls}, repeat_streak={repeated_streak}"
144
+ if pressure in ("high", "critical"):
145
+ line += "\nFinish current subtask. Avoid repeating identical tool calls."
146
+ return line
147
+
148
+ def _build_commit_guidance(self, iters_since: int, repeated_streak: int) -> str:
149
+ return (
150
+ f"# COMMIT MODE (discovery budget exhausted; {iters_since} iters since commit, "
151
+ f"{self._tool_calls} total tool calls, repeat_streak={repeated_streak})\n"
152
+ "STOP exploring. Do NOT run more searches, greps, finds, or file reads for "
153
+ "discovery. Use ONLY the context you already have in this conversation to "
154
+ "complete the user's original task now.\n"
155
+ "Tool calls are still allowed, but ONLY to commit work the user asked for "
156
+ "(edits, writes, running the final command, etc.). If information is missing, "
157
+ "state what you know, state what's missing, and deliver the best answer you "
158
+ "can with what you have. Do not start new investigation threads."
159
+ )
@@ -0,0 +1,7 @@
1
+ {
2
+ "create_SessionTaskBudget": "create a SessionTaskBudget instance to track tool call limits and iteration budgets for an agent loop",
3
+ "record_tool_call": "record a tool call with its name and arguments to the SessionTaskBudget history for budget tracking",
4
+ "snapshot": "get a TaskBudgetSnapshot showing current pressure level, tool call count, and whether the agent should stop",
5
+ "commit": "commit the SessionTaskBudget to switch from discovery mode to commit mode with extended safety limits",
6
+ "review_TaskBudgetSnapshot": "review a TaskBudgetSnapshot dataclass to inspect iteration count, pressure level, and guidance for the agent"
7
+ }
@@ -0,0 +1,90 @@
1
+ """
2
+ Lightweight token-budget tracking for long-running sessions.
3
+
4
+ This is intentionally heuristic rather than tokenizer-accurate. The goal is to
5
+ help the runtime adapt before local-model context pressure becomes a failure.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from oats.session.message import Message
11
+
12
+
13
+ @dataclass
14
+ class BudgetSnapshot:
15
+ estimated_input_tokens: int
16
+ context_window: int
17
+ remaining_tokens: int
18
+ recommended_max_output_tokens: int
19
+ pressure: str
20
+
21
+
22
+ class SessionTokenBudget:
23
+ """Estimate context pressure and recommend output budgets."""
24
+
25
+ def __init__(
26
+ self,
27
+ context_window: int,
28
+ reserve_output_tokens: int = 4096,
29
+ minimum_output_tokens: int = 768,
30
+ ) -> None:
31
+ self.context_window = max(2048, context_window)
32
+ self.reserve_output_tokens = max(256, reserve_output_tokens)
33
+ self.minimum_output_tokens = max(256, minimum_output_tokens)
34
+
35
+ def snapshot(
36
+ self,
37
+ messages: list[Message],
38
+ requested_max_tokens: int | None = None,
39
+ ) -> BudgetSnapshot:
40
+ estimated_input = self._estimate_tokens(messages)
41
+ remaining = max(0, self.context_window - estimated_input)
42
+
43
+ available_for_output = max(
44
+ self.minimum_output_tokens,
45
+ remaining - min(self.reserve_output_tokens, max(0, remaining // 3)),
46
+ )
47
+ recommended = available_for_output
48
+ if requested_max_tokens is not None:
49
+ recommended = min(recommended, requested_max_tokens)
50
+ recommended = max(self.minimum_output_tokens, recommended)
51
+
52
+ ratio = estimated_input / max(1, self.context_window)
53
+ if ratio >= 0.92:
54
+ pressure = "critical"
55
+ elif ratio >= 0.82:
56
+ pressure = "high"
57
+ elif ratio >= 0.65:
58
+ pressure = "medium"
59
+ else:
60
+ pressure = "low"
61
+
62
+ return BudgetSnapshot(
63
+ estimated_input_tokens=estimated_input,
64
+ context_window=self.context_window,
65
+ remaining_tokens=remaining,
66
+ recommended_max_output_tokens=recommended,
67
+ pressure=pressure,
68
+ )
69
+
70
+ def _estimate_tokens(self, messages: list[Message]) -> int:
71
+ total_chars = 0
72
+ for msg in messages:
73
+ total_chars += len(msg.get_text_content() or "")
74
+ for tc in msg.get_tool_calls():
75
+ total_chars += len(str(tc.arguments))
76
+ for tr in msg.get_tool_results():
77
+ total_chars += len(tr.output or "")
78
+ total_chars += len(tr.error or "")
79
+ return total_chars // 4
80
+
81
+
82
+ def format_budget_guidance(snapshot: BudgetSnapshot) -> str:
83
+ """Create a compact prompt section describing current context pressure."""
84
+ line = (
85
+ f"# Budget: {snapshot.estimated_input_tokens}/{snapshot.context_window} tokens used, "
86
+ f"pressure={snapshot.pressure}, max_output={snapshot.recommended_max_output_tokens}"
87
+ )
88
+ if snapshot.pressure in ("high", "critical"):
89
+ line += "\nFinish current work concisely. Avoid long prose."
90
+ return line
@@ -0,0 +1,7 @@
1
+ {
2
+ "create_SessionTokenBudget": "create a SessionTokenBudget instance with a given context window size and output token reserves",
3
+ "call_snapshot": "call snapshot on a SessionTokenBudget to estimate input tokens and get recommended output budget",
4
+ "call_estimate_tokens": "call _estimate_tokens on a SessionTokenBudget to get a heuristic token count from a message list",
5
+ "call_format_budget_guidance": "call format_budget_guidance to produce a compact prompt string describing current context pressure",
6
+ "review_BudgetSnapshot": "review a BudgetSnapshot dataclass to inspect estimated input tokens, remaining tokens, and pressure level"
7
+ }
@@ -0,0 +1,80 @@
1
+ """
2
+ Retention policy for tool results stored in session history.
3
+
4
+ The goal is to keep enough signal for continuation without letting large tool
5
+ outputs dominate long sessions.
6
+ """
7
+ from __future__ import annotations
8
+ from dataclasses import dataclass
9
+ from oats.tool.registry import ToolResult
10
+
11
+
12
+ @dataclass
13
+ class RetainedToolResult:
14
+ output: str
15
+ metadata: dict
16
+
17
+
18
+ def retain_tool_result(tool_name: str, result: ToolResult) -> RetainedToolResult:
19
+ """
20
+ Compress a tool result for session retention.
21
+
22
+ The original `ToolResult` still drives the immediate turn. This helper only
23
+ determines what should be kept in conversation history for future turns.
24
+ """
25
+ output = result.output or ""
26
+ original_length = len(output)
27
+ retained = output
28
+
29
+ if tool_name == "read":
30
+ retained = _compress_read_output(output)
31
+ elif tool_name == "grep":
32
+ retained = _compress_grep_output(output)
33
+ elif tool_name == "bash":
34
+ retained = _compress_bash_output(output, result.error)
35
+ elif tool_name == "lsp":
36
+ retained = _compress_lsp_output(output)
37
+ elif len(output) > 4000:
38
+ retained = _compress_generic(output, head_lines=40, tail_lines=20)
39
+
40
+ metadata = dict(result.metadata or {})
41
+ metadata["retained_output"] = retained
42
+ metadata["retention_applied"] = retained != output
43
+ metadata["original_output_chars"] = original_length
44
+ metadata["retained_output_chars"] = len(retained)
45
+ return RetainedToolResult(output=retained, metadata=metadata)
46
+
47
+
48
+ def _compress_read_output(output: str) -> str:
49
+ return _compress_generic(output, head_lines=120, tail_lines=40)
50
+
51
+
52
+ def _compress_grep_output(output: str) -> str:
53
+ return _compress_generic(output, head_lines=80, tail_lines=20)
54
+
55
+
56
+ def _compress_bash_output(output: str, error: str | None) -> str:
57
+ if error:
58
+ return _compress_generic(output, head_lines=120, tail_lines=80)
59
+ return _compress_generic(output, head_lines=80, tail_lines=40)
60
+
61
+
62
+ def _compress_lsp_output(output: str) -> str:
63
+ return _compress_generic(output, head_lines=120, tail_lines=20)
64
+
65
+
66
+ def _compress_generic(output: str, head_lines: int, tail_lines: int) -> str:
67
+ if not output:
68
+ return output
69
+
70
+ lines = output.splitlines()
71
+ total = len(lines)
72
+ keep = head_lines + tail_lines
73
+ if total <= keep:
74
+ return output
75
+
76
+ head = lines[:head_lines]
77
+ tail = lines[-tail_lines:] if tail_lines > 0 else []
78
+ removed = total - len(head) - len(tail)
79
+ marker = [f"... [{removed} lines omitted for session retention] ..."]
80
+ return "\n".join(head + marker + tail)
@@ -0,0 +1,7 @@
1
+ {
2
+ "retain_tool_result": "compress a tool result for session retention by keeping head and tail lines and omitting the middle",
3
+ "compress_read_output": "compress read tool output by retaining the first 120 and last 40 lines for session history",
4
+ "compress_grep_output": "compress grep tool output by retaining the first 80 and last 20 lines for session history",
5
+ "compress_bash_output": "compress bash tool output by retaining head and tail lines with extra tail when an error is present",
6
+ "compress_generic": "compress any tool output by keeping a configurable number of head and tail lines and inserting an omission marker"
7
+ }
oats/session/usage.py ADDED
@@ -0,0 +1,139 @@
1
+ """
2
+ Usage tracking for coder2 - aggregates statistics across all sessions.
3
+
4
+ Tracks lifetime statistics including:
5
+ - Total sessions created
6
+ - Total prompts (messages) sent
7
+ - Total tokens used (prompt + completion) with input/output breakdown
8
+ - Token breakdown by session
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass, field
13
+ from datetime import datetime
14
+ from typing import Optional
15
+ from oats.session.session import list_sessions, SessionInfo
16
+
17
+
18
+ @dataclass
19
+ class SessionUsageEntry:
20
+ """Per-session usage entry for detailed breakdown."""
21
+
22
+ session_id: str
23
+ title: str
24
+ input_tokens: int = 0
25
+ output_tokens: int = 0
26
+ total_tokens: int = 0
27
+ message_count: int = 0
28
+ created: Optional[datetime] = None
29
+
30
+
31
+ @dataclass
32
+ class UsageStats:
33
+ """Aggregated usage statistics across all sessions."""
34
+
35
+ total_sessions: int = 0
36
+ total_prompts: int = 0 # Total user messages across all sessions
37
+ total_tokens: int = 0 # Total tokens across all sessions
38
+ total_input_tokens: int = 0 # Total input (prompt) tokens
39
+ total_output_tokens: int = 0 # Total output (completion) tokens
40
+ earliest_session: Optional[datetime] = None
41
+ latest_session: Optional[datetime] = None
42
+ sessions: list[SessionUsageEntry] = field(default_factory=list)
43
+
44
+ def to_dict(self) -> dict:
45
+ """Convert to dictionary for display."""
46
+ return {
47
+ "total_sessions": self.total_sessions,
48
+ "total_prompts": self.total_prompts,
49
+ "total_tokens": self.total_tokens,
50
+ "total_input_tokens": self.total_input_tokens,
51
+ "total_output_tokens": self.total_output_tokens,
52
+ "earliest_session": self.earliest_session.isoformat() if self.earliest_session else None,
53
+ "latest_session": self.latest_session.isoformat() if self.latest_session else None,
54
+ }
55
+
56
+
57
+ async def get_usage_stats() -> UsageStats:
58
+ """Calculate aggregated usage statistics across all sessions.
59
+
60
+ Returns:
61
+ UsageStats with lifetime totals for sessions, prompts, and tokens.
62
+ """
63
+ sessions = await list_sessions()
64
+
65
+ if not sessions:
66
+ return UsageStats()
67
+
68
+ stats = UsageStats(
69
+ total_sessions=len(sessions),
70
+ total_prompts=0,
71
+ total_tokens=0,
72
+ total_input_tokens=0,
73
+ total_output_tokens=0,
74
+ earliest_session=None,
75
+ latest_session=None,
76
+ )
77
+
78
+ for session_info in sessions:
79
+ # Accumulate prompt count (message_count includes all messages)
80
+ stats.total_prompts += session_info.message_count
81
+
82
+ # Accumulate token usage
83
+ stats.total_tokens += session_info.total_tokens
84
+ stats.total_input_tokens += getattr(session_info, 'total_input_tokens', 0)
85
+ stats.total_output_tokens += getattr(session_info, 'total_output_tokens', 0)
86
+
87
+ # Track per-session breakdown
88
+ stats.sessions.append(SessionUsageEntry(
89
+ session_id=session_info.id,
90
+ title=session_info.title,
91
+ input_tokens=getattr(session_info, 'total_input_tokens', 0),
92
+ output_tokens=getattr(session_info, 'total_output_tokens', 0),
93
+ total_tokens=session_info.total_tokens,
94
+ message_count=session_info.message_count,
95
+ created=session_info.time.created,
96
+ ))
97
+
98
+ # Track earliest and latest sessions
99
+ created = session_info.time.created
100
+ if stats.earliest_session is None or created < stats.earliest_session:
101
+ stats.earliest_session = created
102
+ if stats.latest_session is None or created > stats.latest_session:
103
+ stats.latest_session = created
104
+
105
+ # Sort sessions by most recent first
106
+ stats.sessions.sort(key=lambda s: s.created or datetime.min, reverse=True)
107
+
108
+ return stats
109
+
110
+
111
+ def format_tokens(count: int) -> str:
112
+ """Format token count with K/M/B suffixes for readability."""
113
+ if count >= 1_000_000_000:
114
+ return f"{count / 1_000_000_000:.1f}B"
115
+ elif count >= 1_000_000:
116
+ return f"{count / 1_000_000:.1f}M"
117
+ elif count >= 1_000:
118
+ return f"{count / 1_000:.1f}K"
119
+ else:
120
+ return str(count)
121
+
122
+
123
+ def format_usage_summary(stats: UsageStats) -> str:
124
+ """Format usage statistics as a human-readable summary."""
125
+ lines = [
126
+ f" Total Sessions: {stats.total_sessions}",
127
+ f" Total Prompts: {stats.total_prompts}",
128
+ f" Total Tokens: {format_tokens(stats.total_tokens)}",
129
+ ]
130
+ if stats.total_input_tokens or stats.total_output_tokens:
131
+ lines.append(f" Input Tokens: {format_tokens(stats.total_input_tokens)}")
132
+ lines.append(f" Output Tokens: {format_tokens(stats.total_output_tokens)}")
133
+
134
+ if stats.earliest_session:
135
+ lines.append(f" First Session: {stats.earliest_session.strftime('%Y-%m-%d %H:%M')}")
136
+ if stats.latest_session:
137
+ lines.append(f" Latest Session: {stats.latest_session.strftime('%Y-%m-%d %H:%M')}")
138
+
139
+ return "\n".join(lines)
@@ -0,0 +1,7 @@
1
+ {
2
+ "get_usage_stats": "get aggregated usage statistics across all sessions including total prompts tokens and session counts",
3
+ "UsageStats_to_dict": "convert a UsageStats object to a dictionary for display with session totals and token breakdowns",
4
+ "format_tokens": "format a token count integer into a human-readable string with K M or B suffixes",
5
+ "format_usage_summary": "format a UsageStats object into a human-readable summary string with sessions prompts and tokens",
6
+ "SessionUsageEntry": "create a per-session usage entry dataclass with session id title tokens and message count"
7
+ }