ithrl 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.
Files changed (108) hide show
  1. ithrl/__init__.py +1 -0
  2. ithrl/__main__.py +6 -0
  3. ithrl/adapters/__init__.py +49 -0
  4. ithrl/adapters/_jsonl.py +266 -0
  5. ithrl/adapters/codex/__init__.py +5 -0
  6. ithrl/adapters/codex/context.py +204 -0
  7. ithrl/adapters/codex/decisions.py +224 -0
  8. ithrl/adapters/codex/discovery.py +60 -0
  9. ithrl/adapters/codex/parser.py +191 -0
  10. ithrl/adapters/contracts.py +106 -0
  11. ithrl/adapters/copilot/__init__.py +5 -0
  12. ithrl/adapters/copilot/context.py +182 -0
  13. ithrl/adapters/copilot/decisions.py +294 -0
  14. ithrl/adapters/copilot/discovery.py +28 -0
  15. ithrl/adapters/copilot/parser.py +283 -0
  16. ithrl/adapters/discovery.py +220 -0
  17. ithrl/adapters/errors.py +52 -0
  18. ithrl/adapters/registry.py +433 -0
  19. ithrl/behavior.py +138 -0
  20. ithrl/calibration.py +434 -0
  21. ithrl/case/__init__.py +33 -0
  22. ithrl/case/contracts.py +88 -0
  23. ithrl/case/evidence.py +169 -0
  24. ithrl/case/filesystem.py +340 -0
  25. ithrl/case/source.py +208 -0
  26. ithrl/case/source_context.py +119 -0
  27. ithrl/case/source_storage.py +234 -0
  28. ithrl/case/storage.py +377 -0
  29. ithrl/cli/__init__.py +968 -0
  30. ithrl/cli/extract.py +478 -0
  31. ithrl/cli/native.py +484 -0
  32. ithrl/cli/output.py +786 -0
  33. ithrl/cli/paths.py +24 -0
  34. ithrl/cli/sampled.py +485 -0
  35. ithrl/cli/sampled_output.py +472 -0
  36. ithrl/cli/sampled_reduction.py +1036 -0
  37. ithrl/cli/sampled_session.py +342 -0
  38. ithrl/cli/shrink.py +523 -0
  39. ithrl/context.py +465 -0
  40. ithrl/decisions.py +271 -0
  41. ithrl/examples/synthetic-trace.json +46 -0
  42. ithrl/execution/__init__.py +68 -0
  43. ithrl/execution/budget.py +146 -0
  44. ithrl/execution/contracts.py +426 -0
  45. ithrl/execution/preflight.py +346 -0
  46. ithrl/experiment.py +453 -0
  47. ithrl/extraction.py +206 -0
  48. ithrl/measurement.py +315 -0
  49. ithrl/observations.py +1148 -0
  50. ithrl/path_safety.py +100 -0
  51. ithrl/reduce/__init__.py +53 -0
  52. ithrl/reduce/contracts.py +508 -0
  53. ithrl/reduce/engine.py +901 -0
  54. ithrl/reduce/search.py +178 -0
  55. ithrl/results/__init__.py +49 -0
  56. ithrl/results/models.py +294 -0
  57. ithrl/results/storage.py +485 -0
  58. ithrl/results/summary.py +420 -0
  59. ithrl/results/validation.py +611 -0
  60. ithrl/run_state.py +1135 -0
  61. ithrl/runner/__init__.py +44 -0
  62. ithrl/runner/codex/__init__.py +17 -0
  63. ithrl/runner/codex/capability.py +708 -0
  64. ithrl/runner/codex/configuration.py +529 -0
  65. ithrl/runner/codex/isolation.py +396 -0
  66. ithrl/runner/codex/materialize.py +358 -0
  67. ithrl/runner/codex/policy.py +67 -0
  68. ithrl/runner/codex/protocol.py +649 -0
  69. ithrl/runner/codex/protocol_contracts.py +171 -0
  70. ithrl/runner/codex/runner.py +533 -0
  71. ithrl/runner/codex/runtime_configuration.py +264 -0
  72. ithrl/runner/codex/schema.py +527 -0
  73. ithrl/runner/codex/transcript.py +130 -0
  74. ithrl/runner/codex/worker.py +175 -0
  75. ithrl/runner/contracts.py +419 -0
  76. ithrl/runner/copilot/__init__.py +18 -0
  77. ithrl/runner/copilot/capability.py +825 -0
  78. ithrl/runner/copilot/configuration.py +201 -0
  79. ithrl/runner/copilot/isolation.py +374 -0
  80. ithrl/runner/copilot/materialize.py +484 -0
  81. ithrl/runner/copilot/policy.py +110 -0
  82. ithrl/runner/copilot/protocol/__init__.py +18 -0
  83. ithrl/runner/copilot/protocol/contracts.py +74 -0
  84. ithrl/runner/copilot/protocol/decoding.py +93 -0
  85. ithrl/runner/copilot/protocol/parser.py +418 -0
  86. ithrl/runner/copilot/protocol/state.py +130 -0
  87. ithrl/runner/copilot/runner.py +425 -0
  88. ithrl/runner/copilot/worker.py +351 -0
  89. ithrl/runner/evidence.py +345 -0
  90. ithrl/runner/fake.py +107 -0
  91. ithrl/runner/registry.py +90 -0
  92. ithrl/runner/runtime/__init__.py +50 -0
  93. ithrl/runner/runtime/contracts.py +157 -0
  94. ithrl/runner/runtime/output.py +184 -0
  95. ithrl/runner/runtime/process.py +954 -0
  96. ithrl/runner/runtime/protection.py +562 -0
  97. ithrl/runner/runtime/state.py +282 -0
  98. ithrl/runner/validation.py +376 -0
  99. ithrl/sampling.py +412 -0
  100. ithrl/statistics.py +276 -0
  101. ithrl/synthetic.py +511 -0
  102. ithrl/timestamps.py +26 -0
  103. ithrl/workflow.py +570 -0
  104. ithrl-0.1.0.dist-info/METADATA +118 -0
  105. ithrl-0.1.0.dist-info/RECORD +108 -0
  106. ithrl-0.1.0.dist-info/WHEEL +4 -0
  107. ithrl-0.1.0.dist-info/entry_points.txt +2 -0
  108. ithrl-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,182 @@
1
+ """Compile editable context graphs from parsed GitHub Copilot CLI sessions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import cast
6
+
7
+ from ithrl.adapters.contracts import NativeSession
8
+ from ithrl.adapters.errors import AdapterError, AdapterErrorCode
9
+ from ithrl.context import (
10
+ MAX_CONTEXT_NODES,
11
+ ContextGraph,
12
+ ContextNode,
13
+ JsonObject,
14
+ JsonValue,
15
+ SourceRecordSelector,
16
+ content_digest,
17
+ stable_source_context_id,
18
+ )
19
+ from ithrl.decisions import DecisionSelector
20
+
21
+ _HARNESS = "copilot"
22
+ _SOURCE = "source/events.jsonl"
23
+
24
+
25
+ def compile_context(session: NativeSession, selector: DecisionSelector) -> ContextGraph:
26
+ """Build context preceding one selector produced from the parsed session."""
27
+
28
+ records = session.records
29
+ _validate_selector(session, selector, records)
30
+ context_count = selector.context_before_line - 1
31
+ if context_count > MAX_CONTEXT_NODES:
32
+ raise AdapterError(
33
+ AdapterErrorCode.CONTEXT_TOO_LARGE,
34
+ "selected decision has too many preceding context records",
35
+ )
36
+ return ContextGraph(nodes=_context_nodes(records[:context_count]))
37
+
38
+
39
+ def source_selector(index: int, record: JsonObject) -> SourceRecordSelector:
40
+ """Return the stable native selector for one Copilot JSONL record."""
41
+
42
+ return SourceRecordSelector(
43
+ line=index + 1,
44
+ identity_key="id",
45
+ identity_value=cast(str, record["id"]),
46
+ )
47
+
48
+
49
+ def opaque_prefix(records: tuple[JsonObject, ...]) -> tuple[bool, ...]:
50
+ """Track whether each source prefix contains frozen opaque context."""
51
+
52
+ prefixes = [False]
53
+ for record in records:
54
+ record_type = cast(str, record["type"])
55
+ prefixes.append(prefixes[-1] or _is_opaque(record_type))
56
+ return tuple(prefixes)
57
+
58
+
59
+ def _validate_selector(
60
+ session: NativeSession,
61
+ selector: DecisionSelector,
62
+ records: tuple[JsonObject, ...],
63
+ ) -> None:
64
+ index = selector.record.line - 1
65
+ if (
66
+ selector.harness != _HARNESS
67
+ or selector.session_id != session.metadata.session_id
68
+ or selector.source != _SOURCE
69
+ or index >= session.record_count
70
+ ):
71
+ raise AdapterError(
72
+ AdapterErrorCode.DECISION_NOT_FOUND,
73
+ "decision selector does not match the selected session",
74
+ )
75
+ record = records[index]
76
+ if selector.record != source_selector(
77
+ index, record
78
+ ) or selector.record_digest != content_digest(record):
79
+ raise AdapterError(
80
+ AdapterErrorCode.DECISION_NOT_FOUND,
81
+ "decision selector does not match the selected session",
82
+ )
83
+
84
+
85
+ def _context_nodes(records: tuple[JsonObject, ...]) -> tuple[ContextNode, ...]:
86
+ nodes: list[ContextNode] = []
87
+ tool_requests: dict[str, str] = {}
88
+ tool_starts: dict[str, str] = {}
89
+
90
+ for index, record in enumerate(records):
91
+ record_type = cast(str, record["type"])
92
+ digest = content_digest(record)
93
+ selector = source_selector(index, record)
94
+ node_id = stable_source_context_id(
95
+ source=_SOURCE,
96
+ selector=selector,
97
+ kind=record_type,
98
+ digest=digest,
99
+ )
100
+ nodes.append(
101
+ ContextNode(
102
+ id=node_id,
103
+ kind=record_type,
104
+ selector=selector,
105
+ mutable=_is_mutable(record_type),
106
+ protected=_is_protected(record_type),
107
+ depends_on=_dependencies(record_type, record, tool_requests, tool_starts),
108
+ source=_SOURCE,
109
+ digest=digest,
110
+ )
111
+ )
112
+ _remember_tool_relationship(record_type, record, node_id, tool_requests, tool_starts)
113
+
114
+ return tuple(nodes)
115
+
116
+
117
+ def _dependencies(
118
+ record_type: str,
119
+ record: JsonObject,
120
+ requests: dict[str, str],
121
+ starts: dict[str, str],
122
+ ) -> tuple[str, ...]:
123
+ tool_call_id = _string(_object(record.get("data")).get("toolCallId"))
124
+ if tool_call_id is None:
125
+ return ()
126
+ if record_type == "tool.execution_start" and tool_call_id in requests:
127
+ return (requests[tool_call_id],)
128
+ if record_type == "tool.execution_complete" and tool_call_id in starts:
129
+ return (starts[tool_call_id],)
130
+ return ()
131
+
132
+
133
+ def _remember_tool_relationship(
134
+ record_type: str,
135
+ record: JsonObject,
136
+ node_id: str,
137
+ requests: dict[str, str],
138
+ starts: dict[str, str],
139
+ ) -> None:
140
+ data = _object(record.get("data"))
141
+ if record_type == "assistant.message":
142
+ tool_requests = data.get("toolRequests")
143
+ if not isinstance(tool_requests, list):
144
+ return
145
+ for request in tool_requests:
146
+ if isinstance(request, dict) and (tool_call_id := _string(request.get("toolCallId"))):
147
+ requests[tool_call_id] = node_id
148
+ elif record_type == "tool.execution_start":
149
+ if tool_call_id := _string(data.get("toolCallId")):
150
+ starts[tool_call_id] = node_id
151
+
152
+
153
+ def _is_mutable(record_type: str) -> bool:
154
+ return record_type in {
155
+ "assistant.message",
156
+ "model.message",
157
+ "skill.invoked",
158
+ "tool.execution_complete",
159
+ "tool.execution_start",
160
+ "user.message",
161
+ }
162
+
163
+
164
+ def _is_protected(record_type: str) -> bool:
165
+ return record_type in {
166
+ "assistant.turn_end",
167
+ "assistant.turn_start",
168
+ "session.start",
169
+ "session.task_complete",
170
+ }
171
+
172
+
173
+ def _is_opaque(record_type: str) -> bool:
174
+ return not _is_mutable(record_type) and not _is_protected(record_type)
175
+
176
+
177
+ def _object(value: JsonValue | None) -> JsonObject:
178
+ return value if isinstance(value, dict) else {}
179
+
180
+
181
+ def _string(value: JsonValue | None) -> str | None:
182
+ return value if isinstance(value, str) and value else None
@@ -0,0 +1,294 @@
1
+ """Enumerate completed decisions from parsed GitHub Copilot CLI sessions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import Counter
6
+ from dataclasses import dataclass
7
+ from typing import cast
8
+
9
+ from ithrl.adapters.contracts import NativeSession
10
+ from ithrl.adapters.copilot.context import opaque_prefix, source_selector
11
+ from ithrl.adapters.errors import AdapterError, AdapterErrorCode
12
+ from ithrl.behavior import (
13
+ BehaviorObservation,
14
+ FinalResponseTarget,
15
+ ShellAction,
16
+ ShellActionTarget,
17
+ )
18
+ from ithrl.context import JsonObject, JsonValue, content_digest
19
+ from ithrl.decisions import (
20
+ MAX_DECISIONS,
21
+ DecisionCaseFacts,
22
+ DecisionDraft,
23
+ DecisionLimitation,
24
+ DecisionSelector,
25
+ TargetCandidate,
26
+ )
27
+
28
+ _ADAPTER = "copilot-session"
29
+ _ADAPTER_VERSION = "1"
30
+ _HARNESS = "copilot"
31
+ _SOURCE = "source/events.jsonl"
32
+ _SHELL_TOOL_NAMES = frozenset({"bash", "shell"})
33
+ _MAX_BEHAVIOR_BYTES = 64 * 1024
34
+ _MAX_SHELL_ACTIONS = 32
35
+
36
+
37
+ @dataclass(frozen=True, slots=True)
38
+ class _DecisionRecord:
39
+ index: int
40
+ context_before_line: int
41
+ turn_id: str
42
+ kind: str
43
+ native_id: str
44
+ observation: BehaviorObservation
45
+ targets: tuple[TargetCandidate, ...]
46
+ model: str | None
47
+
48
+
49
+ @dataclass(frozen=True, slots=True)
50
+ class _TurnInterval:
51
+ turn_id: str
52
+ start: int
53
+ stop: int
54
+
55
+
56
+ def enumerate_decisions(session: NativeSession) -> tuple[DecisionDraft, ...]:
57
+ """Return supported decisions whose enclosing assistant turn completed."""
58
+
59
+ if session.metadata.format.harness != _HARNESS:
60
+ raise ValueError("native session is not a GitHub Copilot CLI session")
61
+
62
+ records = session.records
63
+ completed = _completed_decision_records(records)
64
+ opaque_context = opaque_prefix(records)
65
+ return tuple(_build_draft(session, records, opaque_context, decision) for decision in completed)
66
+
67
+
68
+ def _completed_decision_records(
69
+ records: tuple[JsonObject, ...],
70
+ ) -> tuple[_DecisionRecord, ...]:
71
+ completed_turns = _completed_turns(records)
72
+ starts = {turn.start: turn for turn in completed_turns}
73
+ stops = {turn.stop: turn for turn in completed_turns}
74
+ active_turns: dict[str, list[_TurnInterval]] = {}
75
+ decisions: list[_DecisionRecord] = []
76
+ for index, record in enumerate(records):
77
+ if turn := starts.get(index):
78
+ active_turns.setdefault(turn.turn_id, []).append(turn)
79
+ decision = _decision_from_record(index, record, active_turns)
80
+ if decision is not None:
81
+ if len(decisions) == MAX_DECISIONS:
82
+ raise AdapterError(
83
+ AdapterErrorCode.TOO_MANY_DECISIONS,
84
+ "session contains too many completed decisions",
85
+ )
86
+ decisions.append(decision)
87
+ _close_turn(index, stops, active_turns)
88
+ return tuple(decisions)
89
+
90
+
91
+ def _decision_from_record(
92
+ index: int,
93
+ record: JsonObject,
94
+ active_turns: dict[str, list[_TurnInterval]],
95
+ ) -> _DecisionRecord | None:
96
+ if record.get("type") != "assistant.message":
97
+ return None
98
+ data = _object(record.get("data"))
99
+ turn_id = _bounded_string(data.get("turnId"), 1024)
100
+ turns = active_turns.get(turn_id) if turn_id is not None else None
101
+ turn = turns[-1] if turns else None
102
+ if turn is None:
103
+ return None
104
+ return _assistant_message_decision(index, turn.start + 1, data)
105
+
106
+
107
+ def _assistant_message_decision(
108
+ index: int,
109
+ context_before_line: int,
110
+ data: JsonObject,
111
+ ) -> _DecisionRecord | None:
112
+ turn_id = _bounded_string(data.get("turnId"), 1024)
113
+ native_id = _bounded_string(data.get("messageId"), 1024)
114
+ if turn_id is None or native_id is None:
115
+ return None
116
+
117
+ actions = _shell_actions(data.get("toolRequests"))
118
+ final_response = _final_response(data)
119
+ if not actions and final_response is None:
120
+ return None
121
+ targets = (
122
+ (_target_candidate(FinalResponseTarget(text_equals=final_response), ordinal=1),)
123
+ if final_response is not None
124
+ else _shell_targets(actions)
125
+ )
126
+ return _DecisionRecord(
127
+ index=index,
128
+ context_before_line=context_before_line,
129
+ turn_id=turn_id,
130
+ kind="copilot-assistant-message",
131
+ native_id=native_id,
132
+ observation=BehaviorObservation(
133
+ final_response=final_response,
134
+ shell_actions=actions,
135
+ ),
136
+ targets=targets,
137
+ model=_bounded_string(data.get("model"), 256),
138
+ )
139
+
140
+
141
+ def _final_response(data: JsonObject) -> str | None:
142
+ tool_requests = data.get("toolRequests")
143
+ if (
144
+ data.get("phase") != "final_answer"
145
+ or data.get("parentToolCallId") is not None
146
+ or not isinstance(tool_requests, list)
147
+ or tool_requests
148
+ ):
149
+ return None
150
+ return _behavior_text(data.get("content"))
151
+
152
+
153
+ def _shell_targets(actions: tuple[ShellAction, ...]) -> tuple[TargetCandidate, ...]:
154
+ counts = Counter((action.tool_name, action.command) for action in actions)
155
+ return tuple(
156
+ _target_candidate(
157
+ ShellActionTarget(
158
+ tool_name_equals=tool_name,
159
+ command_equals=command,
160
+ count=count,
161
+ ),
162
+ ordinal=ordinal,
163
+ )
164
+ for ordinal, ((tool_name, command), count) in enumerate(counts.items(), start=1)
165
+ )
166
+
167
+
168
+ def _close_turn(
169
+ index: int,
170
+ stops: dict[int, _TurnInterval],
171
+ active_turns: dict[str, list[_TurnInterval]],
172
+ ) -> None:
173
+ turn = stops.get(index)
174
+ if turn is None:
175
+ return
176
+ turns = active_turns[turn.turn_id]
177
+ turns.pop()
178
+ if not turns:
179
+ del active_turns[turn.turn_id]
180
+
181
+
182
+ def _completed_turns(records: tuple[JsonObject, ...]) -> tuple[_TurnInterval, ...]:
183
+ open_turns: dict[str, list[int]] = {}
184
+ completed: list[_TurnInterval] = []
185
+ for index, record in enumerate(records):
186
+ record_type = record.get("type")
187
+ if record_type not in {"assistant.turn_start", "assistant.turn_end"}:
188
+ continue
189
+ turn_id = _bounded_string(_object(record.get("data")).get("turnId"), 1024)
190
+ if turn_id is None:
191
+ continue
192
+ if record_type == "assistant.turn_start":
193
+ open_turns.setdefault(turn_id, []).append(index)
194
+ elif starts := open_turns.get(turn_id):
195
+ completed.append(_TurnInterval(turn_id=turn_id, start=starts.pop(), stop=index))
196
+ if not starts:
197
+ del open_turns[turn_id]
198
+ return tuple(completed)
199
+
200
+
201
+ def _shell_actions(value: JsonValue | None) -> tuple[ShellAction, ...]:
202
+ if not isinstance(value, list):
203
+ return ()
204
+
205
+ actions: list[ShellAction] = []
206
+ total_bytes = 0
207
+ for request in value:
208
+ if not isinstance(request, dict):
209
+ continue
210
+ name = _bounded_string(request.get("name"), 256)
211
+ arguments = _object(request.get("arguments"))
212
+ command = _behavior_text(arguments.get("command"))
213
+ if name is None or name not in _SHELL_TOOL_NAMES or command is None:
214
+ continue
215
+ total_bytes += len(command.encode("utf-8"))
216
+ if total_bytes > _MAX_BEHAVIOR_BYTES or len(actions) == _MAX_SHELL_ACTIONS:
217
+ return ()
218
+ actions.append(ShellAction(tool_name=name, command=command))
219
+ return tuple(actions)
220
+
221
+
222
+ def _target_candidate(
223
+ target: FinalResponseTarget | ShellActionTarget,
224
+ *,
225
+ ordinal: int,
226
+ ) -> TargetCandidate:
227
+ return TargetCandidate(
228
+ ordinal=ordinal,
229
+ target=target,
230
+ evidence_support="supported",
231
+ experimental_support="unvalidated",
232
+ )
233
+
234
+
235
+ def _build_draft(
236
+ session: NativeSession,
237
+ records: tuple[JsonObject, ...],
238
+ opaque_prefix: tuple[bool, ...],
239
+ decision: _DecisionRecord,
240
+ ) -> DecisionDraft:
241
+ record = records[decision.index]
242
+ selector = _selector(session, record, decision)
243
+ return DecisionDraft(
244
+ case=DecisionCaseFacts(
245
+ adapter=_ADAPTER,
246
+ adapter_version=_ADAPTER_VERSION,
247
+ model_requested=decision.model,
248
+ source_schema=session.metadata.format.name,
249
+ ),
250
+ selector=selector,
251
+ observed_at=cast(str, record["timestamp"]),
252
+ observation=decision.observation,
253
+ targets=decision.targets,
254
+ limitations=_limitations(opaque_prefix[decision.context_before_line - 1]),
255
+ )
256
+
257
+
258
+ def _limitations(has_opaque_context: bool) -> tuple[DecisionLimitation, ...]:
259
+ limitations: list[DecisionLimitation] = ["experimental-support-unvalidated"]
260
+ if has_opaque_context:
261
+ limitations.append("opaque-context-frozen")
262
+ return tuple(limitations)
263
+
264
+
265
+ def _selector(
266
+ session: NativeSession,
267
+ record: JsonObject,
268
+ decision: _DecisionRecord,
269
+ ) -> DecisionSelector:
270
+ return DecisionSelector(
271
+ harness=_HARNESS,
272
+ session_id=session.metadata.session_id,
273
+ turn_id=decision.turn_id,
274
+ kind=decision.kind,
275
+ native_id=decision.native_id,
276
+ source=_SOURCE,
277
+ context_before_line=decision.context_before_line,
278
+ record=source_selector(decision.index, record),
279
+ record_digest=content_digest(record),
280
+ )
281
+
282
+
283
+ def _object(value: JsonValue | None) -> JsonObject:
284
+ return value if isinstance(value, dict) else {}
285
+
286
+
287
+ def _bounded_string(value: JsonValue | None, limit: int) -> str | None:
288
+ return value if isinstance(value, str) and 0 < len(value) <= limit else None
289
+
290
+
291
+ def _behavior_text(value: JsonValue | None) -> str | None:
292
+ if not isinstance(value, str) or not value:
293
+ return None
294
+ return value if len(value.encode("utf-8")) <= _MAX_BEHAVIOR_BYTES else None
@@ -0,0 +1,28 @@
1
+ """Discover ordinary GitHub Copilot CLI session event logs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from ithrl.adapters.discovery import DiscoveryLimits, SourceScan, scan_store
8
+
9
+
10
+ def discover_sources(
11
+ *,
12
+ root: Path | None = None,
13
+ limits: DiscoveryLimits | None = None,
14
+ session_id: str | None = None,
15
+ ) -> SourceScan:
16
+ """Return event logs from the ordinary Copilot CLI session store."""
17
+
18
+ store = root if root is not None else Path.home() / ".copilot" / "session-state"
19
+ return scan_store(
20
+ store,
21
+ max_depth=1,
22
+ accepts=lambda path: (
23
+ len(path.parts) == 2
24
+ and path.name == "events.jsonl"
25
+ and (session_id is None or path.parent.name == session_id)
26
+ ),
27
+ limits=limits or DiscoveryLimits(),
28
+ )