agent-runtime-core 0.6.0__py3-none-any.whl → 0.7.1__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 (33) hide show
  1. agent_runtime_core/__init__.py +118 -2
  2. agent_runtime_core/agentic_loop.py +254 -0
  3. agent_runtime_core/config.py +54 -4
  4. agent_runtime_core/config_schema.py +307 -0
  5. agent_runtime_core/contexts.py +348 -0
  6. agent_runtime_core/interfaces.py +106 -0
  7. agent_runtime_core/json_runtime.py +509 -0
  8. agent_runtime_core/llm/__init__.py +80 -7
  9. agent_runtime_core/llm/anthropic.py +133 -12
  10. agent_runtime_core/llm/models_config.py +180 -0
  11. agent_runtime_core/memory/__init__.py +70 -0
  12. agent_runtime_core/memory/manager.py +554 -0
  13. agent_runtime_core/memory/mixin.py +294 -0
  14. agent_runtime_core/multi_agent.py +569 -0
  15. agent_runtime_core/persistence/__init__.py +2 -0
  16. agent_runtime_core/persistence/file.py +277 -0
  17. agent_runtime_core/rag/__init__.py +65 -0
  18. agent_runtime_core/rag/chunking.py +224 -0
  19. agent_runtime_core/rag/indexer.py +253 -0
  20. agent_runtime_core/rag/retriever.py +261 -0
  21. agent_runtime_core/runner.py +193 -15
  22. agent_runtime_core/tool_calling_agent.py +88 -130
  23. agent_runtime_core/tools.py +179 -0
  24. agent_runtime_core/vectorstore/__init__.py +193 -0
  25. agent_runtime_core/vectorstore/base.py +138 -0
  26. agent_runtime_core/vectorstore/embeddings.py +242 -0
  27. agent_runtime_core/vectorstore/sqlite_vec.py +328 -0
  28. agent_runtime_core/vectorstore/vertex.py +295 -0
  29. {agent_runtime_core-0.6.0.dist-info → agent_runtime_core-0.7.1.dist-info}/METADATA +202 -1
  30. agent_runtime_core-0.7.1.dist-info/RECORD +57 -0
  31. agent_runtime_core-0.6.0.dist-info/RECORD +0 -38
  32. {agent_runtime_core-0.6.0.dist-info → agent_runtime_core-0.7.1.dist-info}/WHEEL +0 -0
  33. {agent_runtime_core-0.6.0.dist-info → agent_runtime_core-0.7.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,307 @@
1
+ """
2
+ AgentConfig - Canonical JSON schema for portable agent definitions.
3
+
4
+ This schema defines the format for agent configurations that can be:
5
+ 1. Stored in Django as JSON revisions
6
+ 2. Loaded from standalone .json files
7
+ 3. Used by agent_runtime_core without Django dependency
8
+
9
+ Example:
10
+ # Load from file
11
+ config = AgentConfig.from_file("my_agent.json")
12
+
13
+ # Create runtime and run
14
+ runtime = JsonAgentRuntime(config, llm_client)
15
+ result = await runtime.run(ctx)
16
+ """
17
+
18
+ from dataclasses import dataclass, field
19
+ from datetime import datetime
20
+ from typing import Any, Optional
21
+ import json
22
+ from pathlib import Path
23
+
24
+
25
+ @dataclass
26
+ class SubAgentToolConfig:
27
+ """
28
+ Configuration for a sub-agent tool (agent-as-tool pattern).
29
+
30
+ This allows an agent to delegate to another agent as if it were a tool.
31
+ The sub-agent can either be referenced by slug (resolved at runtime)
32
+ or embedded inline (for portable standalone configs).
33
+ """
34
+
35
+ name: str # Tool name the parent uses to invoke this agent
36
+ description: str # When to use this agent (shown to parent LLM)
37
+
38
+ # Reference to sub-agent (one of these should be set)
39
+ agent_slug: str = "" # Reference by slug (resolved at runtime from registry)
40
+ agent_config: Optional["AgentConfig"] = None # Embedded config (for standalone)
41
+
42
+ # Invocation settings
43
+ invocation_mode: str = "delegate" # "delegate" or "handoff"
44
+ context_mode: str = "full" # "full", "summary", or "message_only"
45
+ max_turns: Optional[int] = None
46
+
47
+ def to_dict(self) -> dict:
48
+ result = {
49
+ "name": self.name,
50
+ "description": self.description,
51
+ "tool_type": "subagent",
52
+ "invocation_mode": self.invocation_mode,
53
+ "context_mode": self.context_mode,
54
+ }
55
+ if self.agent_slug:
56
+ result["agent_slug"] = self.agent_slug
57
+ if self.agent_config:
58
+ result["agent_config"] = self.agent_config.to_dict()
59
+ if self.max_turns is not None:
60
+ result["max_turns"] = self.max_turns
61
+ return result
62
+
63
+ @classmethod
64
+ def from_dict(cls, data: dict) -> "SubAgentToolConfig":
65
+ agent_config = None
66
+ if "agent_config" in data and data["agent_config"]:
67
+ # Defer import to avoid circular dependency
68
+ agent_config = AgentConfig.from_dict(data["agent_config"])
69
+
70
+ return cls(
71
+ name=data["name"],
72
+ description=data["description"],
73
+ agent_slug=data.get("agent_slug", ""),
74
+ agent_config=agent_config,
75
+ invocation_mode=data.get("invocation_mode", "delegate"),
76
+ context_mode=data.get("context_mode", "full"),
77
+ max_turns=data.get("max_turns"),
78
+ )
79
+
80
+
81
+ @dataclass
82
+ class ToolConfig:
83
+ """Configuration for a single tool."""
84
+
85
+ name: str
86
+ description: str
87
+ parameters: dict # JSON Schema for parameters
88
+ function_path: str # Import path like "myapp.services.orders.lookup_order"
89
+
90
+ # Optional metadata
91
+ requires_confirmation: bool = False
92
+ is_safe: bool = True # No side effects
93
+ timeout_seconds: int = 30
94
+
95
+ def to_dict(self) -> dict:
96
+ return {
97
+ "name": self.name,
98
+ "description": self.description,
99
+ "parameters": self.parameters,
100
+ "function_path": self.function_path,
101
+ "requires_confirmation": self.requires_confirmation,
102
+ "is_safe": self.is_safe,
103
+ "timeout_seconds": self.timeout_seconds,
104
+ }
105
+
106
+ @classmethod
107
+ def from_dict(cls, data: dict) -> "ToolConfig":
108
+ return cls(
109
+ name=data["name"],
110
+ description=data["description"],
111
+ parameters=data.get("parameters", {}),
112
+ function_path=data.get("function_path", ""),
113
+ requires_confirmation=data.get("requires_confirmation", False),
114
+ is_safe=data.get("is_safe", True),
115
+ timeout_seconds=data.get("timeout_seconds", 30),
116
+ )
117
+
118
+
119
+ @dataclass
120
+ class KnowledgeConfig:
121
+ """Configuration for a knowledge source."""
122
+
123
+ name: str
124
+ knowledge_type: str # "text", "file", "url"
125
+ inclusion_mode: str = "always" # "always", "on_demand", "rag"
126
+
127
+ # Content (depends on type)
128
+ content: str = "" # For text type
129
+ file_path: str = "" # For file type
130
+ url: str = "" # For url type
131
+
132
+ def to_dict(self) -> dict:
133
+ return {
134
+ "name": self.name,
135
+ "type": self.knowledge_type,
136
+ "inclusion_mode": self.inclusion_mode,
137
+ "content": self.content,
138
+ "file_path": self.file_path,
139
+ "url": self.url,
140
+ }
141
+
142
+ @classmethod
143
+ def from_dict(cls, data: dict) -> "KnowledgeConfig":
144
+ return cls(
145
+ name=data["name"],
146
+ knowledge_type=data.get("type", "text"),
147
+ inclusion_mode=data.get("inclusion_mode", "always"),
148
+ content=data.get("content", ""),
149
+ file_path=data.get("file_path", ""),
150
+ url=data.get("url", ""),
151
+ )
152
+
153
+
154
+ @dataclass
155
+ class AgentConfig:
156
+ """
157
+ Canonical configuration for an agent.
158
+
159
+ This is the portable format that can be serialized to JSON and
160
+ loaded by any runtime (Django or standalone).
161
+
162
+ For multi-agent systems, sub_agents contains embedded agent configs
163
+ that this agent can delegate to. The sub_agent_tools list defines
164
+ how each sub-agent is exposed as a tool.
165
+ """
166
+
167
+ # Identity
168
+ name: str
169
+ slug: str
170
+ description: str = ""
171
+
172
+ # Core configuration
173
+ system_prompt: str = ""
174
+ model: str = "gpt-4o"
175
+ model_settings: dict = field(default_factory=dict)
176
+
177
+ # Tools and knowledge
178
+ tools: list[ToolConfig] = field(default_factory=list)
179
+ knowledge: list[KnowledgeConfig] = field(default_factory=list)
180
+
181
+ # Sub-agent tools (agent-as-tool pattern)
182
+ # These define other agents this agent can delegate to
183
+ sub_agent_tools: list[SubAgentToolConfig] = field(default_factory=list)
184
+
185
+ # Metadata
186
+ version: str = "1.0"
187
+ schema_version: str = "1" # For future schema migrations
188
+ created_at: Optional[str] = None
189
+ updated_at: Optional[str] = None
190
+
191
+ # Extra config (for extensibility)
192
+ extra: dict = field(default_factory=dict)
193
+
194
+ def to_dict(self) -> dict:
195
+ """Serialize to dictionary (JSON-compatible)."""
196
+ result = {
197
+ "schema_version": self.schema_version,
198
+ "version": self.version,
199
+ "name": self.name,
200
+ "slug": self.slug,
201
+ "description": self.description,
202
+ "system_prompt": self.system_prompt,
203
+ "model": self.model,
204
+ "model_settings": self.model_settings,
205
+ "tools": [t.to_dict() for t in self.tools],
206
+ "knowledge": [k.to_dict() for k in self.knowledge],
207
+ "created_at": self.created_at,
208
+ "updated_at": self.updated_at,
209
+ "extra": self.extra,
210
+ }
211
+ # Only include sub_agent_tools if there are any
212
+ if self.sub_agent_tools:
213
+ result["sub_agent_tools"] = [s.to_dict() for s in self.sub_agent_tools]
214
+ return result
215
+
216
+ def to_json(self, indent: int = 2) -> str:
217
+ """Serialize to JSON string."""
218
+ return json.dumps(self.to_dict(), indent=indent)
219
+
220
+ def save(self, path: str | Path) -> None:
221
+ """Save to a JSON file."""
222
+ path = Path(path)
223
+ path.write_text(self.to_json())
224
+
225
+ @classmethod
226
+ def from_dict(cls, data: dict) -> "AgentConfig":
227
+ """Load from dictionary."""
228
+ # Parse regular tools (skip subagent tools - they're in sub_agent_tools)
229
+ tools = []
230
+ for t in data.get("tools", []):
231
+ if t.get("tool_type") != "subagent":
232
+ tools.append(ToolConfig.from_dict(t))
233
+
234
+ knowledge = [KnowledgeConfig.from_dict(k) for k in data.get("knowledge", [])]
235
+
236
+ # Parse sub-agent tools
237
+ sub_agent_tools = []
238
+ for s in data.get("sub_agent_tools", []):
239
+ sub_agent_tools.append(SubAgentToolConfig.from_dict(s))
240
+ # Also check tools list for subagent type (backwards compat)
241
+ for t in data.get("tools", []):
242
+ if t.get("tool_type") == "subagent":
243
+ sub_agent_tools.append(SubAgentToolConfig.from_dict(t))
244
+
245
+ return cls(
246
+ name=data["name"],
247
+ slug=data["slug"],
248
+ description=data.get("description", ""),
249
+ system_prompt=data.get("system_prompt", ""),
250
+ model=data.get("model", "gpt-4o"),
251
+ model_settings=data.get("model_settings", {}),
252
+ tools=tools,
253
+ knowledge=knowledge,
254
+ sub_agent_tools=sub_agent_tools,
255
+ version=data.get("version", "1.0"),
256
+ schema_version=data.get("schema_version", "1"),
257
+ created_at=data.get("created_at"),
258
+ updated_at=data.get("updated_at"),
259
+ extra=data.get("extra", {}),
260
+ )
261
+
262
+ @classmethod
263
+ def from_json(cls, json_str: str) -> "AgentConfig":
264
+ """Load from JSON string."""
265
+ return cls.from_dict(json.loads(json_str))
266
+
267
+ @classmethod
268
+ def from_file(cls, path: str | Path) -> "AgentConfig":
269
+ """Load from a JSON file."""
270
+ path = Path(path)
271
+ return cls.from_json(path.read_text())
272
+
273
+ def with_timestamp(self) -> "AgentConfig":
274
+ """Return a copy with updated timestamp."""
275
+ now = datetime.utcnow().isoformat() + "Z"
276
+ return AgentConfig(
277
+ name=self.name,
278
+ slug=self.slug,
279
+ description=self.description,
280
+ system_prompt=self.system_prompt,
281
+ model=self.model,
282
+ model_settings=self.model_settings,
283
+ tools=self.tools,
284
+ knowledge=self.knowledge,
285
+ sub_agent_tools=self.sub_agent_tools,
286
+ version=self.version,
287
+ schema_version=self.schema_version,
288
+ created_at=self.created_at or now,
289
+ updated_at=now,
290
+ extra=self.extra,
291
+ )
292
+
293
+ def get_all_embedded_agents(self) -> dict[str, "AgentConfig"]:
294
+ """
295
+ Get all embedded agent configs (for standalone execution).
296
+
297
+ Returns a dict mapping slug -> AgentConfig for all sub-agents
298
+ that have embedded configs (not just slug references).
299
+ """
300
+ agents = {}
301
+ for sub_tool in self.sub_agent_tools:
302
+ if sub_tool.agent_config:
303
+ agents[sub_tool.agent_config.slug] = sub_tool.agent_config
304
+ # Recursively get nested sub-agents
305
+ agents.update(sub_tool.agent_config.get_all_embedded_agents())
306
+ return agents
307
+
@@ -0,0 +1,348 @@
1
+ """
2
+ Concrete RunContext implementations for different use cases.
3
+
4
+ These implementations satisfy the RunContext protocol and can be used
5
+ directly with StepExecutor and agent runtimes.
6
+
7
+ Usage:
8
+ # For simple scripts (in-memory, no persistence)
9
+ ctx = InMemoryRunContext(run_id=uuid4())
10
+
11
+ # For scripts that need persistence across restarts
12
+ ctx = FileRunContext(run_id=uuid4(), checkpoint_dir="./checkpoints")
13
+
14
+ # Use with StepExecutor
15
+ from agent_runtime_core.steps import StepExecutor, Step
16
+ executor = StepExecutor(ctx)
17
+ results = await executor.run([
18
+ Step("fetch", fetch_data),
19
+ Step("process", process_data),
20
+ ])
21
+ """
22
+
23
+ import json
24
+ import os
25
+ from datetime import datetime
26
+ from pathlib import Path
27
+ from typing import Any, Callable, Optional
28
+ from uuid import UUID, uuid4
29
+
30
+ from agent_runtime_core.interfaces import EventType, Message, ToolRegistry
31
+
32
+
33
+ class InMemoryRunContext:
34
+ """
35
+ In-memory RunContext implementation.
36
+
37
+ Good for:
38
+ - Unit testing
39
+ - Simple scripts that don't need persistence
40
+ - Development and prototyping
41
+
42
+ State is lost when the process exits.
43
+
44
+ Example:
45
+ ctx = InMemoryRunContext(
46
+ run_id=uuid4(),
47
+ input_messages=[{"role": "user", "content": "Hello"}],
48
+ )
49
+
50
+ # Use with an agent
51
+ result = await my_agent.run(ctx)
52
+
53
+ # Or with StepExecutor
54
+ executor = StepExecutor(ctx)
55
+ results = await executor.run(steps)
56
+ """
57
+
58
+ def __init__(
59
+ self,
60
+ run_id: Optional[UUID] = None,
61
+ *,
62
+ conversation_id: Optional[UUID] = None,
63
+ input_messages: Optional[list[Message]] = None,
64
+ params: Optional[dict] = None,
65
+ metadata: Optional[dict] = None,
66
+ tool_registry: Optional[ToolRegistry] = None,
67
+ on_event: Optional[Callable[[str, dict], None]] = None,
68
+ ):
69
+ """
70
+ Initialize an in-memory run context.
71
+
72
+ Args:
73
+ run_id: Unique identifier for this run (auto-generated if not provided)
74
+ conversation_id: Associated conversation ID (optional)
75
+ input_messages: Input messages for this run
76
+ params: Additional parameters
77
+ metadata: Run metadata
78
+ tool_registry: Registry of available tools
79
+ on_event: Optional callback for events (for testing/debugging)
80
+ """
81
+ self._run_id = run_id or uuid4()
82
+ self._conversation_id = conversation_id
83
+ self._input_messages = input_messages or []
84
+ self._params = params or {}
85
+ self._metadata = metadata or {}
86
+ self._tool_registry = tool_registry or ToolRegistry()
87
+ self._cancelled = False
88
+ self._state: Optional[dict] = None
89
+ self._events: list[dict] = []
90
+ self._on_event = on_event
91
+
92
+ @property
93
+ def run_id(self) -> UUID:
94
+ """Unique identifier for this run."""
95
+ return self._run_id
96
+
97
+ @property
98
+ def conversation_id(self) -> Optional[UUID]:
99
+ """Conversation this run belongs to (if any)."""
100
+ return self._conversation_id
101
+
102
+ @property
103
+ def input_messages(self) -> list[Message]:
104
+ """Input messages for this run."""
105
+ return self._input_messages
106
+
107
+ @property
108
+ def params(self) -> dict:
109
+ """Additional parameters for this run."""
110
+ return self._params
111
+
112
+ @property
113
+ def metadata(self) -> dict:
114
+ """Metadata associated with this run."""
115
+ return self._metadata
116
+
117
+ @property
118
+ def tool_registry(self) -> ToolRegistry:
119
+ """Registry of available tools for this agent."""
120
+ return self._tool_registry
121
+
122
+ async def emit(self, event_type: EventType | str, payload: dict) -> None:
123
+ """Emit an event (stored in memory)."""
124
+ event_type_str = event_type.value if hasattr(event_type, 'value') else str(event_type)
125
+ event = {
126
+ "event_type": event_type_str,
127
+ "payload": payload,
128
+ "timestamp": datetime.utcnow().isoformat(),
129
+ }
130
+ self._events.append(event)
131
+ if self._on_event:
132
+ self._on_event(event_type_str, payload)
133
+
134
+ async def checkpoint(self, state: dict) -> None:
135
+ """Save a state checkpoint (in memory)."""
136
+ self._state = state
137
+
138
+ async def get_state(self) -> Optional[dict]:
139
+ """Get the last checkpointed state."""
140
+ return self._state
141
+
142
+ def cancelled(self) -> bool:
143
+ """Check if cancellation has been requested."""
144
+ return self._cancelled
145
+
146
+ def cancel(self) -> None:
147
+ """Request cancellation of this run."""
148
+ self._cancelled = True
149
+
150
+ @property
151
+ def events(self) -> list[dict]:
152
+ """Get all emitted events (for testing/debugging)."""
153
+ return self._events.copy()
154
+
155
+ def clear_events(self) -> None:
156
+ """Clear all events (for testing)."""
157
+ self._events.clear()
158
+
159
+
160
+ class FileRunContext:
161
+ """
162
+ File-based RunContext implementation with persistent checkpoints.
163
+
164
+ Good for:
165
+ - Scripts that need to resume after restart
166
+ - Long-running processes without a database
167
+ - Simple persistence without external dependencies
168
+
169
+ Checkpoints are saved as JSON files in the specified directory.
170
+
171
+ Example:
172
+ ctx = FileRunContext(
173
+ run_id=uuid4(),
174
+ checkpoint_dir="./checkpoints",
175
+ input_messages=[{"role": "user", "content": "Process this"}],
176
+ )
177
+
178
+ # Checkpoints are saved to ./checkpoints/{run_id}.json
179
+ executor = StepExecutor(ctx)
180
+ results = await executor.run(steps)
181
+
182
+ # To resume after restart, use the same run_id:
183
+ ctx = FileRunContext(run_id=previous_run_id, checkpoint_dir="./checkpoints")
184
+ results = await executor.run(steps, resume=True)
185
+ """
186
+
187
+ def __init__(
188
+ self,
189
+ run_id: Optional[UUID] = None,
190
+ *,
191
+ checkpoint_dir: str = "./checkpoints",
192
+ conversation_id: Optional[UUID] = None,
193
+ input_messages: Optional[list[Message]] = None,
194
+ params: Optional[dict] = None,
195
+ metadata: Optional[dict] = None,
196
+ tool_registry: Optional[ToolRegistry] = None,
197
+ on_event: Optional[Callable[[str, dict], None]] = None,
198
+ ):
199
+ """
200
+ Initialize a file-based run context.
201
+
202
+ Args:
203
+ run_id: Unique identifier for this run (auto-generated if not provided)
204
+ checkpoint_dir: Directory to store checkpoint files
205
+ conversation_id: Associated conversation ID (optional)
206
+ input_messages: Input messages for this run
207
+ params: Additional parameters
208
+ metadata: Run metadata
209
+ tool_registry: Registry of available tools
210
+ on_event: Optional callback for events
211
+ """
212
+ self._run_id = run_id or uuid4()
213
+ self._checkpoint_dir = Path(checkpoint_dir)
214
+ self._conversation_id = conversation_id
215
+ self._input_messages = input_messages or []
216
+ self._params = params or {}
217
+ self._metadata = metadata or {}
218
+ self._tool_registry = tool_registry or ToolRegistry()
219
+ self._cancelled = False
220
+ self._on_event = on_event
221
+ self._state_cache: Optional[dict] = None
222
+
223
+ # Ensure checkpoint directory exists
224
+ self._checkpoint_dir.mkdir(parents=True, exist_ok=True)
225
+
226
+ @property
227
+ def run_id(self) -> UUID:
228
+ """Unique identifier for this run."""
229
+ return self._run_id
230
+
231
+ @property
232
+ def conversation_id(self) -> Optional[UUID]:
233
+ """Conversation this run belongs to (if any)."""
234
+ return self._conversation_id
235
+
236
+ @property
237
+ def input_messages(self) -> list[Message]:
238
+ """Input messages for this run."""
239
+ return self._input_messages
240
+
241
+ @property
242
+ def params(self) -> dict:
243
+ """Additional parameters for this run."""
244
+ return self._params
245
+
246
+ @property
247
+ def metadata(self) -> dict:
248
+ """Metadata associated with this run."""
249
+ return self._metadata
250
+
251
+ @property
252
+ def tool_registry(self) -> ToolRegistry:
253
+ """Registry of available tools for this agent."""
254
+ return self._tool_registry
255
+
256
+ def _checkpoint_path(self) -> Path:
257
+ """Get the path to the checkpoint file for this run."""
258
+ return self._checkpoint_dir / f"{self._run_id}.json"
259
+
260
+ def _events_path(self) -> Path:
261
+ """Get the path to the events file for this run."""
262
+ return self._checkpoint_dir / f"{self._run_id}_events.jsonl"
263
+
264
+ async def emit(self, event_type: EventType | str, payload: dict) -> None:
265
+ """Emit an event (appended to events file)."""
266
+ event_type_str = event_type.value if hasattr(event_type, 'value') else str(event_type)
267
+ event = {
268
+ "event_type": event_type_str,
269
+ "payload": payload,
270
+ "timestamp": datetime.utcnow().isoformat(),
271
+ }
272
+
273
+ # Append to events file (JSONL format)
274
+ with open(self._events_path(), "a") as f:
275
+ f.write(json.dumps(event) + "\n")
276
+
277
+ if self._on_event:
278
+ self._on_event(event_type_str, payload)
279
+
280
+ async def checkpoint(self, state: dict) -> None:
281
+ """Save a state checkpoint to file."""
282
+ self._state_cache = state
283
+ checkpoint_data = {
284
+ "run_id": str(self._run_id),
285
+ "state": state,
286
+ "updated_at": datetime.utcnow().isoformat(),
287
+ }
288
+
289
+ # Write atomically using temp file
290
+ temp_path = self._checkpoint_path().with_suffix(".tmp")
291
+ with open(temp_path, "w") as f:
292
+ json.dump(checkpoint_data, f, indent=2)
293
+ temp_path.rename(self._checkpoint_path())
294
+
295
+ async def get_state(self) -> Optional[dict]:
296
+ """Get the last checkpointed state from file."""
297
+ if self._state_cache is not None:
298
+ return self._state_cache
299
+
300
+ checkpoint_path = self._checkpoint_path()
301
+ if not checkpoint_path.exists():
302
+ return None
303
+
304
+ try:
305
+ with open(checkpoint_path) as f:
306
+ data = json.load(f)
307
+ self._state_cache = data.get("state")
308
+ return self._state_cache
309
+ except (json.JSONDecodeError, IOError):
310
+ return None
311
+
312
+ def cancelled(self) -> bool:
313
+ """Check if cancellation has been requested."""
314
+ return self._cancelled
315
+
316
+ def cancel(self) -> None:
317
+ """Request cancellation of this run."""
318
+ self._cancelled = True
319
+
320
+ def get_events(self) -> list[dict]:
321
+ """Read all events from the events file."""
322
+ events_path = self._events_path()
323
+ if not events_path.exists():
324
+ return []
325
+
326
+ events = []
327
+ with open(events_path) as f:
328
+ for line in f:
329
+ line = line.strip()
330
+ if line:
331
+ try:
332
+ events.append(json.loads(line))
333
+ except json.JSONDecodeError:
334
+ pass
335
+ return events
336
+
337
+ def clear(self) -> None:
338
+ """Delete checkpoint and events files for this run."""
339
+ checkpoint_path = self._checkpoint_path()
340
+ events_path = self._events_path()
341
+
342
+ if checkpoint_path.exists():
343
+ checkpoint_path.unlink()
344
+ if events_path.exists():
345
+ events_path.unlink()
346
+
347
+ self._state_cache = None
348
+