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.
- oats/AGENT.dir.python.tools.json +1 -0
- oats/AGENT.python.tools.md +131 -0
- oats/agent/AGENT.dir.python.tools.json +1 -0
- oats/agent/AGENT.python.tools.md +19 -0
- oats/agent/agent.py +176 -0
- oats/agent/agent.py.AGENT.python.tools.json +7 -0
- oats/agent_get_tool_choices_for_prompt.py +32 -0
- oats/agent_get_tool_choices_for_prompt.py.AGENT.python.tools.json +7 -0
- oats/call_tool_with_loader1.py +430 -0
- oats/call_tool_with_loader1.py.AGENT.python.tools.json +7 -0
- oats/cli/AGENT.dir.python.tools.json +1 -0
- oats/cli/AGENT.python.tools.md +33 -0
- oats/cli/approval.py +154 -0
- oats/cli/approval.py.AGENT.python.tools.json +7 -0
- oats/cli/check_providers.py +25 -0
- oats/cli/check_providers.py.AGENT.python.tools.json +7 -0
- oats/cli/interactive.py +550 -0
- oats/cli/interactive.py.AGENT.python.tools.json +7 -0
- oats/cli/process_message.py +153 -0
- oats/cli/process_message.py.AGENT.python.tools.json +7 -0
- oats/cli/tui/AGENT.dir.python.tools.json +1 -0
- oats/cli/tui/AGENT.python.tools.md +19 -0
- oats/cli/tui/tui_banner.py +114 -0
- oats/cli/tui/tui_banner.py.AGENT.python.tools.json +7 -0
- oats/cli/tui/tui_consts.py +120 -0
- oats/cli/tui/tui_consts.py.AGENT.python.tools.json +7 -0
- oats/cli/tui/tui_utils.py +492 -0
- oats/cli/tui/tui_utils.py.AGENT.python.tools.json +7 -0
- oats/config/coder.json +27 -0
- oats/core/AGENT.dir.python.tools.json +1 -0
- oats/core/AGENT.python.tools.md +117 -0
- oats/core/__init__.py +19 -0
- oats/core/bus.py +170 -0
- oats/core/bus.py.AGENT.python.tools.json +7 -0
- oats/core/config.py +270 -0
- oats/core/config.py.AGENT.python.tools.json +7 -0
- oats/core/features.py +118 -0
- oats/core/features.py.AGENT.python.tools.json +7 -0
- oats/core/id.py +17 -0
- oats/core/id.py.AGENT.python.tools.json +7 -0
- oats/core/offline.py +93 -0
- oats/core/offline.py.AGENT.python.tools.json +7 -0
- oats/core/profiles.py +179 -0
- oats/core/profiles.py.AGENT.python.tools.json +7 -0
- oats/core/storage.py +207 -0
- oats/core/storage.py.AGENT.python.tools.json +7 -0
- oats/core/tokens.py +80 -0
- oats/core/tokens.py.AGENT.python.tools.json +7 -0
- oats/date.py +83 -0
- oats/date.py.AGENT.python.tools.json +7 -0
- oats/determine_best_tools1.py +311 -0
- oats/determine_best_tools1.py.AGENT.python.tools.json +7 -0
- oats/get_oat_config.py +12 -0
- oats/get_oat_config.py.AGENT.python.tools.json +3 -0
- oats/git/AGENT.dir.python.tools.json +1 -0
- oats/git/AGENT.python.tools.md +89 -0
- oats/git/__init__.py +6 -0
- oats/git/build_git_repo_to_dataset.py +185 -0
- oats/git/build_git_repo_to_dataset.py.AGENT.python.tools.json +7 -0
- oats/git/coauthor.py +75 -0
- oats/git/coauthor.py.AGENT.python.tools.json +7 -0
- oats/git/git_commit_search1.py +537 -0
- oats/git/git_commit_search1.py.AGENT.python.tools.json +7 -0
- oats/git/git_diff_extractor.py +221 -0
- oats/git/git_diff_extractor.py.AGENT.python.tools.json +7 -0
- oats/git/git_to_df_converter.py +115 -0
- oats/git/git_to_df_converter.py.AGENT.python.tools.json +7 -0
- oats/git/repo_to_parquet.py +81 -0
- oats/git/repo_to_parquet.py.AGENT.python.tools.json +7 -0
- oats/git/walk_up_dir_path_to_find_git_config.py +56 -0
- oats/git/walk_up_dir_path_to_find_git_config.py.AGENT.python.tools.json +7 -0
- oats/git/worktree.py +184 -0
- oats/git/worktree.py.AGENT.python.tools.json +7 -0
- oats/hook/AGENT.dir.python.tools.json +1 -0
- oats/hook/AGENT.python.tools.md +19 -0
- oats/hook/__init__.py +23 -0
- oats/hook/engine.py +225 -0
- oats/hook/engine.py.AGENT.python.tools.json +7 -0
- oats/load_tools_from_source1.py +594 -0
- oats/load_tools_from_source1.py.AGENT.python.tools.json +7 -0
- oats/log.py +233 -0
- oats/log.py.AGENT.python.tools.json +7 -0
- oats/lsp/AGENT.dir.python.tools.json +1 -0
- oats/lsp/AGENT.python.tools.md +19 -0
- oats/lsp/__init__.py +1 -0
- oats/lsp/client.py +381 -0
- oats/lsp/client.py.AGENT.python.tools.json +7 -0
- oats/mcp/AGENT.dir.python.tools.json +1 -0
- oats/mcp/AGENT.python.tools.md +159 -0
- oats/mcp/config.py +201 -0
- oats/mcp/config.py.AGENT.python.tools.json +7 -0
- oats/mcp/example_mcp_config.json +58 -0
- oats/mcp/fetch.py +405 -0
- oats/mcp/fetch.py.AGENT.python.tools.json +7 -0
- oats/mcp/index.py +381 -0
- oats/mcp/index.py.AGENT.python.tools.json +7 -0
- oats/mcp/intent.py +427 -0
- oats/mcp/intent.py.AGENT.python.tools.json +7 -0
- oats/mcp/models.py +304 -0
- oats/mcp/models.py.AGENT.python.tools.json +7 -0
- oats/mcp/orchestrator.py +653 -0
- oats/mcp/orchestrator.py.AGENT.python.tools.json +7 -0
- oats/mcp/ranking.py +243 -0
- oats/mcp/ranking.py.AGENT.python.tools.json +7 -0
- oats/mcp/registry.py +567 -0
- oats/mcp/registry.py.AGENT.python.tools.json +7 -0
- oats/mcp/resolver.py +588 -0
- oats/mcp/resolver.py.AGENT.python.tools.json +7 -0
- oats/mcp/tools.py +574 -0
- oats/mcp/tools.py.AGENT.python.tools.json +7 -0
- oats/mcp/tracker.py +254 -0
- oats/mcp/tracker.py.AGENT.python.tools.json +7 -0
- oats/memory/AGENT.dir.python.tools.json +1 -0
- oats/memory/AGENT.python.tools.md +19 -0
- oats/memory/__init__.py +14 -0
- oats/memory/manager.py +180 -0
- oats/memory/manager.py.AGENT.python.tools.json +7 -0
- oats/memory/models.py +97 -0
- oats/memory/models.py.AGENT.python.tools.json +7 -0
- oats/models.py +332 -0
- oats/models.py.AGENT.python.tools.json +7 -0
- oats/oweb/AGENT.dir.python.tools.json +1 -0
- oats/oweb/AGENT.python.tools.md +33 -0
- oats/oweb/get_auth.py +56 -0
- oats/oweb/get_auth.py.AGENT.python.tools.json +7 -0
- oats/oweb/login.py +72 -0
- oats/oweb/login.py.AGENT.python.tools.json +7 -0
- oats/plugins/AGENT.dir.python.tools.json +1 -0
- oats/plugins/AGENT.python.tools.md +33 -0
- oats/plugins/__init__.py +24 -0
- oats/plugins/loader.py +278 -0
- oats/plugins/loader.py.AGENT.python.tools.json +7 -0
- oats/plugins/manifest.py +171 -0
- oats/plugins/manifest.py.AGENT.python.tools.json +7 -0
- oats/pp.py +8 -0
- oats/pp.py.AGENT.python.tools.json +7 -0
- oats/provider/AGENT.dir.python.tools.json +1 -0
- oats/provider/AGENT.python.tools.md +33 -0
- oats/provider/models.py +249 -0
- oats/provider/models.py.AGENT.python.tools.json +7 -0
- oats/provider/provider.py +822 -0
- oats/provider/provider.py.AGENT.python.tools.json +7 -0
- oats/session/AGENT.dir.python.tools.json +1 -0
- oats/session/AGENT.python.tools.md +201 -0
- oats/session/__init__.py +35 -0
- oats/session/build_system_prompt.py +184 -0
- oats/session/build_system_prompt.py.AGENT.python.tools.json +7 -0
- oats/session/caveman.py +177 -0
- oats/session/caveman.py.AGENT.python.tools.json +7 -0
- oats/session/compaction.py +463 -0
- oats/session/compaction.py.AGENT.python.tools.json +7 -0
- oats/session/debug_trace.py +41 -0
- oats/session/debug_trace.py.AGENT.python.tools.json +7 -0
- oats/session/file_cache.py +108 -0
- oats/session/file_cache.py.AGENT.python.tools.json +7 -0
- oats/session/message.py +214 -0
- oats/session/message.py.AGENT.python.tools.json +7 -0
- oats/session/metrics.py +52 -0
- oats/session/metrics.py.AGENT.python.tools.json +7 -0
- oats/session/models.py +43 -0
- oats/session/models.py.AGENT.python.tools.json +5 -0
- oats/session/modes.py +107 -0
- oats/session/modes.py.AGENT.python.tools.json +7 -0
- oats/session/processor.py +1600 -0
- oats/session/processor.py.AGENT.python.tools.json +7 -0
- oats/session/screenshot_store.py +157 -0
- oats/session/screenshot_store.py.AGENT.python.tools.json +7 -0
- oats/session/session.py +224 -0
- oats/session/session.py.AGENT.python.tools.json +7 -0
- oats/session/skill_selector.py +156 -0
- oats/session/skill_selector.py.AGENT.python.tools.json +7 -0
- oats/session/task_budget.py +159 -0
- oats/session/task_budget.py.AGENT.python.tools.json +7 -0
- oats/session/token_budget.py +90 -0
- oats/session/token_budget.py.AGENT.python.tools.json +7 -0
- oats/session/tool_retention.py +80 -0
- oats/session/tool_retention.py.AGENT.python.tools.json +7 -0
- oats/session/usage.py +139 -0
- oats/session/usage.py.AGENT.python.tools.json +7 -0
- oats/tool/AGENT.dir.python.tools.json +1 -0
- oats/tool/AGENT.python.tools.md +299 -0
- oats/tool/agent_tool.py +447 -0
- oats/tool/agent_tool.py.AGENT.python.tools.json +7 -0
- oats/tool/aws_safety.py +189 -0
- oats/tool/aws_safety.py.AGENT.python.tools.json +7 -0
- oats/tool/bash.py +188 -0
- oats/tool/bash.py.AGENT.python.tools.json +7 -0
- oats/tool/edit.py +437 -0
- oats/tool/generate_readme.py +280 -0
- oats/tool/generate_readme.py.AGENT.python.tools.json +7 -0
- oats/tool/glob_tool.py +183 -0
- oats/tool/glob_tool.py.AGENT.python.tools.json +7 -0
- oats/tool/grep.py +337 -0
- oats/tool/grep.py.AGENT.python.tools.json +7 -0
- oats/tool/init_tools.py +152 -0
- oats/tool/init_tools.py.AGENT.python.tools.json +7 -0
- oats/tool/lsp_tool.py +315 -0
- oats/tool/lsp_tool.py.AGENT.python.tools.json +7 -0
- oats/tool/memory_tool.py +241 -0
- oats/tool/memory_tool.py.AGENT.python.tools.json +7 -0
- oats/tool/multiedit.py +198 -0
- oats/tool/multiedit.py.AGENT.python.tools.json +7 -0
- oats/tool/patch.py +343 -0
- oats/tool/patch.py.AGENT.python.tools.json +7 -0
- oats/tool/plan.py +318 -0
- oats/tool/plan.py.AGENT.python.tools.json +7 -0
- oats/tool/playwright_search.py +227 -0
- oats/tool/playwright_search.py.AGENT.python.tools.json +7 -0
- oats/tool/question.py +245 -0
- oats/tool/question.py.AGENT.python.tools.json +7 -0
- oats/tool/read.py +199 -0
- oats/tool/read.py.AGENT.python.tools.json +7 -0
- oats/tool/registry.py +184 -0
- oats/tool/registry.py.AGENT.python.tools.json +7 -0
- oats/tool/todowrite.py +224 -0
- oats/tool/todowrite.py.AGENT.python.tools.json +7 -0
- oats/tool/tool_search.py +176 -0
- oats/tool/tool_search.py.AGENT.python.tools.json +7 -0
- oats/tool/webfetch.py +200 -0
- oats/tool/webfetch.py.AGENT.python.tools.json +7 -0
- oats/tool/websearch.py +277 -0
- oats/tool/websearch.py.AGENT.python.tools.json +7 -0
- oats/tool/write.py +154 -0
- oats/tool/write.py.AGENT.python.tools.json +7 -0
- oats/trajectory/AGENT.dir.python.tools.json +1 -0
- oats/trajectory/AGENT.python.tools.md +61 -0
- oats/trajectory/__init__.py +17 -0
- oats/trajectory/logger.py +119 -0
- oats/trajectory/logger.py.AGENT.python.tools.json +7 -0
- oats/trajectory/metrics.py +222 -0
- oats/trajectory/metrics.py.AGENT.python.tools.json +7 -0
- oats/trajectory/report.py +37 -0
- oats/trajectory/report.py.AGENT.python.tools.json +7 -0
- oats/trajectory/retrieval.py +140 -0
- oats/trajectory/retrieval.py.AGENT.python.tools.json +7 -0
- oats/trajectory/store.py +366 -0
- oats/trajectory/store.py.AGENT.python.tools.json +7 -0
- oats_coder-1.0.2.dist-info/METADATA +271 -0
- oats_coder-1.0.2.dist-info/RECORD +242 -0
- oats_coder-1.0.2.dist-info/WHEEL +4 -0
- oats_coder-1.0.2.dist-info/entry_points.txt +4 -0
- 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
|
+
}
|