Persist runtime owner metadata in sessions
- SHA
c694b5a28f9d20ef9e96c1b1b4926176cd65db28- Parents
-
69d958e - Tree
6af580d
c694b5a
c694b5a28f9d20ef9e96c1b1b4926176cd65db2869d958e
6af580d| Status | File | + | - |
|---|---|---|---|
| M |
src/loader/runtime/bootstrap.py
|
2 | 1 |
| A |
src/loader/runtime/owner_metadata.py
|
73 | 0 |
| M |
src/loader/runtime/public_shell.py
|
21 | 0 |
| M |
src/loader/runtime/session.py
|
33 | 1 |
| M |
tests/test_runtime_bootstrap.py
|
8 | 2 |
| M |
tests/test_runtime_handle.py
|
8 | 2 |
| M |
tests/test_runtime_launcher.py
|
4 | 1 |
| M |
tests/test_runtime_public_shell.py
|
10 | 0 |
| M |
tests/test_session_state.py
|
33 | 0 |
src/loader/runtime/bootstrap.pymodified@@ -17,6 +17,7 @@ from .context import ( | ||
| 17 | 17 | RuntimeSafeguardsProtocol, |
| 18 | 18 | ) |
| 19 | 19 | from .events import TurnSummary |
| 20 | +from .owner_metadata import build_runtime_owner_metadata | |
| 20 | 21 | from .permissions import PermissionConfigStatus, PermissionPolicy |
| 21 | 22 | from .reasoning_service import RuntimeReasoningService |
| 22 | 23 | from .session import ConversationSession |
@@ -174,7 +175,7 @@ def build_runtime_bootstrap_source(source: RuntimeBootstrapSource | Any) -> Runt | ||
| 174 | 175 | _queue_steering_message=source.queue_steering_message, |
| 175 | 176 | _drain_steering_messages=source.drain_steering_messages, |
| 176 | 177 | _refresh_capability_profile=source.refresh_capability_profile, |
| 177 | - metadata={"owner_type": type(source).__name__}, | |
| 178 | + metadata=build_runtime_owner_metadata(source), | |
| 178 | 179 | ) |
| 179 | 180 | |
| 180 | 181 | |
src/loader/runtime/owner_metadata.pyadded@@ -0,0 +1,73 @@ | ||
| 1 | +"""Shared helpers for runtime-owner metadata.""" | |
| 2 | + | |
| 3 | +from __future__ import annotations | |
| 4 | + | |
| 5 | +from typing import Any | |
| 6 | + | |
| 7 | +_KNOWN_RUNTIME_OWNER_PATHS = { | |
| 8 | + "Agent": "public-agent", | |
| 9 | + "RuntimeHandle": "runtime-handle", | |
| 10 | +} | |
| 11 | + | |
| 12 | + | |
| 13 | +def normalize_runtime_owner_type(value: Any) -> str | None: | |
| 14 | + """Coerce persisted runtime-owner types into optional text.""" | |
| 15 | + | |
| 16 | + if value is None: | |
| 17 | + return None | |
| 18 | + text = str(value).strip() | |
| 19 | + return text or None | |
| 20 | + | |
| 21 | + | |
| 22 | +def normalize_runtime_owner_path( | |
| 23 | + value: Any, | |
| 24 | + *, | |
| 25 | + owner_type: str | None = None, | |
| 26 | +) -> str | None: | |
| 27 | + """Coerce persisted runtime-owner paths into canonical text.""" | |
| 28 | + | |
| 29 | + if value is not None: | |
| 30 | + text = str(value).strip() | |
| 31 | + if text: | |
| 32 | + return text | |
| 33 | + if owner_type is None: | |
| 34 | + return None | |
| 35 | + return _KNOWN_RUNTIME_OWNER_PATHS.get(owner_type, _camel_to_kebab(owner_type)) | |
| 36 | + | |
| 37 | + | |
| 38 | +def build_runtime_owner_metadata(source: Any) -> dict[str, str | None]: | |
| 39 | + """Build canonical runtime-owner metadata from one shell owner.""" | |
| 40 | + | |
| 41 | + owner_type = ( | |
| 42 | + normalize_runtime_owner_type(source) | |
| 43 | + if isinstance(source, str) | |
| 44 | + else normalize_runtime_owner_type(type(source).__name__) | |
| 45 | + ) | |
| 46 | + return { | |
| 47 | + "owner_type": owner_type, | |
| 48 | + "owner_path": normalize_runtime_owner_path(None, owner_type=owner_type), | |
| 49 | + } | |
| 50 | + | |
| 51 | + | |
| 52 | +def format_runtime_owner_label( | |
| 53 | + owner_type: str | None, | |
| 54 | + owner_path: str | None, | |
| 55 | +) -> str | None: | |
| 56 | + """Render one compact human-readable runtime-owner label.""" | |
| 57 | + | |
| 58 | + normalized_type = normalize_runtime_owner_type(owner_type) | |
| 59 | + normalized_path = normalize_runtime_owner_path(owner_path, owner_type=normalized_type) | |
| 60 | + if normalized_type and normalized_path: | |
| 61 | + return f"{normalized_path} ({normalized_type})" | |
| 62 | + return normalized_path or normalized_type | |
| 63 | + | |
| 64 | + | |
| 65 | +def _camel_to_kebab(value: str) -> str: | |
| 66 | + """Convert one CamelCase-ish class name into kebab-case.""" | |
| 67 | + | |
| 68 | + chars: list[str] = [] | |
| 69 | + for index, char in enumerate(value): | |
| 70 | + if char.isupper() and index > 0: | |
| 71 | + chars.append("-") | |
| 72 | + chars.append(char.lower()) | |
| 73 | + return "".join(chars) | |
src/loader/runtime/public_shell.pymodified@@ -18,6 +18,7 @@ from .capabilities import CapabilityProfile, resolve_backend_capability_profile | ||
| 18 | 18 | from .dod import DefinitionOfDoneStore |
| 19 | 19 | from .events import AgentEvent, TurnSummary |
| 20 | 20 | from .launcher import build_runtime_launcher |
| 21 | +from .owner_metadata import build_runtime_owner_metadata | |
| 21 | 22 | from .permissions import PermissionConfigStatus, PermissionMode, PermissionPolicy |
| 22 | 23 | from .prompt_history import PromptSnapshot |
| 23 | 24 | from .prompting import build_system_prompt_result |
@@ -174,6 +175,8 @@ def create_runtime_session( | ||
| 174 | 175 | prompt_format: str | None, |
| 175 | 176 | prompt_sections: list[str], |
| 176 | 177 | workflow_mode: str, |
| 178 | + runtime_owner_type: str | None, | |
| 179 | + runtime_owner_path: str | None, | |
| 177 | 180 | rotate_after_bytes: int, |
| 178 | 181 | auto_compaction_input_tokens_threshold: int, |
| 179 | 182 | compaction_keep_last_messages: int, |
@@ -187,6 +190,8 @@ def create_runtime_session( | ||
| 187 | 190 | few_shot_factory=few_shot_factory, |
| 188 | 191 | project_root=project_root, |
| 189 | 192 | messages=messages or [], |
| 193 | + runtime_owner_type=runtime_owner_type, | |
| 194 | + runtime_owner_path=runtime_owner_path, | |
| 190 | 195 | permission_mode=permission_policy.active_mode.as_str(), |
| 191 | 196 | permission_prompting_enabled=permission_policy.prompting_enabled, |
| 192 | 197 | permission_rule_counts=_copy_rule_counts(permission_policy.rule_counts()), |
@@ -211,6 +216,8 @@ def create_runtime_session_install( | ||
| 211 | 216 | prompt_format: str | None, |
| 212 | 217 | prompt_sections: list[str], |
| 213 | 218 | workflow_mode: str, |
| 219 | + runtime_owner_type: str | None, | |
| 220 | + runtime_owner_path: str | None, | |
| 214 | 221 | rotate_after_bytes: int, |
| 215 | 222 | auto_compaction_input_tokens_threshold: int, |
| 216 | 223 | compaction_keep_last_messages: int, |
@@ -227,6 +234,8 @@ def create_runtime_session_install( | ||
| 227 | 234 | prompt_format=prompt_format, |
| 228 | 235 | prompt_sections=prompt_sections, |
| 229 | 236 | workflow_mode=workflow_mode, |
| 237 | + runtime_owner_type=runtime_owner_type, | |
| 238 | + runtime_owner_path=runtime_owner_path, | |
| 230 | 239 | rotate_after_bytes=rotate_after_bytes, |
| 231 | 240 | auto_compaction_input_tokens_threshold=( |
| 232 | 241 | auto_compaction_input_tokens_threshold |
@@ -262,6 +271,15 @@ def apply_runtime_session_install( | ||
| 262 | 271 | owner.prompt_sections = list(install.restored.prompt_sections) |
| 263 | 272 | owner.last_turn_summary = install.restored.last_turn_summary |
| 264 | 273 | owner._system_message = None |
| 274 | + owner_metadata = build_runtime_owner_metadata(owner) | |
| 275 | + if ( | |
| 276 | + install.session.runtime_owner_type != owner_metadata["owner_type"] | |
| 277 | + or install.session.runtime_owner_path != owner_metadata["owner_path"] | |
| 278 | + ): | |
| 279 | + install.session.update_runtime_state( | |
| 280 | + runtime_owner_type=owner_metadata["owner_type"], | |
| 281 | + runtime_owner_path=owner_metadata["owner_path"], | |
| 282 | + ) | |
| 265 | 283 | |
| 266 | 284 | |
| 267 | 285 | def build_fresh_runtime_session_install( |
@@ -272,6 +290,7 @@ def build_fresh_runtime_session_install( | ||
| 272 | 290 | ) -> RuntimeSessionInstall: |
| 273 | 291 | """Build a fresh runtime session install from the current public shell.""" |
| 274 | 292 | |
| 293 | + owner_metadata = build_runtime_owner_metadata(owner) | |
| 275 | 294 | return create_runtime_session_install( |
| 276 | 295 | project_root=owner.project_root, |
| 277 | 296 | messages=messages, |
@@ -280,6 +299,8 @@ def build_fresh_runtime_session_install( | ||
| 280 | 299 | prompt_format=owner.prompt_format, |
| 281 | 300 | prompt_sections=list(owner.prompt_sections), |
| 282 | 301 | workflow_mode=workflow_mode or owner.workflow_mode, |
| 302 | + runtime_owner_type=owner_metadata["owner_type"], | |
| 303 | + runtime_owner_path=owner_metadata["owner_path"], | |
| 283 | 304 | rotate_after_bytes=owner.config.session_rotate_after_bytes, |
| 284 | 305 | auto_compaction_input_tokens_threshold=( |
| 285 | 306 | owner.config.session_auto_compaction_input_tokens_threshold |
src/loader/runtime/session.pymodified@@ -24,11 +24,15 @@ from .completion_trace import ( | ||
| 24 | 24 | has_canonical_completion_trace, |
| 25 | 25 | normalize_completion_trace, |
| 26 | 26 | ) |
| 27 | +from .owner_metadata import ( | |
| 28 | + normalize_runtime_owner_path, | |
| 29 | + normalize_runtime_owner_type, | |
| 30 | +) | |
| 27 | 31 | from .prompt_history import PromptSnapshot, normalize_prompt_history |
| 28 | 32 | from .workflow_ledger import WorkflowLedger |
| 29 | 33 | from .workflow_policy import WorkflowTimelineEntry |
| 30 | 34 | |
| 31 | -SESSION_VERSION = 10 | |
| 35 | +SESSION_VERSION = 11 | |
| 32 | 36 | DEFAULT_ROTATE_AFTER_BYTES = 256 * 1024 |
| 33 | 37 | MAX_ROTATED_FILES = 3 |
| 34 | 38 | _UNSET = object() |
@@ -173,6 +177,8 @@ class SessionSnapshot: | ||
| 173 | 177 | usage: dict[str, int] = field(default_factory=dict) |
| 174 | 178 | active_dod_path: str | None = None |
| 175 | 179 | current_task: str | None = None |
| 180 | + runtime_owner_type: str | None = None | |
| 181 | + runtime_owner_path: str | None = None | |
| 176 | 182 | workflow_mode: str = "execute" |
| 177 | 183 | permission_mode: str = "workspace-write" |
| 178 | 184 | permission_prompting_enabled: bool = False |
@@ -211,6 +217,8 @@ class SessionSnapshot: | ||
| 211 | 217 | "usage": dict(self.usage), |
| 212 | 218 | "active_dod_path": self.active_dod_path, |
| 213 | 219 | "current_task": self.current_task, |
| 220 | + "runtime_owner_type": self.runtime_owner_type, | |
| 221 | + "runtime_owner_path": self.runtime_owner_path, | |
| 214 | 222 | "workflow_mode": self.workflow_mode, |
| 215 | 223 | "permission_mode": self.permission_mode, |
| 216 | 224 | "permission_prompting_enabled": self.permission_prompting_enabled, |
@@ -265,6 +273,15 @@ class SessionSnapshot: | ||
| 265 | 273 | usage=normalize_usage(data.get("usage")), |
| 266 | 274 | active_dod_path=data.get("active_dod_path"), |
| 267 | 275 | current_task=data.get("current_task"), |
| 276 | + runtime_owner_type=normalize_runtime_owner_type( | |
| 277 | + data.get("runtime_owner_type") | |
| 278 | + ), | |
| 279 | + runtime_owner_path=normalize_runtime_owner_path( | |
| 280 | + data.get("runtime_owner_path"), | |
| 281 | + owner_type=normalize_runtime_owner_type( | |
| 282 | + data.get("runtime_owner_type") | |
| 283 | + ), | |
| 284 | + ), | |
| 268 | 285 | workflow_mode=str(data.get("workflow_mode", "execute")), |
| 269 | 286 | permission_mode=str(data.get("permission_mode", "workspace-write")), |
| 270 | 287 | permission_prompting_enabled=bool( |
@@ -434,6 +451,8 @@ class ConversationSession: | ||
| 434 | 451 | usage_totals: dict[str, int] = field(default_factory=dict) |
| 435 | 452 | active_dod_path: str | None = None |
| 436 | 453 | current_task: str | None = None |
| 454 | + runtime_owner_type: str | None = None | |
| 455 | + runtime_owner_path: str | None = None | |
| 437 | 456 | workflow_mode: str = "execute" |
| 438 | 457 | permission_mode: str = "workspace-write" |
| 439 | 458 | permission_prompting_enabled: bool = False |
@@ -568,6 +587,8 @@ class ConversationSession: | ||
| 568 | 587 | *, |
| 569 | 588 | active_dod_path: str | None = None, |
| 570 | 589 | current_task: str | None = None, |
| 590 | + runtime_owner_type: str | None = None, | |
| 591 | + runtime_owner_path: str | None = None, | |
| 571 | 592 | workflow_mode: str | None = None, |
| 572 | 593 | permission_mode: str | None = None, |
| 573 | 594 | permission_prompting_enabled: bool | None = None, |
@@ -594,6 +615,13 @@ class ConversationSession: | ||
| 594 | 615 | self.active_dod_path = active_dod_path |
| 595 | 616 | if current_task is not None: |
| 596 | 617 | self.current_task = current_task |
| 618 | + if runtime_owner_type is not None: | |
| 619 | + self.runtime_owner_type = normalize_runtime_owner_type(runtime_owner_type) | |
| 620 | + if runtime_owner_path is not None or runtime_owner_type is not None: | |
| 621 | + self.runtime_owner_path = normalize_runtime_owner_path( | |
| 622 | + runtime_owner_path, | |
| 623 | + owner_type=self.runtime_owner_type, | |
| 624 | + ) | |
| 597 | 625 | if workflow_mode is not None: |
| 598 | 626 | self.workflow_mode = workflow_mode |
| 599 | 627 | if permission_mode is not None: |
@@ -792,6 +820,8 @@ class ConversationSession: | ||
| 792 | 820 | usage=dict(self.usage_totals), |
| 793 | 821 | active_dod_path=self.active_dod_path, |
| 794 | 822 | current_task=self.current_task, |
| 823 | + runtime_owner_type=self.runtime_owner_type, | |
| 824 | + runtime_owner_path=self.runtime_owner_path, | |
| 795 | 825 | workflow_mode=self.workflow_mode, |
| 796 | 826 | permission_mode=self.permission_mode, |
| 797 | 827 | permission_prompting_enabled=self.permission_prompting_enabled, |
@@ -855,6 +885,8 @@ class ConversationSession: | ||
| 855 | 885 | instance.usage_totals = dict(snapshot.usage) |
| 856 | 886 | instance.active_dod_path = snapshot.active_dod_path |
| 857 | 887 | instance.current_task = snapshot.current_task |
| 888 | + instance.runtime_owner_type = snapshot.runtime_owner_type | |
| 889 | + instance.runtime_owner_path = snapshot.runtime_owner_path | |
| 858 | 890 | instance.workflow_mode = snapshot.workflow_mode |
| 859 | 891 | instance.permission_mode = snapshot.permission_mode |
| 860 | 892 | instance.permission_prompting_enabled = snapshot.permission_prompting_enabled |
tests/test_runtime_bootstrap.pymodified@@ -41,7 +41,10 @@ def test_build_runtime_context_uses_shared_bootstrap_contract( | ||
| 41 | 41 | assert context.workflow_mode == agent.workflow_mode |
| 42 | 42 | assert context.prompt_format == agent.prompt_format |
| 43 | 43 | assert context.prompt_sections == agent.prompt_sections |
| 44 | - assert source.metadata == {"owner_type": "Agent"} | |
| 44 | + assert source.metadata == { | |
| 45 | + "owner_type": "Agent", | |
| 46 | + "owner_path": "public-agent", | |
| 47 | + } | |
| 45 | 48 | |
| 46 | 49 | |
| 47 | 50 | def test_sync_runtime_context_refreshes_prompt_and_capability_state( |
@@ -142,4 +145,7 @@ def test_build_runtime_launcher_wraps_shared_bootstrap_source( | ||
| 142 | 145 | assert isinstance(launcher, RuntimeLauncher) |
| 143 | 146 | assert isinstance(launcher.source, RuntimeBootstrapView) |
| 144 | 147 | assert launcher.source is not agent |
| 145 | - assert launcher.source.metadata == {"owner_type": "Agent"} | |
| 148 | + assert launcher.source.metadata == { | |
| 149 | + "owner_type": "Agent", | |
| 150 | + "owner_path": "public-agent", | |
| 151 | + } | |
tests/test_runtime_handle.pymodified@@ -30,7 +30,10 @@ def test_runtime_handle_builds_runtime_bootstrap_contract( | ||
| 30 | 30 | assert isinstance(launcher, RuntimeLauncher) |
| 31 | 31 | assert isinstance(launcher.source, RuntimeBootstrapView) |
| 32 | 32 | assert launcher.source is not handle |
| 33 | - assert launcher.source.metadata == {"owner_type": "RuntimeHandle"} | |
| 33 | + assert launcher.source.metadata == { | |
| 34 | + "owner_type": "RuntimeHandle", | |
| 35 | + "owner_path": "runtime-handle", | |
| 36 | + } | |
| 34 | 37 | assert context.project_root == temp_dir.resolve() |
| 35 | 38 | assert context.backend is handle.backend |
| 36 | 39 | assert context.registry is handle.registry |
@@ -63,7 +66,10 @@ async def test_runtime_handle_runs_conversation_runtime_without_agent( | ||
| 63 | 66 | ) |
| 64 | 67 | |
| 65 | 68 | assert summary.final_response == "Runtime handle reply." |
| 66 | - assert runtime.source.metadata == {"owner_type": "RuntimeHandle"} | |
| 69 | + assert runtime.source.metadata == { | |
| 70 | + "owner_type": "RuntimeHandle", | |
| 71 | + "owner_path": "runtime-handle", | |
| 72 | + } | |
| 67 | 73 | assert any(event.type == "response" for event in events) |
| 68 | 74 | |
| 69 | 75 | |
tests/test_runtime_launcher.pymodified@@ -28,7 +28,10 @@ def test_build_runtime_launcher_returns_launcher_for_agent_source( | ||
| 28 | 28 | assert isinstance(launcher, RuntimeLauncher) |
| 29 | 29 | assert isinstance(launcher.source, RuntimeBootstrapView) |
| 30 | 30 | assert launcher.source is not agent |
| 31 | - assert launcher.source.metadata == {"owner_type": "Agent"} | |
| 31 | + assert launcher.source.metadata == { | |
| 32 | + "owner_type": "Agent", | |
| 33 | + "owner_path": "public-agent", | |
| 34 | + } | |
| 32 | 35 | |
| 33 | 36 | |
| 34 | 37 | @pytest.mark.asyncio |
tests/test_runtime_public_shell.pymodified@@ -71,6 +71,8 @@ def test_create_runtime_session_copies_public_shell_state(temp_dir: Path) -> Non | ||
| 71 | 71 | prompt_format="native", |
| 72 | 72 | prompt_sections=["Runtime Config", "Workflow Context"], |
| 73 | 73 | workflow_mode="execute", |
| 74 | + runtime_owner_type="RuntimeHandle", | |
| 75 | + runtime_owner_path="runtime-handle", | |
| 74 | 76 | rotate_after_bytes=handle.config.session_rotate_after_bytes, |
| 75 | 77 | auto_compaction_input_tokens_threshold=( |
| 76 | 78 | handle.config.session_auto_compaction_input_tokens_threshold |
@@ -83,6 +85,8 @@ def test_create_runtime_session_copies_public_shell_state(temp_dir: Path) -> Non | ||
| 83 | 85 | assert session.permission_mode == handle.active_permission_mode |
| 84 | 86 | assert session.permission_prompting_enabled is handle.permission_policy.prompting_enabled |
| 85 | 87 | assert session.permission_rule_counts == handle.permission_policy.rule_counts() |
| 88 | + assert session.runtime_owner_type == "RuntimeHandle" | |
| 89 | + assert session.runtime_owner_path == "runtime-handle" | |
| 86 | 90 | assert session.prompt_format == "native" |
| 87 | 91 | assert session.prompt_sections == ["Runtime Config", "Workflow Context"] |
| 88 | 92 | |
@@ -374,6 +378,8 @@ def test_create_runtime_session_install_builds_restored_shell_state( | ||
| 374 | 378 | prompt_format="native", |
| 375 | 379 | prompt_sections=["Runtime Config", "Workflow Context"], |
| 376 | 380 | workflow_mode="execute", |
| 381 | + runtime_owner_type="RuntimeHandle", | |
| 382 | + runtime_owner_path="runtime-handle", | |
| 377 | 383 | rotate_after_bytes=handle.config.session_rotate_after_bytes, |
| 378 | 384 | auto_compaction_input_tokens_threshold=( |
| 379 | 385 | handle.config.session_auto_compaction_input_tokens_threshold |
@@ -402,6 +408,8 @@ def test_apply_runtime_session_install_updates_owner_shell_state( | ||
| 402 | 408 | prompt_format="native", |
| 403 | 409 | prompt_sections=["Runtime Config", "Workflow Context"], |
| 404 | 410 | workflow_mode="plan", |
| 411 | + runtime_owner_type="Agent", | |
| 412 | + runtime_owner_path="public-agent", | |
| 405 | 413 | rotate_after_bytes=handle.config.session_rotate_after_bytes, |
| 406 | 414 | auto_compaction_input_tokens_threshold=( |
| 407 | 415 | handle.config.session_auto_compaction_input_tokens_threshold |
@@ -423,6 +431,8 @@ def test_apply_runtime_session_install_updates_owner_shell_state( | ||
| 423 | 431 | assert handle.active_permission_mode == "prompt" |
| 424 | 432 | assert handle.prompt_format == "native" |
| 425 | 433 | assert handle.prompt_sections == ["Runtime Config", "Workflow Context"] |
| 434 | + assert handle.session.runtime_owner_type == "RuntimeHandle" | |
| 435 | + assert handle.session.runtime_owner_path == "runtime-handle" | |
| 426 | 436 | |
| 427 | 437 | |
| 428 | 438 | def test_build_fresh_runtime_session_install_uses_current_owner_shell_state( |
tests/test_session_state.pymodified@@ -12,6 +12,7 @@ from loader.llm.base import CompletionResponse, Message, Role, ToolCall | ||
| 12 | 12 | from loader.runtime.completion_trace import CompletionTraceEntry |
| 13 | 13 | from loader.runtime.evidence_provenance import EvidenceProvenance |
| 14 | 14 | from loader.runtime.prompt_history import PromptSnapshot |
| 15 | +from loader.runtime.runtime_handle import RuntimeHandle | |
| 15 | 16 | from loader.runtime.session import ConversationSession |
| 16 | 17 | from loader.runtime.workflow_ledger import WorkflowLedger, WorkflowLedgerItem |
| 17 | 18 | from loader.runtime.workflow_policy import WorkflowTimelineEntry |
@@ -173,6 +174,7 @@ def test_session_persists_permission_policy_metadata(temp_dir: Path) -> None: | ||
| 173 | 174 | |
| 174 | 175 | session.update_runtime_state( |
| 175 | 176 | current_task="Inspect permission history", |
| 177 | + runtime_owner_type="RuntimeHandle", | |
| 176 | 178 | permission_mode="allow", |
| 177 | 179 | permission_prompting_enabled=True, |
| 178 | 180 | permission_rule_counts={"allow": 2, "deny": 1, "ask": 4}, |
@@ -237,6 +239,8 @@ def test_session_persists_permission_policy_metadata(temp_dir: Path) -> None: | ||
| 237 | 239 | assert reloaded.permission_rules_source == str( |
| 238 | 240 | temp_dir / ".loader" / "permission-rules.json" |
| 239 | 241 | ) |
| 242 | + assert reloaded.runtime_owner_type == "RuntimeHandle" | |
| 243 | + assert reloaded.runtime_owner_path == "runtime-handle" | |
| 240 | 244 | assert reloaded.prompt_format == "native" |
| 241 | 245 | assert reloaded.prompt_sections == [ |
| 242 | 246 | "Runtime Config", |
@@ -274,6 +278,35 @@ def test_session_persists_permission_policy_metadata(temp_dir: Path) -> None: | ||
| 274 | 278 | ] |
| 275 | 279 | |
| 276 | 280 | |
| 281 | +def test_resume_session_updates_runtime_owner_metadata(temp_dir: Path) -> None: | |
| 282 | + agent = Agent( | |
| 283 | + backend=ScriptedBackend(), | |
| 284 | + config=AgentConfig(auto_context=False, stream=False), | |
| 285 | + project_root=temp_dir, | |
| 286 | + ) | |
| 287 | + agent.session.persist() | |
| 288 | + session_id = agent.session.session_id | |
| 289 | + | |
| 290 | + handle = RuntimeHandle( | |
| 291 | + backend=ScriptedBackend(), | |
| 292 | + config=AgentConfig(auto_context=False, stream=False), | |
| 293 | + project_root=temp_dir, | |
| 294 | + ) | |
| 295 | + | |
| 296 | + assert handle.resume_session(session_id) is True | |
| 297 | + | |
| 298 | + reloaded = ConversationSession.load( | |
| 299 | + project_root=temp_dir, | |
| 300 | + system_message_factory=_dummy_system, | |
| 301 | + few_shot_factory=_dummy_few_shots, | |
| 302 | + session_id=session_id, | |
| 303 | + ) | |
| 304 | + | |
| 305 | + assert reloaded is not None | |
| 306 | + assert reloaded.runtime_owner_type == "RuntimeHandle" | |
| 307 | + assert reloaded.runtime_owner_path == "runtime-handle" | |
| 308 | + | |
| 309 | + | |
| 277 | 310 | def test_session_prefers_canonical_workflow_timeline_for_completion_trace( |
| 278 | 311 | temp_dir: Path, |
| 279 | 312 | ) -> None: |