code-puppy 0.0.173__py3-none-any.whl → 0.0.174__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.
- code_puppy/agent.py +11 -11
- code_puppy/agents/__init__.py +4 -6
- code_puppy/agents/agent_manager.py +15 -187
- code_puppy/agents/base_agent.py +470 -63
- code_puppy/command_line/command_handler.py +40 -41
- code_puppy/command_line/mcp/start_all_command.py +3 -6
- code_puppy/command_line/mcp/start_command.py +0 -5
- code_puppy/command_line/mcp/stop_all_command.py +3 -6
- code_puppy/command_line/mcp/stop_command.py +2 -6
- code_puppy/command_line/model_picker_completion.py +2 -2
- code_puppy/command_line/prompt_toolkit_completion.py +2 -2
- code_puppy/config.py +2 -2
- code_puppy/main.py +12 -49
- code_puppy/summarization_agent.py +2 -2
- code_puppy/tools/agent_tools.py +5 -4
- code_puppy/tools/browser/vqa_agent.py +1 -3
- code_puppy/tui/app.py +48 -77
- code_puppy/tui/screens/settings.py +2 -2
- {code_puppy-0.0.173.dist-info → code_puppy-0.0.174.dist-info}/METADATA +2 -2
- {code_puppy-0.0.173.dist-info → code_puppy-0.0.174.dist-info}/RECORD +24 -29
- code_puppy/agents/agent_orchestrator.json +0 -26
- code_puppy/agents/runtime_manager.py +0 -272
- code_puppy/command_line/meta_command_handler.py +0 -153
- code_puppy/message_history_processor.py +0 -408
- code_puppy/state_management.py +0 -58
- {code_puppy-0.0.173.data → code_puppy-0.0.174.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.173.dist-info → code_puppy-0.0.174.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.173.dist-info → code_puppy-0.0.174.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.173.dist-info → code_puppy-0.0.174.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
|
|
3
|
-
from rich.console import Console
|
|
4
|
-
|
|
5
|
-
from code_puppy.command_line.model_picker_completion import (
|
|
6
|
-
load_model_names,
|
|
7
|
-
update_model_in_input,
|
|
8
|
-
)
|
|
9
|
-
from code_puppy.config import get_config_keys
|
|
10
|
-
from code_puppy.command_line.utils import make_directory_table
|
|
11
|
-
from code_puppy.command_line.motd import print_motd
|
|
12
|
-
|
|
13
|
-
META_COMMANDS_HELP = """
|
|
14
|
-
[bold magenta]Meta Commands Help[/bold magenta]
|
|
15
|
-
~help, ~h Show this help message
|
|
16
|
-
~cd <dir> Change directory or show directories
|
|
17
|
-
~m <model> Set active model
|
|
18
|
-
~motd Show the latest message of the day (MOTD)
|
|
19
|
-
~show Show puppy config key-values
|
|
20
|
-
~set Set puppy config key-values (message_limit, protected_token_count, compaction_threshold, allow_recursion, etc.)
|
|
21
|
-
~<unknown> Show unknown meta command warning
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def handle_meta_command(command: str, console: Console) -> bool:
|
|
26
|
-
"""
|
|
27
|
-
Handle meta/config commands prefixed with '~'.
|
|
28
|
-
Returns True if the command was handled (even if just an error/help), False if not.
|
|
29
|
-
"""
|
|
30
|
-
command = command.strip()
|
|
31
|
-
|
|
32
|
-
if command.strip().startswith("~motd"):
|
|
33
|
-
print_motd(console, force=True)
|
|
34
|
-
return True
|
|
35
|
-
|
|
36
|
-
if command.startswith("~cd"):
|
|
37
|
-
tokens = command.split()
|
|
38
|
-
if len(tokens) == 1:
|
|
39
|
-
try:
|
|
40
|
-
table = make_directory_table()
|
|
41
|
-
console.print(table)
|
|
42
|
-
except Exception as e:
|
|
43
|
-
console.print(f"[red]Error listing directory:[/red] {e}")
|
|
44
|
-
return True
|
|
45
|
-
elif len(tokens) == 2:
|
|
46
|
-
dirname = tokens[1]
|
|
47
|
-
target = os.path.expanduser(dirname)
|
|
48
|
-
if not os.path.isabs(target):
|
|
49
|
-
target = os.path.join(os.getcwd(), target)
|
|
50
|
-
if os.path.isdir(target):
|
|
51
|
-
os.chdir(target)
|
|
52
|
-
console.print(
|
|
53
|
-
f"[bold green]Changed directory to:[/bold green] [cyan]{target}[/cyan]"
|
|
54
|
-
)
|
|
55
|
-
else:
|
|
56
|
-
console.print(f"[red]Not a directory:[/red] [bold]{dirname}[/bold]")
|
|
57
|
-
return True
|
|
58
|
-
|
|
59
|
-
if command.strip().startswith("~show"):
|
|
60
|
-
from code_puppy.command_line.model_picker_completion import get_active_model
|
|
61
|
-
from code_puppy.config import (
|
|
62
|
-
get_owner_name,
|
|
63
|
-
get_puppy_name,
|
|
64
|
-
get_yolo_mode,
|
|
65
|
-
get_message_limit,
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
puppy_name = get_puppy_name()
|
|
69
|
-
owner_name = get_owner_name()
|
|
70
|
-
model = get_active_model()
|
|
71
|
-
yolo_mode = get_yolo_mode()
|
|
72
|
-
msg_limit = get_message_limit()
|
|
73
|
-
console.print(f"""[bold magenta]🐶 Puppy Status[/bold magenta]
|
|
74
|
-
|
|
75
|
-
[bold]puppy_name:[/bold] [cyan]{puppy_name}[/cyan]
|
|
76
|
-
[bold]owner_name:[/bold] [cyan]{owner_name}[/cyan]
|
|
77
|
-
[bold]model:[/bold] [green]{model}[/green]
|
|
78
|
-
[bold]YOLO_MODE:[/bold] {"[red]ON[/red]" if yolo_mode else "[yellow]off[/yellow]"}
|
|
79
|
-
[bold]message_limit:[/bold] [cyan]{msg_limit}[/cyan] requests per minute
|
|
80
|
-
""")
|
|
81
|
-
return True
|
|
82
|
-
|
|
83
|
-
if command.startswith("~set"):
|
|
84
|
-
# Syntax: ~set KEY=VALUE or ~set KEY VALUE
|
|
85
|
-
from code_puppy.config import set_config_value
|
|
86
|
-
|
|
87
|
-
tokens = command.split(None, 2)
|
|
88
|
-
argstr = command[len("~set") :].strip()
|
|
89
|
-
key = None
|
|
90
|
-
value = None
|
|
91
|
-
if "=" in argstr:
|
|
92
|
-
key, value = argstr.split("=", 1)
|
|
93
|
-
key = key.strip()
|
|
94
|
-
value = value.strip()
|
|
95
|
-
elif len(tokens) >= 3:
|
|
96
|
-
key = tokens[1]
|
|
97
|
-
value = tokens[2]
|
|
98
|
-
elif len(tokens) == 2:
|
|
99
|
-
key = tokens[1]
|
|
100
|
-
value = ""
|
|
101
|
-
else:
|
|
102
|
-
console.print(
|
|
103
|
-
f"[yellow]Usage:[/yellow] ~set KEY=VALUE or ~set KEY VALUE\nConfig keys: {', '.join(get_config_keys())}"
|
|
104
|
-
)
|
|
105
|
-
return True
|
|
106
|
-
if key:
|
|
107
|
-
set_config_value(key, value)
|
|
108
|
-
console.print(
|
|
109
|
-
f'[green]🌶 Set[/green] [cyan]{key}[/cyan] = "{value}" in puppy.cfg!'
|
|
110
|
-
)
|
|
111
|
-
else:
|
|
112
|
-
console.print("[red]You must supply a key.[/red]")
|
|
113
|
-
return True
|
|
114
|
-
|
|
115
|
-
if command.startswith("~m"):
|
|
116
|
-
# Try setting model and show confirmation
|
|
117
|
-
new_input = update_model_in_input(command)
|
|
118
|
-
if new_input is not None:
|
|
119
|
-
from code_puppy.command_line.model_picker_completion import get_active_model
|
|
120
|
-
from code_puppy.agents.runtime_manager import get_runtime_agent_manager
|
|
121
|
-
|
|
122
|
-
model = get_active_model()
|
|
123
|
-
# Make sure this is called for the test
|
|
124
|
-
manager = get_runtime_agent_manager()
|
|
125
|
-
manager.reload_agent()
|
|
126
|
-
console.print(
|
|
127
|
-
f"[bold green]Active model set and loaded:[/bold green] [cyan]{model}[/cyan]"
|
|
128
|
-
)
|
|
129
|
-
return True
|
|
130
|
-
# If no model matched, show available models
|
|
131
|
-
model_names = load_model_names()
|
|
132
|
-
console.print("[yellow]Usage:[/yellow] ~m <model-name>")
|
|
133
|
-
console.print(f"[yellow]Available models:[/yellow] {', '.join(model_names)}")
|
|
134
|
-
return True
|
|
135
|
-
if command in ("~help", "~h"):
|
|
136
|
-
console.print(META_COMMANDS_HELP)
|
|
137
|
-
return True
|
|
138
|
-
if command.startswith("~"):
|
|
139
|
-
name = command[1:].split()[0] if len(command) > 1 else ""
|
|
140
|
-
if name:
|
|
141
|
-
console.print(
|
|
142
|
-
f"[yellow]Unknown meta command:[/yellow] {command}\n[dim]Type ~help for options.[/dim]"
|
|
143
|
-
)
|
|
144
|
-
else:
|
|
145
|
-
# Show current model ONLY here
|
|
146
|
-
from code_puppy.command_line.model_picker_completion import get_active_model
|
|
147
|
-
|
|
148
|
-
current_model = get_active_model()
|
|
149
|
-
console.print(
|
|
150
|
-
f"[bold green]Current Model:[/bold green] [cyan]{current_model}[/cyan]"
|
|
151
|
-
)
|
|
152
|
-
return True
|
|
153
|
-
return False
|
|
@@ -1,408 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import queue
|
|
3
|
-
from typing import Any, List, Set, Tuple, Union
|
|
4
|
-
|
|
5
|
-
import pydantic
|
|
6
|
-
from pydantic_ai.messages import (
|
|
7
|
-
ModelMessage,
|
|
8
|
-
ModelRequest,
|
|
9
|
-
TextPart,
|
|
10
|
-
ToolCallPart,
|
|
11
|
-
ToolCallPartDelta,
|
|
12
|
-
ToolReturn,
|
|
13
|
-
ToolReturnPart,
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
from code_puppy.config import (
|
|
17
|
-
get_model_name,
|
|
18
|
-
get_protected_token_count,
|
|
19
|
-
get_compaction_threshold,
|
|
20
|
-
get_compaction_strategy,
|
|
21
|
-
)
|
|
22
|
-
from code_puppy.messaging import emit_error, emit_info, emit_warning
|
|
23
|
-
from code_puppy.model_factory import ModelFactory
|
|
24
|
-
from code_puppy.state_management import (
|
|
25
|
-
add_compacted_message_hash,
|
|
26
|
-
get_compacted_message_hashes,
|
|
27
|
-
get_message_history,
|
|
28
|
-
set_message_history,
|
|
29
|
-
)
|
|
30
|
-
from code_puppy.summarization_agent import run_summarization_sync
|
|
31
|
-
|
|
32
|
-
# Protected tokens are now configurable via get_protected_token_count()
|
|
33
|
-
# Default is 50000 but can be customized in ~/.code_puppy/puppy.cfg
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def stringify_message_part(part) -> str:
|
|
37
|
-
"""
|
|
38
|
-
Convert a message part to a string representation for token estimation or other uses.
|
|
39
|
-
|
|
40
|
-
Args:
|
|
41
|
-
part: A message part that may contain content or be a tool call
|
|
42
|
-
|
|
43
|
-
Returns:
|
|
44
|
-
String representation of the message part
|
|
45
|
-
"""
|
|
46
|
-
# Get current agent to use its method
|
|
47
|
-
from code_puppy.agents.agent_manager import get_current_agent_config
|
|
48
|
-
current_agent = get_current_agent_config()
|
|
49
|
-
return current_agent.stringify_message_part(part)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def estimate_tokens_for_message(message: ModelMessage) -> int:
|
|
53
|
-
"""
|
|
54
|
-
Estimate the number of tokens in a message using len(message) - 4.
|
|
55
|
-
Simple and fast replacement for tiktoken.
|
|
56
|
-
"""
|
|
57
|
-
# Get current agent to use its method
|
|
58
|
-
from code_puppy.agents.agent_manager import get_current_agent_config
|
|
59
|
-
current_agent = get_current_agent_config()
|
|
60
|
-
return current_agent.estimate_tokens_for_message(message)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def filter_huge_messages(messages: List[ModelMessage]) -> List[ModelMessage]:
|
|
64
|
-
if not messages:
|
|
65
|
-
return []
|
|
66
|
-
|
|
67
|
-
# Get current agent to use its method
|
|
68
|
-
from code_puppy.agents.agent_manager import get_current_agent_config
|
|
69
|
-
current_agent = get_current_agent_config()
|
|
70
|
-
|
|
71
|
-
# Never drop the system prompt, even if it is extremely large.
|
|
72
|
-
system_message, *rest = messages
|
|
73
|
-
filtered_rest = [
|
|
74
|
-
m for m in rest if current_agent.estimate_tokens_for_message(m) < 50000
|
|
75
|
-
]
|
|
76
|
-
return [system_message] + filtered_rest
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
def _is_tool_call_part(part: Any) -> bool:
|
|
80
|
-
# Get current agent to use its method
|
|
81
|
-
from code_puppy.agents.agent_manager import get_current_agent_config
|
|
82
|
-
current_agent = get_current_agent_config()
|
|
83
|
-
return current_agent._is_tool_call_part(part)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def _is_tool_return_part(part: Any) -> bool:
|
|
87
|
-
# Get current agent to use its method
|
|
88
|
-
from code_puppy.agents.agent_manager import get_current_agent_config
|
|
89
|
-
current_agent = get_current_agent_config()
|
|
90
|
-
return current_agent._is_tool_return_part(part)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def split_messages_for_protected_summarization(
|
|
94
|
-
messages: List[ModelMessage], with_protection: bool = True
|
|
95
|
-
) -> Tuple[List[ModelMessage], List[ModelMessage]]:
|
|
96
|
-
"""
|
|
97
|
-
Split messages into two groups: messages to summarize and protected recent messages.
|
|
98
|
-
|
|
99
|
-
Returns:
|
|
100
|
-
Tuple of (messages_to_summarize, protected_messages)
|
|
101
|
-
|
|
102
|
-
The protected_messages are the most recent messages that total up to the configured protected token count.
|
|
103
|
-
The system message (first message) is always protected.
|
|
104
|
-
All other messages that don't fit in the protected zone will be summarized.
|
|
105
|
-
"""
|
|
106
|
-
if len(messages) <= 1: # Just system message or empty
|
|
107
|
-
return [], messages
|
|
108
|
-
|
|
109
|
-
# Always protect the system message (first message)
|
|
110
|
-
system_message = messages[0]
|
|
111
|
-
from code_puppy.agents.agent_manager import get_current_agent_config
|
|
112
|
-
current_agent = get_current_agent_config()
|
|
113
|
-
system_tokens = current_agent.estimate_tokens_for_message(system_message)
|
|
114
|
-
|
|
115
|
-
if not with_protection:
|
|
116
|
-
# If not protecting, summarize everything except the system message
|
|
117
|
-
return messages[1:], [system_message]
|
|
118
|
-
|
|
119
|
-
if len(messages) == 1:
|
|
120
|
-
return [], messages
|
|
121
|
-
|
|
122
|
-
# Get the configured protected token count
|
|
123
|
-
protected_tokens_limit = get_protected_token_count()
|
|
124
|
-
|
|
125
|
-
# Calculate tokens for messages from most recent backwards (excluding system message)
|
|
126
|
-
protected_messages = []
|
|
127
|
-
protected_token_count = system_tokens # Start with system message tokens
|
|
128
|
-
|
|
129
|
-
# Go backwards through non-system messages to find protected zone
|
|
130
|
-
for i in range(len(messages) - 1, 0, -1): # Stop at 1, not 0 (skip system message)
|
|
131
|
-
message = messages[i]
|
|
132
|
-
message_tokens = current_agent.estimate_tokens_for_message(message)
|
|
133
|
-
|
|
134
|
-
# If adding this message would exceed protected tokens, stop here
|
|
135
|
-
if protected_token_count + message_tokens > protected_tokens_limit:
|
|
136
|
-
break
|
|
137
|
-
|
|
138
|
-
protected_messages.append(message)
|
|
139
|
-
protected_token_count += message_tokens
|
|
140
|
-
|
|
141
|
-
# Messages that were added while scanning backwards are currently in reverse order.
|
|
142
|
-
# Reverse them to restore chronological ordering, then prepend the system prompt.
|
|
143
|
-
protected_messages.reverse()
|
|
144
|
-
protected_messages.insert(0, system_message)
|
|
145
|
-
|
|
146
|
-
# Messages to summarize are everything between the system message and the
|
|
147
|
-
# protected tail zone we just constructed.
|
|
148
|
-
protected_start_idx = max(1, len(messages) - (len(protected_messages) - 1))
|
|
149
|
-
messages_to_summarize = messages[1:protected_start_idx]
|
|
150
|
-
|
|
151
|
-
emit_info(
|
|
152
|
-
f"🔒 Protecting {len(protected_messages)} recent messages ({protected_token_count} tokens, limit: {protected_tokens_limit})"
|
|
153
|
-
)
|
|
154
|
-
emit_info(f"📝 Summarizing {len(messages_to_summarize)} older messages")
|
|
155
|
-
|
|
156
|
-
return messages_to_summarize, protected_messages
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
def run_summarization_sync(
|
|
160
|
-
instructions: str,
|
|
161
|
-
message_history: List[ModelMessage],
|
|
162
|
-
) -> Union[List[ModelMessage], str]:
|
|
163
|
-
"""
|
|
164
|
-
Run summarization synchronously using the configured summarization agent.
|
|
165
|
-
This is exposed as a global function so tests can mock it.
|
|
166
|
-
"""
|
|
167
|
-
from code_puppy.summarization_agent import run_summarization_sync as _run_summarization_sync
|
|
168
|
-
return _run_summarization_sync(instructions, message_history)
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
def summarize_messages(
|
|
172
|
-
messages: List[ModelMessage], with_protection: bool = True
|
|
173
|
-
) -> Tuple[List[ModelMessage], List[ModelMessage]]:
|
|
174
|
-
"""
|
|
175
|
-
Summarize messages while protecting recent messages up to PROTECTED_TOKENS.
|
|
176
|
-
|
|
177
|
-
Returns:
|
|
178
|
-
Tuple of (compacted_messages, summarized_source_messages)
|
|
179
|
-
where compacted_messages always preserves the original system message
|
|
180
|
-
as the first entry.
|
|
181
|
-
"""
|
|
182
|
-
if not messages:
|
|
183
|
-
return [], []
|
|
184
|
-
|
|
185
|
-
# Split messages into those to summarize and those to protect
|
|
186
|
-
messages_to_summarize, protected_messages = split_messages_for_protected_summarization(
|
|
187
|
-
messages, with_protection
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
# If nothing to summarize, return the original list
|
|
191
|
-
if not messages_to_summarize:
|
|
192
|
-
return prune_interrupted_tool_calls(messages), []
|
|
193
|
-
|
|
194
|
-
# Get the system message (always the first message)
|
|
195
|
-
system_message = messages[0]
|
|
196
|
-
|
|
197
|
-
# Instructions for the summarization agent
|
|
198
|
-
instructions = (
|
|
199
|
-
"The input will be a log of Agentic AI steps that have been taken"
|
|
200
|
-
" as well as user queries, etc. Summarize the contents of these steps."
|
|
201
|
-
" The high level details should remain but the bulk of the content from tool-call"
|
|
202
|
-
" responses should be compacted and summarized. For example if you see a tool-call"
|
|
203
|
-
" reading a file, and the file contents are large, then in your summary you might just"
|
|
204
|
-
" write: * used read_file on space_invaders.cpp - contents removed."
|
|
205
|
-
"\n Make sure your result is a bulleted list of all steps and interactions."
|
|
206
|
-
"\n\nNOTE: This summary represents older conversation history. Recent messages are preserved separately."
|
|
207
|
-
)
|
|
208
|
-
|
|
209
|
-
try:
|
|
210
|
-
# Use the global function so tests can mock it
|
|
211
|
-
new_messages = run_summarization_sync(
|
|
212
|
-
instructions, message_history=messages_to_summarize
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
if not isinstance(new_messages, list):
|
|
216
|
-
emit_warning(
|
|
217
|
-
"Summarization agent returned non-list output; wrapping into message request"
|
|
218
|
-
)
|
|
219
|
-
new_messages = [ModelRequest([TextPart(str(new_messages))])]
|
|
220
|
-
|
|
221
|
-
# Construct compacted messages: system message + new summarized messages + protected tail
|
|
222
|
-
compacted: List[ModelMessage] = [system_message] + list(new_messages)
|
|
223
|
-
|
|
224
|
-
# Drop the system message from protected_messages because we already included it
|
|
225
|
-
protected_tail = [msg for msg in protected_messages if msg is not system_message]
|
|
226
|
-
|
|
227
|
-
compacted.extend(protected_tail)
|
|
228
|
-
|
|
229
|
-
return prune_interrupted_tool_calls(compacted), messages_to_summarize
|
|
230
|
-
except Exception as e:
|
|
231
|
-
emit_error(f"Summarization failed during compaction: {e}")
|
|
232
|
-
return messages, [] # Return original messages on failure
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
def summarize_message(message: ModelMessage) -> ModelMessage:
|
|
236
|
-
# Get current agent to use its method
|
|
237
|
-
from code_puppy.agents.agent_manager import get_current_agent_config
|
|
238
|
-
current_agent = get_current_agent_config()
|
|
239
|
-
|
|
240
|
-
return current_agent.summarize_message(message)
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
def get_model_context_length() -> int:
|
|
244
|
-
"""
|
|
245
|
-
Get the context length for the currently configured model from models.json
|
|
246
|
-
"""
|
|
247
|
-
# Get current agent to use its method
|
|
248
|
-
from code_puppy.agents.agent_manager import get_current_agent_config
|
|
249
|
-
current_agent = get_current_agent_config()
|
|
250
|
-
|
|
251
|
-
return current_agent.get_model_context_length()
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
def prune_interrupted_tool_calls(messages: List[ModelMessage]) -> List[ModelMessage]:
|
|
255
|
-
"""
|
|
256
|
-
Remove any messages that participate in mismatched tool call sequences.
|
|
257
|
-
|
|
258
|
-
A mismatched tool call id is one that appears in a ToolCall (model/tool request)
|
|
259
|
-
without a corresponding tool return, or vice versa. We preserve original order
|
|
260
|
-
and only drop messages that contain parts referencing mismatched tool_call_ids.
|
|
261
|
-
"""
|
|
262
|
-
# Get current agent to use its method
|
|
263
|
-
from code_puppy.agents.agent_manager import get_current_agent_config
|
|
264
|
-
current_agent = get_current_agent_config()
|
|
265
|
-
|
|
266
|
-
return current_agent.prune_interrupted_tool_calls(messages)
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
def message_history_processor(messages: List[ModelMessage]) -> List[ModelMessage]:
|
|
270
|
-
# Get current agent to use its methods
|
|
271
|
-
from code_puppy.agents.agent_manager import get_current_agent_config
|
|
272
|
-
current_agent = get_current_agent_config()
|
|
273
|
-
|
|
274
|
-
cleaned_history = current_agent.prune_interrupted_tool_calls(messages)
|
|
275
|
-
|
|
276
|
-
total_current_tokens = sum(
|
|
277
|
-
current_agent.estimate_tokens_for_message(msg) for msg in cleaned_history
|
|
278
|
-
)
|
|
279
|
-
|
|
280
|
-
model_max = current_agent.get_model_context_length()
|
|
281
|
-
|
|
282
|
-
proportion_used = total_current_tokens / model_max if model_max else 0
|
|
283
|
-
|
|
284
|
-
# Check if we're in TUI mode and can update the status bar
|
|
285
|
-
from code_puppy.tui_state import get_tui_app_instance, is_tui_mode
|
|
286
|
-
|
|
287
|
-
if is_tui_mode():
|
|
288
|
-
tui_app = get_tui_app_instance()
|
|
289
|
-
if tui_app:
|
|
290
|
-
try:
|
|
291
|
-
# Update the status bar instead of emitting a chat message
|
|
292
|
-
status_bar = tui_app.query_one("StatusBar")
|
|
293
|
-
status_bar.update_token_info(
|
|
294
|
-
total_current_tokens, model_max, proportion_used
|
|
295
|
-
)
|
|
296
|
-
except Exception as e:
|
|
297
|
-
emit_error(e)
|
|
298
|
-
# Fallback to chat message if status bar update fails
|
|
299
|
-
emit_info(
|
|
300
|
-
f"\n[bold white on blue] Tokens in context: {total_current_tokens}, total model capacity: {model_max}, proportion used: {proportion_used:.2f} [/bold white on blue] \n",
|
|
301
|
-
message_group="token_context_status",
|
|
302
|
-
)
|
|
303
|
-
else:
|
|
304
|
-
# Fallback if no TUI app instance
|
|
305
|
-
emit_info(
|
|
306
|
-
f"\n[bold white on blue] Tokens in context: {total_current_tokens}, total model capacity: {model_max}, proportion used: {proportion_used:.2f} [/bold white on blue] \n",
|
|
307
|
-
message_group="token_context_status",
|
|
308
|
-
)
|
|
309
|
-
else:
|
|
310
|
-
# Non-TUI mode - emit to console as before
|
|
311
|
-
emit_info(
|
|
312
|
-
f"\n[bold white on blue] Tokens in context: {total_current_tokens}, total model capacity: {model_max}, proportion used: {proportion_used:.2f} [/bold white on blue] \n"
|
|
313
|
-
)
|
|
314
|
-
# Get the configured compaction threshold
|
|
315
|
-
compaction_threshold = get_compaction_threshold()
|
|
316
|
-
|
|
317
|
-
# Get the configured compaction strategy
|
|
318
|
-
compaction_strategy = get_compaction_strategy()
|
|
319
|
-
|
|
320
|
-
if proportion_used > compaction_threshold:
|
|
321
|
-
filtered_history = current_agent.filter_huge_messages(cleaned_history)
|
|
322
|
-
|
|
323
|
-
if compaction_strategy == "truncation":
|
|
324
|
-
protected_tokens = get_protected_token_count()
|
|
325
|
-
result_messages = truncation(filtered_history, protected_tokens)
|
|
326
|
-
summarized_messages: List[ModelMessage] = []
|
|
327
|
-
else:
|
|
328
|
-
result_messages, summarized_messages = summarize_messages(
|
|
329
|
-
filtered_history
|
|
330
|
-
)
|
|
331
|
-
|
|
332
|
-
final_token_count = sum(
|
|
333
|
-
current_agent.estimate_tokens_for_message(msg) for msg in result_messages
|
|
334
|
-
)
|
|
335
|
-
# Update status bar with final token count if in TUI mode
|
|
336
|
-
if is_tui_mode():
|
|
337
|
-
tui_app = get_tui_app_instance()
|
|
338
|
-
if tui_app:
|
|
339
|
-
try:
|
|
340
|
-
status_bar = tui_app.query_one("StatusBar")
|
|
341
|
-
status_bar.update_token_info(
|
|
342
|
-
final_token_count, model_max, final_token_count / model_max
|
|
343
|
-
)
|
|
344
|
-
except Exception:
|
|
345
|
-
emit_info(
|
|
346
|
-
f"Final token count after processing: {final_token_count}",
|
|
347
|
-
message_group="token_context_status",
|
|
348
|
-
)
|
|
349
|
-
else:
|
|
350
|
-
emit_info(
|
|
351
|
-
f"Final token count after processing: {final_token_count}",
|
|
352
|
-
message_group="token_context_status",
|
|
353
|
-
)
|
|
354
|
-
else:
|
|
355
|
-
emit_info(f"Final token count after processing: {final_token_count}")
|
|
356
|
-
set_message_history(result_messages)
|
|
357
|
-
for m in summarized_messages:
|
|
358
|
-
add_compacted_message_hash(current_agent.hash_message(m))
|
|
359
|
-
return result_messages
|
|
360
|
-
|
|
361
|
-
set_message_history(cleaned_history)
|
|
362
|
-
return cleaned_history
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
def truncation(
|
|
366
|
-
messages: List[ModelMessage], protected_tokens: int
|
|
367
|
-
) -> List[ModelMessage]:
|
|
368
|
-
emit_info("Truncating message history to manage token usage")
|
|
369
|
-
result = [messages[0]] # Always keep the first message (system prompt)
|
|
370
|
-
num_tokens = 0
|
|
371
|
-
stack = queue.LifoQueue()
|
|
372
|
-
|
|
373
|
-
# Put messages in reverse order (most recent first) into the stack
|
|
374
|
-
# but break when we exceed protected_tokens
|
|
375
|
-
for idx, msg in enumerate(reversed(messages[1:])): # Skip the first message
|
|
376
|
-
num_tokens += estimate_tokens_for_message(msg)
|
|
377
|
-
if num_tokens > protected_tokens:
|
|
378
|
-
break
|
|
379
|
-
stack.put(msg)
|
|
380
|
-
|
|
381
|
-
# Pop messages from stack to get them in chronological order
|
|
382
|
-
while not stack.empty():
|
|
383
|
-
result.append(stack.get())
|
|
384
|
-
|
|
385
|
-
result = prune_interrupted_tool_calls(result)
|
|
386
|
-
return result
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
def message_history_accumulator(messages: List[Any]):
|
|
390
|
-
existing_history = list(get_message_history())
|
|
391
|
-
|
|
392
|
-
# Get current agent to use its method
|
|
393
|
-
from code_puppy.agents.agent_manager import get_current_agent_config
|
|
394
|
-
current_agent = get_current_agent_config()
|
|
395
|
-
|
|
396
|
-
seen_hashes = {current_agent.hash_message(message) for message in existing_history}
|
|
397
|
-
compacted_hashes = get_compacted_message_hashes()
|
|
398
|
-
|
|
399
|
-
for message in messages:
|
|
400
|
-
message_hash = current_agent.hash_message(message)
|
|
401
|
-
if message_hash in seen_hashes or message_hash in compacted_hashes:
|
|
402
|
-
continue
|
|
403
|
-
existing_history.append(message)
|
|
404
|
-
seen_hashes.add(message_hash)
|
|
405
|
-
|
|
406
|
-
updated_history = message_history_processor(existing_history)
|
|
407
|
-
set_message_history(updated_history)
|
|
408
|
-
return updated_history
|
code_puppy/state_management.py
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
from types import ModuleType
|
|
2
|
-
from typing import Any, List, Set
|
|
3
|
-
|
|
4
|
-
from code_puppy.messaging import emit_info
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def _require_agent_manager() -> ModuleType:
|
|
8
|
-
"""Import the agent manager module, raising if it is unavailable."""
|
|
9
|
-
try:
|
|
10
|
-
from code_puppy.agents import agent_manager
|
|
11
|
-
except Exception as error: # pragma: no cover - import errors surface immediately
|
|
12
|
-
raise RuntimeError("Agent manager module unavailable") from error
|
|
13
|
-
return agent_manager
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def add_compacted_message_hash(message_hash: str) -> None:
|
|
17
|
-
"""Add a message hash to the set of compacted message hashes."""
|
|
18
|
-
manager = _require_agent_manager()
|
|
19
|
-
manager.add_current_agent_compacted_message_hash(message_hash)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def get_compacted_message_hashes() -> Set[str]:
|
|
23
|
-
"""Get the set of compacted message hashes."""
|
|
24
|
-
manager = _require_agent_manager()
|
|
25
|
-
return manager.get_current_agent_compacted_message_hashes()
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def get_message_history() -> List[Any]:
|
|
29
|
-
"""Get message history for the active agent."""
|
|
30
|
-
manager = _require_agent_manager()
|
|
31
|
-
return manager.get_current_agent_message_history()
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def set_message_history(history: List[Any]) -> None:
|
|
35
|
-
"""Replace the message history for the active agent."""
|
|
36
|
-
manager = _require_agent_manager()
|
|
37
|
-
manager.set_current_agent_message_history(history)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def clear_message_history() -> None:
|
|
41
|
-
"""Clear message history for the active agent."""
|
|
42
|
-
manager = _require_agent_manager()
|
|
43
|
-
manager.clear_current_agent_message_history()
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def append_to_message_history(message: Any) -> None:
|
|
47
|
-
"""Append a message to the active agent's history."""
|
|
48
|
-
manager = _require_agent_manager()
|
|
49
|
-
manager.append_to_current_agent_message_history(message)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def extend_message_history(history: List[Any]) -> None:
|
|
53
|
-
"""Extend the active agent's message history."""
|
|
54
|
-
manager = _require_agent_manager()
|
|
55
|
-
manager.extend_current_agent_message_history(history)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|