overcode 0.1.0__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.
- overcode/__init__.py +5 -0
- overcode/cli.py +812 -0
- overcode/config.py +72 -0
- overcode/daemon.py +1184 -0
- overcode/daemon_claude_skill.md +180 -0
- overcode/daemon_state.py +113 -0
- overcode/data_export.py +257 -0
- overcode/dependency_check.py +227 -0
- overcode/exceptions.py +219 -0
- overcode/history_reader.py +448 -0
- overcode/implementations.py +214 -0
- overcode/interfaces.py +49 -0
- overcode/launcher.py +434 -0
- overcode/logging_config.py +193 -0
- overcode/mocks.py +152 -0
- overcode/monitor_daemon.py +808 -0
- overcode/monitor_daemon_state.py +358 -0
- overcode/pid_utils.py +225 -0
- overcode/presence_logger.py +454 -0
- overcode/protocols.py +143 -0
- overcode/session_manager.py +606 -0
- overcode/settings.py +412 -0
- overcode/standing_instructions.py +276 -0
- overcode/status_constants.py +190 -0
- overcode/status_detector.py +339 -0
- overcode/status_history.py +164 -0
- overcode/status_patterns.py +264 -0
- overcode/summarizer_client.py +136 -0
- overcode/summarizer_component.py +312 -0
- overcode/supervisor_daemon.py +1000 -0
- overcode/supervisor_layout.sh +50 -0
- overcode/tmux_manager.py +228 -0
- overcode/tui.py +2549 -0
- overcode/tui_helpers.py +495 -0
- overcode/web_api.py +279 -0
- overcode/web_server.py +138 -0
- overcode/web_templates.py +563 -0
- overcode-0.1.0.dist-info/METADATA +87 -0
- overcode-0.1.0.dist-info/RECORD +43 -0
- overcode-0.1.0.dist-info/WHEEL +5 -0
- overcode-0.1.0.dist-info/entry_points.txt +2 -0
- overcode-0.1.0.dist-info/licenses/LICENSE +21 -0
- overcode-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Overcode Supervisor Skill
|
|
2
|
+
|
|
3
|
+
You are the Overcode supervisor agent. Your mission: **Make all RED sessions GREEN**.
|
|
4
|
+
|
|
5
|
+
## Your Role
|
|
6
|
+
|
|
7
|
+
You monitor and unblock Claude agent sessions running in tmux. When sessions get stuck (RED status), you help them make progress by:
|
|
8
|
+
- Reading their output to understand what they're stuck on
|
|
9
|
+
- Making decisions based on their autopilot instructions
|
|
10
|
+
- Approving safe permission requests
|
|
11
|
+
- Sending guidance or clarifying information
|
|
12
|
+
- Having multi-turn conversations with agents
|
|
13
|
+
|
|
14
|
+
**When all sessions are GREEN, your job is done - exit successfully.**
|
|
15
|
+
|
|
16
|
+
## How to Control Sessions (Recommended)
|
|
17
|
+
|
|
18
|
+
Use the `overcode` CLI commands - they're simpler than raw tmux:
|
|
19
|
+
|
|
20
|
+
### Check Status
|
|
21
|
+
```bash
|
|
22
|
+
# List all agents with status
|
|
23
|
+
overcode list
|
|
24
|
+
|
|
25
|
+
# See what an agent is stuck on
|
|
26
|
+
overcode show my-agent
|
|
27
|
+
overcode show my-agent --lines 100 # more context
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Unblock Agents
|
|
31
|
+
```bash
|
|
32
|
+
# Send a text response (+ Enter)
|
|
33
|
+
overcode send my-agent "yes"
|
|
34
|
+
overcode send my-agent "Focus on the core feature first"
|
|
35
|
+
|
|
36
|
+
# Approve a permission request (press Enter)
|
|
37
|
+
overcode send my-agent enter
|
|
38
|
+
|
|
39
|
+
# Reject a permission request (press Escape)
|
|
40
|
+
overcode send my-agent escape
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Alternative: Direct Tmux Commands
|
|
44
|
+
|
|
45
|
+
For fine-grained control, use tmux directly:
|
|
46
|
+
|
|
47
|
+
### Read Session Output
|
|
48
|
+
```bash
|
|
49
|
+
# Read last 50 lines from a session's pane
|
|
50
|
+
tmux capture-pane -t agents:{window_num} -p -S -50
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Send Text to Session
|
|
54
|
+
```bash
|
|
55
|
+
# Send text (no Enter)
|
|
56
|
+
tmux send-keys -t agents:{window_num} "your text here"
|
|
57
|
+
|
|
58
|
+
# Send text with Enter
|
|
59
|
+
tmux send-keys -t agents:{window_num} "your text here" C-m
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Approve/Reject Permissions
|
|
63
|
+
```bash
|
|
64
|
+
# Approve (press Enter)
|
|
65
|
+
tmux send-keys -t agents:{window_num} "" C-m
|
|
66
|
+
|
|
67
|
+
# Reject (press Escape)
|
|
68
|
+
tmux send-keys -t agents:{window_num} Escape
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### List All Sessions
|
|
72
|
+
```bash
|
|
73
|
+
# See all windows and their status
|
|
74
|
+
tmux list-windows -t agents
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Session State Information
|
|
78
|
+
|
|
79
|
+
Session states are tracked in `~/.overcode/sessions/sessions.json`. Read this to understand:
|
|
80
|
+
- Session name, window number, autopilot instructions
|
|
81
|
+
- Current status (running/waiting)
|
|
82
|
+
- Standing instructions for the session
|
|
83
|
+
- Repo context and working directory
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# View all session state
|
|
87
|
+
cat ~/.overcode/sessions/sessions.json | jq
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Approval Rules
|
|
91
|
+
|
|
92
|
+
You must follow these rules when deciding to approve operations:
|
|
93
|
+
|
|
94
|
+
### ✅ Auto-Approve (Safe Operations)
|
|
95
|
+
- Read, Write, Edit, Grep, Glob within the session's working directory
|
|
96
|
+
- WebFetch (read-only web requests)
|
|
97
|
+
- git add, git commit, git status, git diff
|
|
98
|
+
- npm install, pip install (dependency management)
|
|
99
|
+
- Running tests (pytest, npm test, etc.)
|
|
100
|
+
|
|
101
|
+
### ⚠️ Use Judgment (Check Context)
|
|
102
|
+
- git push (only if work is complete and tests pass)
|
|
103
|
+
- Operations near but not in working directory
|
|
104
|
+
- Creating files outside project structure
|
|
105
|
+
|
|
106
|
+
### ❌ Reject (Unsafe/Out of Scope)
|
|
107
|
+
- Operations outside the working directory entirely
|
|
108
|
+
- rm -rf on large directories
|
|
109
|
+
- Operations on user's personal files (/Users/{user}/Documents, etc.)
|
|
110
|
+
- Network writes to external services (unless explicitly in autopilot goal)
|
|
111
|
+
|
|
112
|
+
## Workflow Example
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
# 1. Read current session states
|
|
116
|
+
cat ~/.overcode/sessions/sessions.json | jq '.[] | {name, tmux_window, standing_instructions, stats}'
|
|
117
|
+
|
|
118
|
+
# 2. Find RED sessions
|
|
119
|
+
# Check TUI or parse status
|
|
120
|
+
|
|
121
|
+
# 3. For each RED session, read output
|
|
122
|
+
tmux capture-pane -t agents:1 -p -S -100
|
|
123
|
+
|
|
124
|
+
# 4. Make decision based on:
|
|
125
|
+
# - What they're stuck on
|
|
126
|
+
# - Their autopilot instruction
|
|
127
|
+
# - Approval rules
|
|
128
|
+
|
|
129
|
+
# 5a. If permission request is safe, approve:
|
|
130
|
+
tmux send-keys -t agents:1 "" C-m
|
|
131
|
+
|
|
132
|
+
# 5b. If they need guidance, send message:
|
|
133
|
+
tmux send-keys -t agents:1 "Focus on the core feature first, implement error handling later." C-m
|
|
134
|
+
|
|
135
|
+
# 5c. If permission unsafe, reject:
|
|
136
|
+
tmux send-keys -t agents:1 Escape
|
|
137
|
+
|
|
138
|
+
# 6. Log your action
|
|
139
|
+
echo "$(date): Approved Write permission for recipe-book session (within working dir)" >> ~/.overcode/supervisor.log
|
|
140
|
+
|
|
141
|
+
# 7. Repeat for other RED sessions
|
|
142
|
+
|
|
143
|
+
# 8. When all GREEN, exit
|
|
144
|
+
exit 0
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Real Example
|
|
148
|
+
|
|
149
|
+
**Session:** recipe-book
|
|
150
|
+
**Window:** 1
|
|
151
|
+
**Autopilot:** "Keep organizing recipes into categories"
|
|
152
|
+
**Stuck on:** Permission to write `/home/user/recipes/desserts.md`
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
# Read output to see context
|
|
156
|
+
tmux capture-pane -t agents:1 -p -S -100
|
|
157
|
+
|
|
158
|
+
# Decision: File is within working directory (/home/user/recipes)
|
|
159
|
+
# Decision: Aligns with "organizing recipes into categories"
|
|
160
|
+
# Decision: APPROVE
|
|
161
|
+
|
|
162
|
+
# Execute approval
|
|
163
|
+
tmux send-keys -t agents:1 "" C-m
|
|
164
|
+
|
|
165
|
+
# Log action
|
|
166
|
+
echo "$(date): recipe-book - Approved write to desserts.md (within working dir, aligns with goal)" >> ~/.overcode/supervisor.log
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Your Process
|
|
170
|
+
|
|
171
|
+
1. **Survey** - Read all session states from sessions.json
|
|
172
|
+
2. **Identify** - Find RED sessions (waiting for user)
|
|
173
|
+
3. **Investigate** - Read their tmux output to see what they're stuck on
|
|
174
|
+
4. **Decide** - Apply approval rules and autopilot context
|
|
175
|
+
5. **Act** - Send tmux commands to unblock them
|
|
176
|
+
6. **Log** - Record your decisions
|
|
177
|
+
7. **Repeat** - Check if more sessions need help
|
|
178
|
+
8. **Exit** - When all GREEN, your job is complete
|
|
179
|
+
|
|
180
|
+
Remember: You're a decision-making agent that helps other agents make progress. Be helpful but safe. When in doubt, err on the side of caution.
|
overcode/daemon_state.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Daemon state management.
|
|
3
|
+
|
|
4
|
+
Tracks and persists daemon state for communication with the TUI.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from .settings import PATHS, DAEMON
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Daemon operation modes
|
|
17
|
+
MODE_OFF = "off" # Daemon not running
|
|
18
|
+
MODE_MONITOR = "monitor" # Track stats/state but never launch supervisor claude
|
|
19
|
+
MODE_SUPERVISE = "supervise" # Full supervision (monitor + launch claude when needed)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class DaemonState:
|
|
24
|
+
"""Tracks daemon state for TUI display.
|
|
25
|
+
|
|
26
|
+
This class is used by:
|
|
27
|
+
- The daemon to persist its current state
|
|
28
|
+
- The TUI to read and display daemon status
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
loop_count: int = 0
|
|
32
|
+
current_interval: int = field(default_factory=lambda: DAEMON.interval_fast)
|
|
33
|
+
last_loop_time: Optional[datetime] = None
|
|
34
|
+
started_at: Optional[datetime] = None
|
|
35
|
+
status: str = "starting" # starting, active, idle, sleeping, waiting, supervising, no_agents, stopped
|
|
36
|
+
last_activity: Optional[datetime] = None
|
|
37
|
+
daemon_claude_launches: int = 0
|
|
38
|
+
mode: str = MODE_SUPERVISE # off, monitor, supervise
|
|
39
|
+
|
|
40
|
+
def to_dict(self) -> dict:
|
|
41
|
+
"""Convert state to dictionary for JSON serialization."""
|
|
42
|
+
return {
|
|
43
|
+
"loop_count": self.loop_count,
|
|
44
|
+
"current_interval": self.current_interval,
|
|
45
|
+
"last_loop_time": self.last_loop_time.isoformat() if self.last_loop_time else None,
|
|
46
|
+
"started_at": self.started_at.isoformat() if self.started_at else None,
|
|
47
|
+
"status": self.status,
|
|
48
|
+
"last_activity": self.last_activity.isoformat() if self.last_activity else None,
|
|
49
|
+
"daemon_claude_launches": self.daemon_claude_launches,
|
|
50
|
+
"mode": self.mode,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def from_dict(cls, data: dict) -> "DaemonState":
|
|
55
|
+
"""Create state from dictionary."""
|
|
56
|
+
state = cls()
|
|
57
|
+
state.loop_count = data.get("loop_count", 0)
|
|
58
|
+
state.current_interval = data.get("current_interval", DAEMON.interval_fast)
|
|
59
|
+
state.status = data.get("status", "unknown")
|
|
60
|
+
state.daemon_claude_launches = data.get("daemon_claude_launches", 0)
|
|
61
|
+
state.mode = data.get("mode", MODE_SUPERVISE)
|
|
62
|
+
|
|
63
|
+
if data.get("last_loop_time"):
|
|
64
|
+
state.last_loop_time = datetime.fromisoformat(data["last_loop_time"])
|
|
65
|
+
if data.get("started_at"):
|
|
66
|
+
state.started_at = datetime.fromisoformat(data["started_at"])
|
|
67
|
+
if data.get("last_activity"):
|
|
68
|
+
state.last_activity = datetime.fromisoformat(data["last_activity"])
|
|
69
|
+
|
|
70
|
+
return state
|
|
71
|
+
|
|
72
|
+
def save(self, state_file: Optional[Path] = None) -> None:
|
|
73
|
+
"""Save state to file for TUI to read.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
state_file: Optional path override (for testing)
|
|
77
|
+
"""
|
|
78
|
+
path = state_file or PATHS.daemon_state
|
|
79
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
with open(path, 'w') as f:
|
|
81
|
+
json.dump(self.to_dict(), f, indent=2)
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def load(cls, state_file: Optional[Path] = None) -> Optional["DaemonState"]:
|
|
85
|
+
"""Load state from file (used by TUI).
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
state_file: Optional path override (for testing)
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
DaemonState if file exists and is valid, None otherwise
|
|
92
|
+
"""
|
|
93
|
+
path = state_file or PATHS.daemon_state
|
|
94
|
+
if not path.exists():
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
with open(path) as f:
|
|
99
|
+
data = json.load(f)
|
|
100
|
+
return cls.from_dict(data)
|
|
101
|
+
except (json.JSONDecodeError, KeyError, ValueError):
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def get_daemon_state() -> Optional[DaemonState]:
|
|
106
|
+
"""Get the current daemon state from file.
|
|
107
|
+
|
|
108
|
+
Convenience function for TUI and other consumers.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
DaemonState if daemon is running and state file exists, None otherwise
|
|
112
|
+
"""
|
|
113
|
+
return DaemonState.load()
|
overcode/data_export.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data export functionality for Overcode.
|
|
3
|
+
|
|
4
|
+
Exports session data to Parquet format for analysis in Jupyter notebooks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, Any, Optional
|
|
10
|
+
|
|
11
|
+
from .session_manager import SessionManager
|
|
12
|
+
from .status_history import read_agent_status_history
|
|
13
|
+
from .presence_logger import read_presence_history
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def export_to_parquet(
|
|
17
|
+
output_path: str,
|
|
18
|
+
include_archived: bool = True,
|
|
19
|
+
include_timeline: bool = True,
|
|
20
|
+
include_presence: bool = True,
|
|
21
|
+
) -> Dict[str, Any]:
|
|
22
|
+
"""Export overcode data to Parquet format.
|
|
23
|
+
|
|
24
|
+
Creates a multi-table parquet file suitable for pandas analysis.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
output_path: Path to output parquet file
|
|
28
|
+
include_archived: Include archived sessions
|
|
29
|
+
include_timeline: Include agent status timeline
|
|
30
|
+
include_presence: Include user presence data
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Dict with counts of exported data
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
ImportError: If pyarrow is not installed
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
import pyarrow as pa
|
|
40
|
+
import pyarrow.parquet as pq
|
|
41
|
+
except ImportError:
|
|
42
|
+
raise ImportError(
|
|
43
|
+
"pyarrow is required for parquet export. "
|
|
44
|
+
"Install it with: pip install pyarrow"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
sessions = SessionManager()
|
|
48
|
+
result = {
|
|
49
|
+
"sessions_count": 0,
|
|
50
|
+
"archived_count": 0,
|
|
51
|
+
"timeline_rows": 0,
|
|
52
|
+
"presence_rows": 0,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# Collect session data
|
|
56
|
+
session_records = []
|
|
57
|
+
|
|
58
|
+
# Active sessions
|
|
59
|
+
for s in sessions.list_sessions():
|
|
60
|
+
record = _session_to_record(s, is_archived=False)
|
|
61
|
+
session_records.append(record)
|
|
62
|
+
|
|
63
|
+
result["sessions_count"] = len(session_records)
|
|
64
|
+
|
|
65
|
+
# Archived sessions
|
|
66
|
+
if include_archived:
|
|
67
|
+
archived = sessions.list_archived_sessions()
|
|
68
|
+
for s in archived:
|
|
69
|
+
record = _session_to_record(s, is_archived=True)
|
|
70
|
+
record["end_time"] = getattr(s, "_end_time", None)
|
|
71
|
+
session_records.append(record)
|
|
72
|
+
result["archived_count"] = len(archived)
|
|
73
|
+
|
|
74
|
+
# Build sessions table
|
|
75
|
+
sessions_table = _build_sessions_table(session_records)
|
|
76
|
+
|
|
77
|
+
# Build timeline table
|
|
78
|
+
timeline_table = None
|
|
79
|
+
if include_timeline:
|
|
80
|
+
timeline_records = _build_timeline_records()
|
|
81
|
+
if timeline_records:
|
|
82
|
+
timeline_table = _build_timeline_table(timeline_records)
|
|
83
|
+
result["timeline_rows"] = len(timeline_records)
|
|
84
|
+
|
|
85
|
+
# Build presence table
|
|
86
|
+
presence_table = None
|
|
87
|
+
if include_presence:
|
|
88
|
+
presence_records = _build_presence_records()
|
|
89
|
+
if presence_records:
|
|
90
|
+
presence_table = _build_presence_table(presence_records)
|
|
91
|
+
result["presence_rows"] = len(presence_records)
|
|
92
|
+
|
|
93
|
+
# Write to parquet
|
|
94
|
+
# Use a directory-based approach for multiple tables
|
|
95
|
+
output = Path(output_path)
|
|
96
|
+
if output.suffix != ".parquet":
|
|
97
|
+
output = output.with_suffix(".parquet")
|
|
98
|
+
|
|
99
|
+
# For simplicity, write sessions as the main table
|
|
100
|
+
# with timeline and presence as separate files if requested
|
|
101
|
+
pq.write_table(sessions_table, output)
|
|
102
|
+
|
|
103
|
+
# Write additional tables as separate files
|
|
104
|
+
if timeline_table is not None:
|
|
105
|
+
timeline_path = output.with_stem(output.stem + "_timeline")
|
|
106
|
+
pq.write_table(timeline_table, timeline_path)
|
|
107
|
+
|
|
108
|
+
if presence_table is not None:
|
|
109
|
+
presence_path = output.with_stem(output.stem + "_presence")
|
|
110
|
+
pq.write_table(presence_table, presence_path)
|
|
111
|
+
|
|
112
|
+
return result
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _session_to_record(session, is_archived: bool) -> Dict[str, Any]:
|
|
116
|
+
"""Convert a Session to a flat dictionary record."""
|
|
117
|
+
stats = session.stats
|
|
118
|
+
return {
|
|
119
|
+
"id": session.id,
|
|
120
|
+
"name": session.name,
|
|
121
|
+
"tmux_session": session.tmux_session,
|
|
122
|
+
"tmux_window": session.tmux_window,
|
|
123
|
+
"start_directory": session.start_directory,
|
|
124
|
+
"start_time": session.start_time,
|
|
125
|
+
"end_time": None, # Set for archived
|
|
126
|
+
"repo_name": session.repo_name,
|
|
127
|
+
"branch": session.branch,
|
|
128
|
+
"status": session.status,
|
|
129
|
+
"is_archived": is_archived,
|
|
130
|
+
"permissiveness_mode": session.permissiveness_mode,
|
|
131
|
+
"standing_instructions": session.standing_instructions,
|
|
132
|
+
"standing_instructions_preset": session.standing_instructions_preset,
|
|
133
|
+
# Stats
|
|
134
|
+
"interaction_count": stats.interaction_count,
|
|
135
|
+
"estimated_cost_usd": stats.estimated_cost_usd,
|
|
136
|
+
"total_tokens": stats.total_tokens,
|
|
137
|
+
"input_tokens": stats.input_tokens,
|
|
138
|
+
"output_tokens": stats.output_tokens,
|
|
139
|
+
"cache_creation_tokens": stats.cache_creation_tokens,
|
|
140
|
+
"cache_read_tokens": stats.cache_read_tokens,
|
|
141
|
+
"steers_count": stats.steers_count,
|
|
142
|
+
"last_activity": stats.last_activity,
|
|
143
|
+
"current_task": stats.current_task,
|
|
144
|
+
"current_state": stats.current_state,
|
|
145
|
+
"state_since": stats.state_since,
|
|
146
|
+
"green_time_seconds": stats.green_time_seconds,
|
|
147
|
+
"non_green_time_seconds": stats.non_green_time_seconds,
|
|
148
|
+
"last_stats_update": stats.last_stats_update,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _build_sessions_table(records):
|
|
153
|
+
"""Build a PyArrow table from session records."""
|
|
154
|
+
import pyarrow as pa
|
|
155
|
+
|
|
156
|
+
if not records:
|
|
157
|
+
# Return empty table with schema
|
|
158
|
+
schema = pa.schema([
|
|
159
|
+
("id", pa.string()),
|
|
160
|
+
("name", pa.string()),
|
|
161
|
+
("start_time", pa.string()),
|
|
162
|
+
("end_time", pa.string()),
|
|
163
|
+
("is_archived", pa.bool_()),
|
|
164
|
+
("interaction_count", pa.int64()),
|
|
165
|
+
("total_tokens", pa.int64()),
|
|
166
|
+
("estimated_cost_usd", pa.float64()),
|
|
167
|
+
("green_time_seconds", pa.float64()),
|
|
168
|
+
("non_green_time_seconds", pa.float64()),
|
|
169
|
+
])
|
|
170
|
+
# Create empty arrays for each column
|
|
171
|
+
empty_arrays = {field.name: pa.array([], type=field.type) for field in schema}
|
|
172
|
+
return pa.table(empty_arrays, schema=schema)
|
|
173
|
+
|
|
174
|
+
# Build arrays from records
|
|
175
|
+
arrays = {}
|
|
176
|
+
for key in records[0].keys():
|
|
177
|
+
values = [r.get(key) for r in records]
|
|
178
|
+
arrays[key] = values
|
|
179
|
+
|
|
180
|
+
return pa.table(arrays)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _build_timeline_records():
|
|
184
|
+
"""Build timeline records from agent status history."""
|
|
185
|
+
records = []
|
|
186
|
+
|
|
187
|
+
# Read last 24 hours of timeline data
|
|
188
|
+
# Returns List[Tuple[datetime, agent, status, activity]]
|
|
189
|
+
history = read_agent_status_history(hours=24.0)
|
|
190
|
+
|
|
191
|
+
for ts, agent_name, status, activity in history:
|
|
192
|
+
records.append({
|
|
193
|
+
"timestamp": ts.isoformat() if isinstance(ts, datetime) else str(ts),
|
|
194
|
+
"agent": agent_name,
|
|
195
|
+
"status": status,
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
return records
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _build_timeline_table(records):
|
|
202
|
+
"""Build a PyArrow table from timeline records."""
|
|
203
|
+
import pyarrow as pa
|
|
204
|
+
|
|
205
|
+
if not records:
|
|
206
|
+
schema = pa.schema([
|
|
207
|
+
("timestamp", pa.string()),
|
|
208
|
+
("agent", pa.string()),
|
|
209
|
+
("status", pa.string()),
|
|
210
|
+
])
|
|
211
|
+
empty_arrays = {field.name: pa.array([], type=field.type) for field in schema}
|
|
212
|
+
return pa.table(empty_arrays, schema=schema)
|
|
213
|
+
|
|
214
|
+
arrays = {}
|
|
215
|
+
for key in records[0].keys():
|
|
216
|
+
values = [r.get(key) for r in records]
|
|
217
|
+
arrays[key] = values
|
|
218
|
+
|
|
219
|
+
return pa.table(arrays)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _build_presence_records():
|
|
223
|
+
"""Build presence records from presence log."""
|
|
224
|
+
records = []
|
|
225
|
+
|
|
226
|
+
# Read last 24 hours of presence data
|
|
227
|
+
history = read_presence_history(hours=24.0)
|
|
228
|
+
|
|
229
|
+
for ts, state in history:
|
|
230
|
+
records.append({
|
|
231
|
+
"timestamp": ts.isoformat() if isinstance(ts, datetime) else str(ts),
|
|
232
|
+
"state": state,
|
|
233
|
+
"state_name": {1: "locked", 2: "inactive", 3: "active"}.get(state, "unknown"),
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
return records
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _build_presence_table(records):
|
|
240
|
+
"""Build a PyArrow table from presence records."""
|
|
241
|
+
import pyarrow as pa
|
|
242
|
+
|
|
243
|
+
if not records:
|
|
244
|
+
schema = pa.schema([
|
|
245
|
+
("timestamp", pa.string()),
|
|
246
|
+
("state", pa.int64()),
|
|
247
|
+
("state_name", pa.string()),
|
|
248
|
+
])
|
|
249
|
+
empty_arrays = {field.name: pa.array([], type=field.type) for field in schema}
|
|
250
|
+
return pa.table(empty_arrays, schema=schema)
|
|
251
|
+
|
|
252
|
+
arrays = {}
|
|
253
|
+
for key in records[0].keys():
|
|
254
|
+
values = [r.get(key) for r in records]
|
|
255
|
+
arrays[key] = values
|
|
256
|
+
|
|
257
|
+
return pa.table(arrays)
|