code-puppy 0.0.341__py3-none-any.whl → 0.0.361__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/agents/__init__.py +2 -0
- code_puppy/agents/agent_manager.py +49 -0
- code_puppy/agents/agent_pack_leader.py +383 -0
- code_puppy/agents/agent_qa_kitten.py +12 -7
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/base_agent.py +34 -252
- code_puppy/agents/event_stream_handler.py +350 -0
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +321 -0
- code_puppy/agents/pack/retriever.py +393 -0
- code_puppy/agents/pack/shepherd.py +348 -0
- code_puppy/agents/pack/terrier.py +287 -0
- code_puppy/agents/pack/watchdog.py +367 -0
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +169 -0
- code_puppy/api/main.py +21 -0
- code_puppy/api/pty_manager.py +446 -0
- code_puppy/api/routers/__init__.py +12 -0
- code_puppy/api/routers/agents.py +36 -0
- code_puppy/api/routers/commands.py +217 -0
- code_puppy/api/routers/config.py +74 -0
- code_puppy/api/routers/sessions.py +232 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +73 -0
- code_puppy/claude_cache_client.py +249 -34
- code_puppy/cli_runner.py +4 -3
- code_puppy/command_line/add_model_menu.py +8 -9
- code_puppy/command_line/core_commands.py +85 -0
- code_puppy/command_line/mcp/catalog_server_installer.py +5 -6
- code_puppy/command_line/mcp/custom_server_form.py +54 -19
- code_puppy/command_line/mcp/custom_server_installer.py +8 -9
- code_puppy/command_line/mcp/handler.py +0 -2
- code_puppy/command_line/mcp/help_command.py +1 -5
- code_puppy/command_line/mcp/start_command.py +36 -18
- code_puppy/command_line/onboarding_slides.py +0 -1
- code_puppy/command_line/prompt_toolkit_completion.py +16 -10
- code_puppy/command_line/utils.py +54 -0
- code_puppy/config.py +66 -62
- code_puppy/mcp_/async_lifecycle.py +35 -4
- code_puppy/mcp_/managed_server.py +49 -20
- code_puppy/mcp_/manager.py +81 -52
- code_puppy/messaging/__init__.py +15 -0
- code_puppy/messaging/message_queue.py +11 -23
- code_puppy/messaging/messages.py +27 -0
- code_puppy/messaging/queue_console.py +1 -1
- code_puppy/messaging/rich_renderer.py +36 -1
- code_puppy/messaging/spinner/__init__.py +20 -2
- code_puppy/messaging/subagent_console.py +461 -0
- code_puppy/model_utils.py +54 -0
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +90 -19
- code_puppy/plugins/antigravity_oauth/transport.py +1 -0
- code_puppy/plugins/frontend_emitter/__init__.py +25 -0
- code_puppy/plugins/frontend_emitter/emitter.py +121 -0
- code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/status_display.py +6 -2
- code_puppy/tools/__init__.py +37 -1
- code_puppy/tools/agent_tools.py +139 -36
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +6 -6
- code_puppy/tools/browser/browser_interactions.py +21 -20
- code_puppy/tools/browser/browser_locators.py +9 -9
- code_puppy/tools/browser/browser_navigation.py +7 -7
- code_puppy/tools/browser/browser_screenshot.py +78 -140
- code_puppy/tools/browser/browser_scripts.py +15 -13
- code_puppy/tools/browser/camoufox_manager.py +226 -64
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +521 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +292 -101
- code_puppy/tools/common.py +176 -1
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/subagent_context.py +158 -0
- {code_puppy-0.0.341.dist-info → code_puppy-0.0.361.dist-info}/METADATA +13 -11
- {code_puppy-0.0.341.dist-info → code_puppy-0.0.361.dist-info}/RECORD +84 -53
- code_puppy/command_line/mcp/add_command.py +0 -170
- code_puppy/tools/browser/vqa_agent.py +0 -90
- {code_puppy-0.0.341.data → code_puppy-0.0.361.data}/data/code_puppy/models.json +0 -0
- {code_puppy-0.0.341.data → code_puppy-0.0.361.data}/data/code_puppy/models_dev_api.json +0 -0
- {code_puppy-0.0.341.dist-info → code_puppy-0.0.361.dist-info}/WHEEL +0 -0
- {code_puppy-0.0.341.dist-info → code_puppy-0.0.361.dist-info}/entry_points.txt +0 -0
- {code_puppy-0.0.341.dist-info → code_puppy-0.0.361.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Commands API endpoints for slash command execution and autocomplete.
|
|
2
|
+
|
|
3
|
+
This router provides REST endpoints for:
|
|
4
|
+
- Listing all available slash commands
|
|
5
|
+
- Getting info about specific commands
|
|
6
|
+
- Executing slash commands
|
|
7
|
+
- Autocomplete suggestions for partial commands
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
12
|
+
from typing import Any, List, Optional
|
|
13
|
+
|
|
14
|
+
from fastapi import APIRouter, HTTPException
|
|
15
|
+
from pydantic import BaseModel
|
|
16
|
+
|
|
17
|
+
# Thread pool for blocking command execution
|
|
18
|
+
_executor = ThreadPoolExecutor(max_workers=4)
|
|
19
|
+
|
|
20
|
+
# Timeout for command execution (seconds)
|
|
21
|
+
COMMAND_TIMEOUT = 30.0
|
|
22
|
+
|
|
23
|
+
router = APIRouter()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# =============================================================================
|
|
27
|
+
# Pydantic Models
|
|
28
|
+
# =============================================================================
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CommandInfo(BaseModel):
|
|
32
|
+
"""Information about a registered command."""
|
|
33
|
+
|
|
34
|
+
name: str
|
|
35
|
+
description: str
|
|
36
|
+
usage: str
|
|
37
|
+
aliases: List[str] = []
|
|
38
|
+
category: str = "core"
|
|
39
|
+
detailed_help: Optional[str] = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CommandExecuteRequest(BaseModel):
|
|
43
|
+
"""Request to execute a slash command."""
|
|
44
|
+
|
|
45
|
+
command: str # Full command string, e.g., "/set model=gpt-4o"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class CommandExecuteResponse(BaseModel):
|
|
49
|
+
"""Response from executing a slash command."""
|
|
50
|
+
|
|
51
|
+
success: bool
|
|
52
|
+
result: Any = None
|
|
53
|
+
error: Optional[str] = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class AutocompleteRequest(BaseModel):
|
|
57
|
+
"""Request for command autocomplete."""
|
|
58
|
+
|
|
59
|
+
partial: str # Partial command string, e.g., "/se" or "/set mo"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class AutocompleteResponse(BaseModel):
|
|
63
|
+
"""Response with autocomplete suggestions."""
|
|
64
|
+
|
|
65
|
+
suggestions: List[str]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# =============================================================================
|
|
69
|
+
# Endpoints
|
|
70
|
+
# =============================================================================
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@router.get("/")
|
|
74
|
+
async def list_commands() -> List[CommandInfo]:
|
|
75
|
+
"""List all available slash commands.
|
|
76
|
+
|
|
77
|
+
Returns a sorted list of all unique commands (no alias duplicates),
|
|
78
|
+
with their metadata including name, description, usage, aliases,
|
|
79
|
+
category, and detailed help.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
List[CommandInfo]: Sorted list of command information.
|
|
83
|
+
"""
|
|
84
|
+
from code_puppy.command_line.command_registry import get_unique_commands
|
|
85
|
+
|
|
86
|
+
commands = []
|
|
87
|
+
for cmd in get_unique_commands():
|
|
88
|
+
commands.append(
|
|
89
|
+
CommandInfo(
|
|
90
|
+
name=cmd.name,
|
|
91
|
+
description=cmd.description,
|
|
92
|
+
usage=cmd.usage,
|
|
93
|
+
aliases=cmd.aliases,
|
|
94
|
+
category=cmd.category,
|
|
95
|
+
detailed_help=cmd.detailed_help,
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
return sorted(commands, key=lambda c: c.name)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@router.get("/{name}")
|
|
102
|
+
async def get_command_info(name: str) -> CommandInfo:
|
|
103
|
+
"""Get detailed info about a specific command.
|
|
104
|
+
|
|
105
|
+
Looks up a command by name or alias (case-insensitive).
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
name: Command name or alias (without leading /).
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
CommandInfo: Full command information.
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
HTTPException: 404 if command not found.
|
|
115
|
+
"""
|
|
116
|
+
from code_puppy.command_line.command_registry import get_command
|
|
117
|
+
|
|
118
|
+
cmd = get_command(name)
|
|
119
|
+
if not cmd:
|
|
120
|
+
raise HTTPException(404, f"Command '/{name}' not found")
|
|
121
|
+
|
|
122
|
+
return CommandInfo(
|
|
123
|
+
name=cmd.name,
|
|
124
|
+
description=cmd.description,
|
|
125
|
+
usage=cmd.usage,
|
|
126
|
+
aliases=cmd.aliases,
|
|
127
|
+
category=cmd.category,
|
|
128
|
+
detailed_help=cmd.detailed_help,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@router.post("/execute")
|
|
133
|
+
async def execute_command(request: CommandExecuteRequest) -> CommandExecuteResponse:
|
|
134
|
+
"""Execute a slash command.
|
|
135
|
+
|
|
136
|
+
Takes a command string (with or without leading /) and executes it
|
|
137
|
+
using the command handler. Runs in a thread pool to avoid blocking
|
|
138
|
+
the event loop, with a timeout to prevent hangs.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
request: CommandExecuteRequest with the command to execute.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
CommandExecuteResponse: Result of command execution.
|
|
145
|
+
"""
|
|
146
|
+
from code_puppy.command_line.command_handler import handle_command
|
|
147
|
+
|
|
148
|
+
command = request.command
|
|
149
|
+
if not command.startswith("/"):
|
|
150
|
+
command = "/" + command
|
|
151
|
+
|
|
152
|
+
loop = asyncio.get_running_loop()
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
# Run blocking command in thread pool with timeout
|
|
156
|
+
result = await asyncio.wait_for(
|
|
157
|
+
loop.run_in_executor(_executor, handle_command, command),
|
|
158
|
+
timeout=COMMAND_TIMEOUT,
|
|
159
|
+
)
|
|
160
|
+
return CommandExecuteResponse(success=True, result=result)
|
|
161
|
+
except asyncio.TimeoutError:
|
|
162
|
+
return CommandExecuteResponse(
|
|
163
|
+
success=False, error=f"Command timed out after {COMMAND_TIMEOUT}s"
|
|
164
|
+
)
|
|
165
|
+
except Exception as e:
|
|
166
|
+
return CommandExecuteResponse(success=False, error=str(e))
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@router.post("/autocomplete")
|
|
170
|
+
async def autocomplete_command(request: AutocompleteRequest) -> AutocompleteResponse:
|
|
171
|
+
"""Get autocomplete suggestions for a partial command.
|
|
172
|
+
|
|
173
|
+
Provides intelligent autocomplete based on partial input:
|
|
174
|
+
- Empty input: returns all command names
|
|
175
|
+
- Partial command name: returns matching commands and aliases
|
|
176
|
+
- Complete command with args: returns usage hint
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
request: AutocompleteRequest with partial command string.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
AutocompleteResponse: List of autocomplete suggestions.
|
|
183
|
+
"""
|
|
184
|
+
from code_puppy.command_line.command_registry import (
|
|
185
|
+
get_command,
|
|
186
|
+
get_unique_commands,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
partial = request.partial.lstrip("/")
|
|
190
|
+
|
|
191
|
+
# If empty, return all command names
|
|
192
|
+
if not partial:
|
|
193
|
+
suggestions = [f"/{cmd.name}" for cmd in get_unique_commands()]
|
|
194
|
+
return AutocompleteResponse(suggestions=sorted(suggestions))
|
|
195
|
+
|
|
196
|
+
# Split into command name and args
|
|
197
|
+
parts = partial.split(maxsplit=1)
|
|
198
|
+
cmd_partial = parts[0].lower()
|
|
199
|
+
|
|
200
|
+
# If just the command name (no space yet), suggest matching commands
|
|
201
|
+
if len(parts) == 1:
|
|
202
|
+
suggestions = []
|
|
203
|
+
for cmd in get_unique_commands():
|
|
204
|
+
if cmd.name.startswith(cmd_partial):
|
|
205
|
+
suggestions.append(f"/{cmd.name}")
|
|
206
|
+
for alias in cmd.aliases:
|
|
207
|
+
if alias.startswith(cmd_partial):
|
|
208
|
+
suggestions.append(f"/{alias}")
|
|
209
|
+
return AutocompleteResponse(suggestions=sorted(set(suggestions)))
|
|
210
|
+
|
|
211
|
+
# Command name complete, suggest based on command type
|
|
212
|
+
# (For now, just return the command usage as a hint)
|
|
213
|
+
cmd = get_command(cmd_partial)
|
|
214
|
+
if cmd:
|
|
215
|
+
return AutocompleteResponse(suggestions=[cmd.usage])
|
|
216
|
+
|
|
217
|
+
return AutocompleteResponse(suggestions=[])
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Configuration management API endpoints."""
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, HTTPException
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
from typing import Any, Dict, List
|
|
6
|
+
|
|
7
|
+
router = APIRouter()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ConfigValue(BaseModel):
|
|
11
|
+
key: str
|
|
12
|
+
value: Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ConfigUpdate(BaseModel):
|
|
16
|
+
value: Any
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@router.get("/")
|
|
20
|
+
async def list_config() -> Dict[str, Any]:
|
|
21
|
+
"""List all configuration keys and their current values."""
|
|
22
|
+
from code_puppy.config import get_config_keys, get_value
|
|
23
|
+
|
|
24
|
+
config = {}
|
|
25
|
+
for key in get_config_keys():
|
|
26
|
+
config[key] = get_value(key)
|
|
27
|
+
return {"config": config}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@router.get("/keys")
|
|
31
|
+
async def get_config_keys_list() -> List[str]:
|
|
32
|
+
"""Get list of all valid configuration keys."""
|
|
33
|
+
from code_puppy.config import get_config_keys
|
|
34
|
+
|
|
35
|
+
return get_config_keys()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@router.get("/{key}")
|
|
39
|
+
async def get_config_value(key: str) -> ConfigValue:
|
|
40
|
+
"""Get a specific configuration value."""
|
|
41
|
+
from code_puppy.config import get_config_keys, get_value
|
|
42
|
+
|
|
43
|
+
valid_keys = get_config_keys()
|
|
44
|
+
if key not in valid_keys:
|
|
45
|
+
raise HTTPException(
|
|
46
|
+
404, f"Config key '{key}' not found. Valid keys: {valid_keys}"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
value = get_value(key)
|
|
50
|
+
return ConfigValue(key=key, value=value)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@router.put("/{key}")
|
|
54
|
+
async def set_config_value(key: str, update: ConfigUpdate) -> ConfigValue:
|
|
55
|
+
"""Set a configuration value."""
|
|
56
|
+
from code_puppy.config import get_config_keys, get_value, set_value
|
|
57
|
+
|
|
58
|
+
valid_keys = get_config_keys()
|
|
59
|
+
if key not in valid_keys:
|
|
60
|
+
raise HTTPException(
|
|
61
|
+
404, f"Config key '{key}' not found. Valid keys: {valid_keys}"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
set_value(key, str(update.value))
|
|
65
|
+
return ConfigValue(key=key, value=get_value(key))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@router.delete("/{key}")
|
|
69
|
+
async def reset_config_value(key: str) -> Dict[str, str]:
|
|
70
|
+
"""Reset a configuration value to default (remove from config file)."""
|
|
71
|
+
from code_puppy.config import reset_value
|
|
72
|
+
|
|
73
|
+
reset_value(key)
|
|
74
|
+
return {"message": f"Config key '{key}' reset to default"}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""Sessions API endpoints for retrieving subagent session data."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import pickle
|
|
6
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
from fastapi import APIRouter, HTTPException
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
# Thread pool for blocking file I/O
|
|
14
|
+
_executor = ThreadPoolExecutor(max_workers=2)
|
|
15
|
+
|
|
16
|
+
# Timeout for file operations (seconds)
|
|
17
|
+
FILE_IO_TIMEOUT = 10.0
|
|
18
|
+
|
|
19
|
+
router = APIRouter()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SessionInfo(BaseModel):
|
|
23
|
+
"""Session metadata information."""
|
|
24
|
+
|
|
25
|
+
session_id: str
|
|
26
|
+
agent_name: Optional[str] = None
|
|
27
|
+
initial_prompt: Optional[str] = None
|
|
28
|
+
created_at: Optional[str] = None
|
|
29
|
+
last_updated: Optional[str] = None
|
|
30
|
+
message_count: int = 0
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class MessageContent(BaseModel):
|
|
34
|
+
"""Message content with role and optional timestamp."""
|
|
35
|
+
|
|
36
|
+
role: str
|
|
37
|
+
content: Any
|
|
38
|
+
timestamp: Optional[str] = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class SessionDetail(SessionInfo):
|
|
42
|
+
"""Session info with full message history."""
|
|
43
|
+
|
|
44
|
+
messages: List[Dict[str, Any]] = []
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _get_sessions_dir() -> Path:
|
|
48
|
+
"""Get the subagent sessions directory.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Path to the subagent sessions directory
|
|
52
|
+
"""
|
|
53
|
+
from code_puppy.config import DATA_DIR
|
|
54
|
+
|
|
55
|
+
return Path(DATA_DIR) / "subagent_sessions"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _serialize_message(msg: Any) -> Dict[str, Any]:
|
|
59
|
+
"""Serialize a pydantic-ai message to a JSON-safe dict.
|
|
60
|
+
|
|
61
|
+
Handles various pydantic-ai message types that may be stored
|
|
62
|
+
in the pickle files.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
msg: A pydantic-ai message object
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
JSON-serializable dictionary representation of the message
|
|
69
|
+
"""
|
|
70
|
+
# Handle pydantic v2 models with model_dump
|
|
71
|
+
if hasattr(msg, "model_dump"):
|
|
72
|
+
return msg.model_dump(mode="json")
|
|
73
|
+
# Handle objects with __dict__ (convert values to strings for safety)
|
|
74
|
+
elif hasattr(msg, "__dict__"):
|
|
75
|
+
return {k: str(v) for k, v in msg.__dict__.items()}
|
|
76
|
+
# Fallback: wrap in a content dict
|
|
77
|
+
else:
|
|
78
|
+
return {"content": str(msg)}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _load_json_sync(file_path: Path) -> dict:
|
|
82
|
+
"""Synchronous JSON file load (for use in executor)."""
|
|
83
|
+
with open(file_path, "r") as f:
|
|
84
|
+
return json.load(f)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _load_pickle_sync(file_path: Path) -> Any:
|
|
88
|
+
"""Synchronous pickle file load (for use in executor)."""
|
|
89
|
+
with open(file_path, "rb") as f:
|
|
90
|
+
return pickle.load(f)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@router.get("/")
|
|
94
|
+
async def list_sessions() -> List[SessionInfo]:
|
|
95
|
+
"""List all available sessions.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
List of SessionInfo objects for each session found
|
|
99
|
+
"""
|
|
100
|
+
sessions_dir = _get_sessions_dir()
|
|
101
|
+
if not sessions_dir.exists():
|
|
102
|
+
return []
|
|
103
|
+
|
|
104
|
+
loop = asyncio.get_running_loop()
|
|
105
|
+
sessions = []
|
|
106
|
+
|
|
107
|
+
for txt_file in sessions_dir.glob("*.txt"):
|
|
108
|
+
session_id = txt_file.stem
|
|
109
|
+
try:
|
|
110
|
+
# Run blocking I/O in thread pool with timeout
|
|
111
|
+
metadata = await asyncio.wait_for(
|
|
112
|
+
loop.run_in_executor(_executor, _load_json_sync, txt_file),
|
|
113
|
+
timeout=FILE_IO_TIMEOUT,
|
|
114
|
+
)
|
|
115
|
+
sessions.append(
|
|
116
|
+
SessionInfo(
|
|
117
|
+
session_id=session_id,
|
|
118
|
+
agent_name=metadata.get("agent_name"),
|
|
119
|
+
initial_prompt=metadata.get("initial_prompt"),
|
|
120
|
+
created_at=metadata.get("created_at"),
|
|
121
|
+
last_updated=metadata.get("last_updated"),
|
|
122
|
+
message_count=metadata.get("message_count", 0),
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
except asyncio.TimeoutError:
|
|
126
|
+
# Timed out reading file, include basic info
|
|
127
|
+
sessions.append(SessionInfo(session_id=session_id))
|
|
128
|
+
except Exception:
|
|
129
|
+
# If we can't parse metadata, still include basic session info
|
|
130
|
+
sessions.append(SessionInfo(session_id=session_id))
|
|
131
|
+
|
|
132
|
+
return sessions
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@router.get("/{session_id}")
|
|
136
|
+
async def get_session(session_id: str) -> SessionInfo:
|
|
137
|
+
"""Get session metadata.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
session_id: The session identifier
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
SessionInfo with metadata for the specified session
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
HTTPException: 404 if session not found, 504 on timeout
|
|
147
|
+
"""
|
|
148
|
+
sessions_dir = _get_sessions_dir()
|
|
149
|
+
txt_file = sessions_dir / f"{session_id}.txt"
|
|
150
|
+
|
|
151
|
+
if not txt_file.exists():
|
|
152
|
+
raise HTTPException(404, f"Session '{session_id}' not found")
|
|
153
|
+
|
|
154
|
+
loop = asyncio.get_running_loop()
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
metadata = await asyncio.wait_for(
|
|
158
|
+
loop.run_in_executor(_executor, _load_json_sync, txt_file),
|
|
159
|
+
timeout=FILE_IO_TIMEOUT,
|
|
160
|
+
)
|
|
161
|
+
except asyncio.TimeoutError:
|
|
162
|
+
raise HTTPException(504, f"Timeout reading session '{session_id}'")
|
|
163
|
+
|
|
164
|
+
return SessionInfo(
|
|
165
|
+
session_id=session_id,
|
|
166
|
+
agent_name=metadata.get("agent_name"),
|
|
167
|
+
initial_prompt=metadata.get("initial_prompt"),
|
|
168
|
+
created_at=metadata.get("created_at"),
|
|
169
|
+
last_updated=metadata.get("last_updated"),
|
|
170
|
+
message_count=metadata.get("message_count", 0),
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@router.get("/{session_id}/messages")
|
|
175
|
+
async def get_session_messages(session_id: str) -> List[Dict[str, Any]]:
|
|
176
|
+
"""Get the full message history for a session.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
session_id: The session identifier
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
List of serialized message dictionaries
|
|
183
|
+
|
|
184
|
+
Raises:
|
|
185
|
+
HTTPException: 404 if session messages not found, 500 on load error, 504 on timeout
|
|
186
|
+
"""
|
|
187
|
+
sessions_dir = _get_sessions_dir()
|
|
188
|
+
pkl_file = sessions_dir / f"{session_id}.pkl"
|
|
189
|
+
|
|
190
|
+
if not pkl_file.exists():
|
|
191
|
+
raise HTTPException(404, f"Session '{session_id}' messages not found")
|
|
192
|
+
|
|
193
|
+
loop = asyncio.get_running_loop()
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
messages = await asyncio.wait_for(
|
|
197
|
+
loop.run_in_executor(_executor, _load_pickle_sync, pkl_file),
|
|
198
|
+
timeout=FILE_IO_TIMEOUT,
|
|
199
|
+
)
|
|
200
|
+
return [_serialize_message(msg) for msg in messages]
|
|
201
|
+
except asyncio.TimeoutError:
|
|
202
|
+
raise HTTPException(504, f"Timeout loading session '{session_id}' messages")
|
|
203
|
+
except Exception as e:
|
|
204
|
+
raise HTTPException(500, f"Error loading session messages: {e}")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@router.delete("/{session_id}")
|
|
208
|
+
async def delete_session(session_id: str) -> Dict[str, str]:
|
|
209
|
+
"""Delete a session and its data.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
session_id: The session identifier
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Success message dict
|
|
216
|
+
|
|
217
|
+
Raises:
|
|
218
|
+
HTTPException: 404 if session not found
|
|
219
|
+
"""
|
|
220
|
+
sessions_dir = _get_sessions_dir()
|
|
221
|
+
txt_file = sessions_dir / f"{session_id}.txt"
|
|
222
|
+
pkl_file = sessions_dir / f"{session_id}.pkl"
|
|
223
|
+
|
|
224
|
+
if not txt_file.exists() and not pkl_file.exists():
|
|
225
|
+
raise HTTPException(404, f"Session '{session_id}' not found")
|
|
226
|
+
|
|
227
|
+
if txt_file.exists():
|
|
228
|
+
txt_file.unlink()
|
|
229
|
+
if pkl_file.exists():
|
|
230
|
+
pkl_file.unlink()
|
|
231
|
+
|
|
232
|
+
return {"message": f"Session '{session_id}' deleted"}
|