emdash-core 0.1.7__py3-none-any.whl → 0.1.25__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.
- emdash_core/__init__.py +6 -1
- emdash_core/agent/events.py +29 -0
- emdash_core/agent/prompts/__init__.py +5 -0
- emdash_core/agent/prompts/main_agent.py +22 -2
- emdash_core/agent/prompts/plan_mode.py +126 -0
- emdash_core/agent/prompts/subagents.py +11 -7
- emdash_core/agent/prompts/workflow.py +138 -43
- emdash_core/agent/providers/base.py +4 -0
- emdash_core/agent/providers/models.py +7 -0
- emdash_core/agent/providers/openai_provider.py +74 -2
- emdash_core/agent/runner.py +556 -34
- emdash_core/agent/skills.py +319 -0
- emdash_core/agent/toolkit.py +48 -0
- emdash_core/agent/tools/__init__.py +3 -2
- emdash_core/agent/tools/modes.py +197 -53
- emdash_core/agent/tools/search.py +4 -0
- emdash_core/agent/tools/skill.py +193 -0
- emdash_core/agent/tools/spec.py +61 -94
- emdash_core/agent/tools/tasks.py +15 -78
- emdash_core/api/agent.py +7 -7
- emdash_core/api/index.py +1 -1
- emdash_core/api/projectmd.py +4 -2
- emdash_core/api/router.py +2 -0
- emdash_core/api/skills.py +241 -0
- emdash_core/checkpoint/__init__.py +40 -0
- emdash_core/checkpoint/cli.py +175 -0
- emdash_core/checkpoint/git_operations.py +250 -0
- emdash_core/checkpoint/manager.py +231 -0
- emdash_core/checkpoint/models.py +107 -0
- emdash_core/checkpoint/storage.py +201 -0
- emdash_core/config.py +1 -1
- emdash_core/core/config.py +18 -2
- emdash_core/graph/schema.py +5 -5
- emdash_core/ingestion/orchestrator.py +19 -10
- emdash_core/models/agent.py +1 -1
- emdash_core/server.py +42 -0
- emdash_core/sse/stream.py +1 -0
- {emdash_core-0.1.7.dist-info → emdash_core-0.1.25.dist-info}/METADATA +1 -2
- {emdash_core-0.1.7.dist-info → emdash_core-0.1.25.dist-info}/RECORD +41 -31
- {emdash_core-0.1.7.dist-info → emdash_core-0.1.25.dist-info}/entry_points.txt +1 -0
- {emdash_core-0.1.7.dist-info → emdash_core-0.1.25.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Checkpoint manager for orchestrating git-based checkpoints."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from uuid import uuid4
|
|
8
|
+
|
|
9
|
+
from ..utils.logger import log
|
|
10
|
+
from .models import CheckpointMetadata, ConversationState
|
|
11
|
+
from .git_operations import GitCheckpointOperations
|
|
12
|
+
from .storage import CheckpointStorage
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CheckpointManager:
|
|
16
|
+
"""Manages git-based checkpoints for agent sessions.
|
|
17
|
+
|
|
18
|
+
Creates checkpoint commits after each agentic loop completes,
|
|
19
|
+
storing file changes in git and conversation state in files.
|
|
20
|
+
|
|
21
|
+
Example:
|
|
22
|
+
manager = CheckpointManager(repo_root=Path("."))
|
|
23
|
+
|
|
24
|
+
# After agent completes a run
|
|
25
|
+
checkpoint = manager.create_checkpoint(
|
|
26
|
+
messages=runner._messages,
|
|
27
|
+
model=runner.model,
|
|
28
|
+
system_prompt=runner.system_prompt,
|
|
29
|
+
tools_used=["read_file", "write_to_file"],
|
|
30
|
+
token_usage={"input": 1000, "output": 500},
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# List checkpoints
|
|
34
|
+
for cp in manager.list_checkpoints():
|
|
35
|
+
print(f"{cp.id}: {cp.summary}")
|
|
36
|
+
|
|
37
|
+
# Restore to checkpoint
|
|
38
|
+
conv = manager.restore_checkpoint("cp_abc123_001")
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
repo_root: Path,
|
|
44
|
+
session_id: Optional[str] = None,
|
|
45
|
+
enabled: bool = True,
|
|
46
|
+
):
|
|
47
|
+
"""Initialize checkpoint manager.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
repo_root: Root of the git repository
|
|
51
|
+
session_id: Optional session ID (auto-generated if None)
|
|
52
|
+
enabled: Whether checkpointing is enabled
|
|
53
|
+
"""
|
|
54
|
+
self.repo_root = repo_root.resolve()
|
|
55
|
+
self.session_id = session_id or str(uuid4())[:8]
|
|
56
|
+
self.enabled = enabled
|
|
57
|
+
self.iteration = 0
|
|
58
|
+
|
|
59
|
+
self.git_ops = GitCheckpointOperations(repo_root)
|
|
60
|
+
self.storage = CheckpointStorage(repo_root)
|
|
61
|
+
|
|
62
|
+
def create_checkpoint(
|
|
63
|
+
self,
|
|
64
|
+
messages: list[dict],
|
|
65
|
+
model: str,
|
|
66
|
+
system_prompt: str,
|
|
67
|
+
tools_used: list[str],
|
|
68
|
+
token_usage: dict[str, int],
|
|
69
|
+
summary: Optional[str] = None,
|
|
70
|
+
) -> Optional[CheckpointMetadata]:
|
|
71
|
+
"""Create a checkpoint after successful agent run.
|
|
72
|
+
|
|
73
|
+
Creates a git commit with all file changes and saves
|
|
74
|
+
conversation state for later restoration.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
messages: Full conversation message history
|
|
78
|
+
model: Model used for the run
|
|
79
|
+
system_prompt: System prompt used
|
|
80
|
+
tools_used: List of tool names used during the run
|
|
81
|
+
token_usage: Token usage stats (input, output, thinking)
|
|
82
|
+
summary: Optional human-readable summary
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
CheckpointMetadata if checkpoint was created, None if no changes
|
|
86
|
+
"""
|
|
87
|
+
if not self.enabled:
|
|
88
|
+
log.debug("Checkpointing disabled, skipping")
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
# Check for changes
|
|
92
|
+
if not self.git_ops.has_changes():
|
|
93
|
+
log.debug("No changes to checkpoint")
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
self.iteration += 1
|
|
97
|
+
checkpoint_id = f"cp_{self.session_id}_{self.iteration:03d}"
|
|
98
|
+
timestamp = datetime.now().isoformat()
|
|
99
|
+
|
|
100
|
+
# Generate summary if not provided
|
|
101
|
+
if not summary:
|
|
102
|
+
summary = self._generate_summary(messages)
|
|
103
|
+
|
|
104
|
+
# Get list of modified files
|
|
105
|
+
files_modified = self.git_ops.get_modified_files()
|
|
106
|
+
|
|
107
|
+
# Build metadata
|
|
108
|
+
metadata = CheckpointMetadata(
|
|
109
|
+
id=checkpoint_id,
|
|
110
|
+
session_id=self.session_id,
|
|
111
|
+
iteration=self.iteration,
|
|
112
|
+
timestamp=timestamp,
|
|
113
|
+
summary=summary,
|
|
114
|
+
tools_used=tools_used,
|
|
115
|
+
files_modified=files_modified,
|
|
116
|
+
token_usage=token_usage,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Save conversation state first (so it's included in the commit)
|
|
120
|
+
conv_state = ConversationState(
|
|
121
|
+
messages=messages,
|
|
122
|
+
model=model,
|
|
123
|
+
system_prompt_hash=hashlib.sha256(
|
|
124
|
+
system_prompt.encode()
|
|
125
|
+
).hexdigest()[:16],
|
|
126
|
+
token_usage=token_usage,
|
|
127
|
+
)
|
|
128
|
+
self.storage.save_conversation(checkpoint_id, self.session_id, conv_state)
|
|
129
|
+
|
|
130
|
+
# Create git commit (includes the conversation file)
|
|
131
|
+
commit_sha = self.git_ops.create_checkpoint_commit(metadata, summary)
|
|
132
|
+
metadata.commit_sha = commit_sha
|
|
133
|
+
|
|
134
|
+
# Update index
|
|
135
|
+
self.storage.update_index(metadata)
|
|
136
|
+
|
|
137
|
+
log.info(f"Created checkpoint {checkpoint_id} at {commit_sha[:8]}")
|
|
138
|
+
return metadata
|
|
139
|
+
|
|
140
|
+
def _generate_summary(self, messages: list[dict]) -> str:
|
|
141
|
+
"""Generate a brief summary from messages.
|
|
142
|
+
|
|
143
|
+
Extracts the last user query and summarizes the response.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
messages: Conversation messages
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Brief summary string
|
|
150
|
+
"""
|
|
151
|
+
# Find last user message
|
|
152
|
+
user_query = ""
|
|
153
|
+
for msg in reversed(messages):
|
|
154
|
+
if msg.get("role") == "user":
|
|
155
|
+
content = msg.get("content", "")
|
|
156
|
+
if isinstance(content, str) and not content.startswith("[SYSTEM"):
|
|
157
|
+
user_query = content[:100]
|
|
158
|
+
if len(content) > 100:
|
|
159
|
+
user_query += "..."
|
|
160
|
+
break
|
|
161
|
+
|
|
162
|
+
if user_query:
|
|
163
|
+
return f"Response to: {user_query}"
|
|
164
|
+
return "Agent checkpoint"
|
|
165
|
+
|
|
166
|
+
def restore_checkpoint(
|
|
167
|
+
self,
|
|
168
|
+
checkpoint_id: str,
|
|
169
|
+
restore_conversation: bool = True,
|
|
170
|
+
create_branch: bool = True,
|
|
171
|
+
) -> Optional[ConversationState]:
|
|
172
|
+
"""Restore to a checkpoint.
|
|
173
|
+
|
|
174
|
+
Checks out the git commit and optionally loads conversation state.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
checkpoint_id: ID of checkpoint to restore
|
|
178
|
+
restore_conversation: Whether to load conversation state
|
|
179
|
+
create_branch: Whether to create a branch at the restored state
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
ConversationState if restore_conversation=True, None otherwise
|
|
183
|
+
|
|
184
|
+
Raises:
|
|
185
|
+
ValueError: If checkpoint not found
|
|
186
|
+
"""
|
|
187
|
+
# Find checkpoint in index
|
|
188
|
+
metadata = self.storage.find_checkpoint(checkpoint_id)
|
|
189
|
+
if not metadata:
|
|
190
|
+
raise ValueError(f"Checkpoint not found: {checkpoint_id}")
|
|
191
|
+
|
|
192
|
+
if not metadata.commit_sha:
|
|
193
|
+
raise ValueError(f"Checkpoint {checkpoint_id} has no commit SHA")
|
|
194
|
+
|
|
195
|
+
# Git checkout
|
|
196
|
+
self.git_ops.restore_to_commit(metadata.commit_sha, create_branch)
|
|
197
|
+
|
|
198
|
+
# Load conversation if requested
|
|
199
|
+
if restore_conversation:
|
|
200
|
+
return self.storage.load_conversation(
|
|
201
|
+
checkpoint_id,
|
|
202
|
+
metadata.session_id,
|
|
203
|
+
)
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
def list_checkpoints(
|
|
207
|
+
self,
|
|
208
|
+
session_id: Optional[str] = None,
|
|
209
|
+
limit: int = 50,
|
|
210
|
+
) -> list[CheckpointMetadata]:
|
|
211
|
+
"""List checkpoints.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
session_id: Filter by session ID (optional)
|
|
215
|
+
limit: Maximum number of checkpoints
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
List of CheckpointMetadata, most recent first
|
|
219
|
+
"""
|
|
220
|
+
return self.storage.get_checkpoints(session_id, limit)
|
|
221
|
+
|
|
222
|
+
def get_checkpoint(self, checkpoint_id: str) -> Optional[CheckpointMetadata]:
|
|
223
|
+
"""Get a specific checkpoint by ID.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
checkpoint_id: Checkpoint ID
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
CheckpointMetadata if found, None otherwise
|
|
230
|
+
"""
|
|
231
|
+
return self.storage.find_checkpoint(checkpoint_id)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Data models for checkpoint system."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field, asdict
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class CheckpointMetadata:
|
|
9
|
+
"""Metadata stored in commit message and index.
|
|
10
|
+
|
|
11
|
+
This is the lightweight metadata that can be parsed from
|
|
12
|
+
commit messages and stored in the index for fast lookup.
|
|
13
|
+
"""
|
|
14
|
+
id: str
|
|
15
|
+
session_id: str
|
|
16
|
+
iteration: int
|
|
17
|
+
timestamp: str
|
|
18
|
+
commit_sha: Optional[str] = None
|
|
19
|
+
summary: str = ""
|
|
20
|
+
tools_used: list[str] = field(default_factory=list)
|
|
21
|
+
files_modified: list[str] = field(default_factory=list)
|
|
22
|
+
token_usage: dict[str, int] = field(default_factory=dict)
|
|
23
|
+
|
|
24
|
+
def to_dict(self) -> dict[str, Any]:
|
|
25
|
+
"""Convert to dictionary for JSON serialization."""
|
|
26
|
+
return asdict(self)
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def from_dict(cls, data: dict[str, Any]) -> "CheckpointMetadata":
|
|
30
|
+
"""Create from dictionary."""
|
|
31
|
+
return cls(**data)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class ConversationState:
|
|
36
|
+
"""Full conversation state for restoration.
|
|
37
|
+
|
|
38
|
+
This is the complete state needed to restore an agent
|
|
39
|
+
session, including all messages and context.
|
|
40
|
+
"""
|
|
41
|
+
messages: list[dict]
|
|
42
|
+
model: str
|
|
43
|
+
system_prompt_hash: str
|
|
44
|
+
token_usage: dict[str, int] = field(default_factory=dict)
|
|
45
|
+
|
|
46
|
+
def to_dict(self) -> dict[str, Any]:
|
|
47
|
+
"""Convert to dictionary for JSON serialization."""
|
|
48
|
+
return {
|
|
49
|
+
"version": "1.0",
|
|
50
|
+
"messages": self.messages,
|
|
51
|
+
"model": self.model,
|
|
52
|
+
"system_prompt_hash": self.system_prompt_hash,
|
|
53
|
+
"token_usage": self.token_usage,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def from_dict(cls, data: dict[str, Any]) -> "ConversationState":
|
|
58
|
+
"""Create from dictionary."""
|
|
59
|
+
return cls(
|
|
60
|
+
messages=data.get("messages", []),
|
|
61
|
+
model=data.get("model", ""),
|
|
62
|
+
system_prompt_hash=data.get("system_prompt_hash", ""),
|
|
63
|
+
token_usage=data.get("token_usage", {}),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class CheckpointIndex:
|
|
69
|
+
"""Index of all checkpoints for fast lookup.
|
|
70
|
+
|
|
71
|
+
Stored at .emdash/checkpoints/index.json
|
|
72
|
+
"""
|
|
73
|
+
version: str = "1.0"
|
|
74
|
+
checkpoints: list[CheckpointMetadata] = field(default_factory=list)
|
|
75
|
+
|
|
76
|
+
def to_dict(self) -> dict[str, Any]:
|
|
77
|
+
"""Convert to dictionary for JSON serialization."""
|
|
78
|
+
return {
|
|
79
|
+
"version": self.version,
|
|
80
|
+
"checkpoints": [cp.to_dict() for cp in self.checkpoints],
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def from_dict(cls, data: dict[str, Any]) -> "CheckpointIndex":
|
|
85
|
+
"""Create from dictionary."""
|
|
86
|
+
return cls(
|
|
87
|
+
version=data.get("version", "1.0"),
|
|
88
|
+
checkpoints=[
|
|
89
|
+
CheckpointMetadata.from_dict(cp)
|
|
90
|
+
for cp in data.get("checkpoints", [])
|
|
91
|
+
],
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def add(self, metadata: CheckpointMetadata) -> None:
|
|
95
|
+
"""Add a checkpoint to the index."""
|
|
96
|
+
self.checkpoints.insert(0, metadata) # Most recent first
|
|
97
|
+
|
|
98
|
+
def find(self, checkpoint_id: str) -> Optional[CheckpointMetadata]:
|
|
99
|
+
"""Find a checkpoint by ID."""
|
|
100
|
+
for cp in self.checkpoints:
|
|
101
|
+
if cp.id == checkpoint_id:
|
|
102
|
+
return cp
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
def find_by_session(self, session_id: str) -> list[CheckpointMetadata]:
|
|
106
|
+
"""Find all checkpoints for a session."""
|
|
107
|
+
return [cp for cp in self.checkpoints if cp.session_id == session_id]
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""File storage for checkpoint data."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from ..utils.logger import log
|
|
8
|
+
from .models import CheckpointIndex, CheckpointMetadata, ConversationState
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CheckpointStorage:
|
|
12
|
+
"""Handles checkpoint file storage.
|
|
13
|
+
|
|
14
|
+
Stores conversation state and checkpoint index in:
|
|
15
|
+
.emdash/checkpoints/
|
|
16
|
+
index.json # Checkpoint index
|
|
17
|
+
{session_id}/{checkpoint_id}/
|
|
18
|
+
conversation.json # Full conversation state
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
CHECKPOINT_DIR = ".emdash/checkpoints"
|
|
22
|
+
INDEX_FILE = "index.json"
|
|
23
|
+
|
|
24
|
+
def __init__(self, repo_root: Path):
|
|
25
|
+
"""Initialize checkpoint storage.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
repo_root: Root of the repository
|
|
29
|
+
"""
|
|
30
|
+
self.repo_root = repo_root.resolve()
|
|
31
|
+
self.checkpoint_dir = self.repo_root / self.CHECKPOINT_DIR
|
|
32
|
+
|
|
33
|
+
def _ensure_dir(self, path: Path) -> None:
|
|
34
|
+
"""Ensure directory exists."""
|
|
35
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
|
|
37
|
+
def _get_checkpoint_path(self, session_id: str, checkpoint_id: str) -> Path:
|
|
38
|
+
"""Get path for a checkpoint's data directory."""
|
|
39
|
+
return self.checkpoint_dir / session_id / checkpoint_id
|
|
40
|
+
|
|
41
|
+
def save_conversation(
|
|
42
|
+
self,
|
|
43
|
+
checkpoint_id: str,
|
|
44
|
+
session_id: str,
|
|
45
|
+
state: ConversationState,
|
|
46
|
+
) -> Path:
|
|
47
|
+
"""Save conversation state to checkpoint directory.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
checkpoint_id: Unique checkpoint ID
|
|
51
|
+
session_id: Session ID
|
|
52
|
+
state: Conversation state to save
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Path to saved file
|
|
56
|
+
"""
|
|
57
|
+
checkpoint_path = self._get_checkpoint_path(session_id, checkpoint_id)
|
|
58
|
+
self._ensure_dir(checkpoint_path)
|
|
59
|
+
|
|
60
|
+
file_path = checkpoint_path / "conversation.json"
|
|
61
|
+
with open(file_path, "w") as f:
|
|
62
|
+
json.dump(state.to_dict(), f, indent=2)
|
|
63
|
+
|
|
64
|
+
log.debug(f"Saved conversation to {file_path}")
|
|
65
|
+
return file_path
|
|
66
|
+
|
|
67
|
+
def load_conversation(
|
|
68
|
+
self,
|
|
69
|
+
checkpoint_id: str,
|
|
70
|
+
session_id: str,
|
|
71
|
+
) -> Optional[ConversationState]:
|
|
72
|
+
"""Load conversation state from checkpoint.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
checkpoint_id: Unique checkpoint ID
|
|
76
|
+
session_id: Session ID
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
ConversationState if found, None otherwise
|
|
80
|
+
"""
|
|
81
|
+
checkpoint_path = self._get_checkpoint_path(session_id, checkpoint_id)
|
|
82
|
+
file_path = checkpoint_path / "conversation.json"
|
|
83
|
+
|
|
84
|
+
if not file_path.exists():
|
|
85
|
+
log.warning(f"Conversation file not found: {file_path}")
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
with open(file_path) as f:
|
|
89
|
+
data = json.load(f)
|
|
90
|
+
return ConversationState.from_dict(data)
|
|
91
|
+
|
|
92
|
+
def get_index_path(self) -> Path:
|
|
93
|
+
"""Get path to index file."""
|
|
94
|
+
return self.checkpoint_dir / self.INDEX_FILE
|
|
95
|
+
|
|
96
|
+
def load_index(self) -> CheckpointIndex:
|
|
97
|
+
"""Load checkpoint index.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
CheckpointIndex, empty if file doesn't exist
|
|
101
|
+
"""
|
|
102
|
+
index_path = self.get_index_path()
|
|
103
|
+
if not index_path.exists():
|
|
104
|
+
return CheckpointIndex()
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
with open(index_path) as f:
|
|
108
|
+
data = json.load(f)
|
|
109
|
+
return CheckpointIndex.from_dict(data)
|
|
110
|
+
except (json.JSONDecodeError, KeyError) as e:
|
|
111
|
+
log.warning(f"Failed to load checkpoint index: {e}")
|
|
112
|
+
return CheckpointIndex()
|
|
113
|
+
|
|
114
|
+
def save_index(self, index: CheckpointIndex) -> None:
|
|
115
|
+
"""Save checkpoint index.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
index: CheckpointIndex to save
|
|
119
|
+
"""
|
|
120
|
+
self._ensure_dir(self.checkpoint_dir)
|
|
121
|
+
index_path = self.get_index_path()
|
|
122
|
+
|
|
123
|
+
with open(index_path, "w") as f:
|
|
124
|
+
json.dump(index.to_dict(), f, indent=2)
|
|
125
|
+
|
|
126
|
+
log.debug(f"Saved checkpoint index to {index_path}")
|
|
127
|
+
|
|
128
|
+
def update_index(self, metadata: CheckpointMetadata) -> None:
|
|
129
|
+
"""Add a checkpoint to the index.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
metadata: Checkpoint metadata to add
|
|
133
|
+
"""
|
|
134
|
+
index = self.load_index()
|
|
135
|
+
index.add(metadata)
|
|
136
|
+
self.save_index(index)
|
|
137
|
+
|
|
138
|
+
def get_checkpoints(
|
|
139
|
+
self,
|
|
140
|
+
session_id: Optional[str] = None,
|
|
141
|
+
limit: int = 50,
|
|
142
|
+
) -> list[CheckpointMetadata]:
|
|
143
|
+
"""Get checkpoints from index.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
session_id: Filter by session ID (optional)
|
|
147
|
+
limit: Maximum number of checkpoints to return
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
List of CheckpointMetadata, most recent first
|
|
151
|
+
"""
|
|
152
|
+
index = self.load_index()
|
|
153
|
+
|
|
154
|
+
if session_id:
|
|
155
|
+
checkpoints = index.find_by_session(session_id)
|
|
156
|
+
else:
|
|
157
|
+
checkpoints = index.checkpoints
|
|
158
|
+
|
|
159
|
+
return checkpoints[:limit]
|
|
160
|
+
|
|
161
|
+
def find_checkpoint(self, checkpoint_id: str) -> Optional[CheckpointMetadata]:
|
|
162
|
+
"""Find a checkpoint by ID.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
checkpoint_id: Checkpoint ID to find
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
CheckpointMetadata if found, None otherwise
|
|
169
|
+
"""
|
|
170
|
+
index = self.load_index()
|
|
171
|
+
return index.find(checkpoint_id)
|
|
172
|
+
|
|
173
|
+
def delete_checkpoint(self, checkpoint_id: str, session_id: str) -> bool:
|
|
174
|
+
"""Delete a checkpoint's data.
|
|
175
|
+
|
|
176
|
+
Note: This only deletes the conversation data, not the git commit.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
checkpoint_id: Checkpoint ID to delete
|
|
180
|
+
session_id: Session ID
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
True if deleted, False if not found
|
|
184
|
+
"""
|
|
185
|
+
checkpoint_path = self._get_checkpoint_path(session_id, checkpoint_id)
|
|
186
|
+
|
|
187
|
+
if not checkpoint_path.exists():
|
|
188
|
+
return False
|
|
189
|
+
|
|
190
|
+
import shutil
|
|
191
|
+
shutil.rmtree(checkpoint_path)
|
|
192
|
+
|
|
193
|
+
# Update index
|
|
194
|
+
index = self.load_index()
|
|
195
|
+
index.checkpoints = [
|
|
196
|
+
cp for cp in index.checkpoints if cp.id != checkpoint_id
|
|
197
|
+
]
|
|
198
|
+
self.save_index(index)
|
|
199
|
+
|
|
200
|
+
log.info(f"Deleted checkpoint {checkpoint_id}")
|
|
201
|
+
return True
|
emdash_core/config.py
CHANGED
|
@@ -36,7 +36,7 @@ class ServerConfig(BaseSettings):
|
|
|
36
36
|
|
|
37
37
|
# Agent settings
|
|
38
38
|
default_model: str = Field(default_factory=_get_default_model, description="Default LLM model")
|
|
39
|
-
max_iterations: int = Field(default=
|
|
39
|
+
max_iterations: int = Field(default=100, description="Max agent iterations")
|
|
40
40
|
context_threshold: float = Field(default=0.6, description="Context window threshold for summarization")
|
|
41
41
|
|
|
42
42
|
# SSE settings
|
emdash_core/core/config.py
CHANGED
|
@@ -268,7 +268,7 @@ class AgentConfig(BaseModel):
|
|
|
268
268
|
)
|
|
269
269
|
|
|
270
270
|
max_iterations: int = Field(
|
|
271
|
-
default=
|
|
271
|
+
default=100,
|
|
272
272
|
ge=10,
|
|
273
273
|
le=200,
|
|
274
274
|
description="Maximum tool call iterations before stopping",
|
|
@@ -281,13 +281,29 @@ class AgentConfig(BaseModel):
|
|
|
281
281
|
description="Maximum tokens for tool output (estimated at ~4 chars/token)",
|
|
282
282
|
)
|
|
283
283
|
|
|
284
|
+
context_compact_threshold: float = Field(
|
|
285
|
+
default=0.8,
|
|
286
|
+
ge=0.5,
|
|
287
|
+
le=0.95,
|
|
288
|
+
description="Trigger context compaction at this % of model's context limit",
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
context_compact_target: float = Field(
|
|
292
|
+
default=0.5,
|
|
293
|
+
ge=0.3,
|
|
294
|
+
le=0.7,
|
|
295
|
+
description="Target context size after compaction (% of model's limit)",
|
|
296
|
+
)
|
|
297
|
+
|
|
284
298
|
@classmethod
|
|
285
299
|
def from_env(cls) -> "AgentConfig":
|
|
286
300
|
"""Load configuration from environment variables."""
|
|
287
301
|
return cls(
|
|
288
302
|
max_context_messages=int(os.getenv("EMDASH_MAX_CONTEXT_MESSAGES", "25")),
|
|
289
|
-
max_iterations=int(os.getenv("EMDASH_MAX_ITERATIONS", "
|
|
303
|
+
max_iterations=int(os.getenv("EMDASH_MAX_ITERATIONS", "100")),
|
|
290
304
|
tool_max_output_tokens=int(os.getenv("EMDASH_TOOL_MAX_OUTPUT", "25000")),
|
|
305
|
+
context_compact_threshold=float(os.getenv("EMDASH_CONTEXT_COMPACT_THRESHOLD", "0.8")),
|
|
306
|
+
context_compact_target=float(os.getenv("EMDASH_CONTEXT_COMPACT_TARGET", "0.5")),
|
|
291
307
|
)
|
|
292
308
|
|
|
293
309
|
|
emdash_core/graph/schema.py
CHANGED
|
@@ -101,8 +101,8 @@ class SchemaManager:
|
|
|
101
101
|
PRIMARY KEY (url)
|
|
102
102
|
)
|
|
103
103
|
""",
|
|
104
|
-
"
|
|
105
|
-
CREATE NODE TABLE
|
|
104
|
+
"Commit": """
|
|
105
|
+
CREATE NODE TABLE Commit (
|
|
106
106
|
sha STRING,
|
|
107
107
|
message STRING,
|
|
108
108
|
timestamp TIMESTAMP,
|
|
@@ -202,12 +202,12 @@ class SchemaManager:
|
|
|
202
202
|
""",
|
|
203
203
|
"AUTHORED_BY": """
|
|
204
204
|
CREATE REL TABLE AUTHORED_BY (
|
|
205
|
-
FROM
|
|
205
|
+
FROM Commit TO Author
|
|
206
206
|
)
|
|
207
207
|
""",
|
|
208
208
|
"COMMIT_MODIFIES": """
|
|
209
209
|
CREATE REL TABLE COMMIT_MODIFIES (
|
|
210
|
-
FROM
|
|
210
|
+
FROM Commit TO File,
|
|
211
211
|
change_type STRING,
|
|
212
212
|
insertions INT64,
|
|
213
213
|
deletions INT64,
|
|
@@ -216,7 +216,7 @@ class SchemaManager:
|
|
|
216
216
|
""",
|
|
217
217
|
"PR_CONTAINS": """
|
|
218
218
|
CREATE REL TABLE PR_CONTAINS (
|
|
219
|
-
FROM PullRequest TO
|
|
219
|
+
FROM PullRequest TO Commit
|
|
220
220
|
)
|
|
221
221
|
""",
|
|
222
222
|
"PR_MODIFIES": """
|