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.
Files changed (41) hide show
  1. emdash_core/__init__.py +6 -1
  2. emdash_core/agent/events.py +29 -0
  3. emdash_core/agent/prompts/__init__.py +5 -0
  4. emdash_core/agent/prompts/main_agent.py +22 -2
  5. emdash_core/agent/prompts/plan_mode.py +126 -0
  6. emdash_core/agent/prompts/subagents.py +11 -7
  7. emdash_core/agent/prompts/workflow.py +138 -43
  8. emdash_core/agent/providers/base.py +4 -0
  9. emdash_core/agent/providers/models.py +7 -0
  10. emdash_core/agent/providers/openai_provider.py +74 -2
  11. emdash_core/agent/runner.py +556 -34
  12. emdash_core/agent/skills.py +319 -0
  13. emdash_core/agent/toolkit.py +48 -0
  14. emdash_core/agent/tools/__init__.py +3 -2
  15. emdash_core/agent/tools/modes.py +197 -53
  16. emdash_core/agent/tools/search.py +4 -0
  17. emdash_core/agent/tools/skill.py +193 -0
  18. emdash_core/agent/tools/spec.py +61 -94
  19. emdash_core/agent/tools/tasks.py +15 -78
  20. emdash_core/api/agent.py +7 -7
  21. emdash_core/api/index.py +1 -1
  22. emdash_core/api/projectmd.py +4 -2
  23. emdash_core/api/router.py +2 -0
  24. emdash_core/api/skills.py +241 -0
  25. emdash_core/checkpoint/__init__.py +40 -0
  26. emdash_core/checkpoint/cli.py +175 -0
  27. emdash_core/checkpoint/git_operations.py +250 -0
  28. emdash_core/checkpoint/manager.py +231 -0
  29. emdash_core/checkpoint/models.py +107 -0
  30. emdash_core/checkpoint/storage.py +201 -0
  31. emdash_core/config.py +1 -1
  32. emdash_core/core/config.py +18 -2
  33. emdash_core/graph/schema.py +5 -5
  34. emdash_core/ingestion/orchestrator.py +19 -10
  35. emdash_core/models/agent.py +1 -1
  36. emdash_core/server.py +42 -0
  37. emdash_core/sse/stream.py +1 -0
  38. {emdash_core-0.1.7.dist-info → emdash_core-0.1.25.dist-info}/METADATA +1 -2
  39. {emdash_core-0.1.7.dist-info → emdash_core-0.1.25.dist-info}/RECORD +41 -31
  40. {emdash_core-0.1.7.dist-info → emdash_core-0.1.25.dist-info}/entry_points.txt +1 -0
  41. {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=50, description="Max agent iterations")
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
@@ -268,7 +268,7 @@ class AgentConfig(BaseModel):
268
268
  )
269
269
 
270
270
  max_iterations: int = Field(
271
- default=50,
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", "50")),
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
 
@@ -101,8 +101,8 @@ class SchemaManager:
101
101
  PRIMARY KEY (url)
102
102
  )
103
103
  """,
104
- "GitCommit": """
105
- CREATE NODE TABLE GitCommit (
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 GitCommit TO Author
205
+ FROM Commit TO Author
206
206
  )
207
207
  """,
208
208
  "COMMIT_MODIFIES": """
209
209
  CREATE REL TABLE COMMIT_MODIFIES (
210
- FROM GitCommit TO File,
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 GitCommit
219
+ FROM PullRequest TO Commit
220
220
  )
221
221
  """,
222
222
  "PR_MODIFIES": """