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 | RuntimeSafeguardsProtocol, | 17 | RuntimeSafeguardsProtocol, |
| 18 | ) | 18 | ) |
| 19 | from .events import TurnSummary | 19 | from .events import TurnSummary |
| 20 | +from .owner_metadata import build_runtime_owner_metadata | ||
| 20 | from .permissions import PermissionConfigStatus, PermissionPolicy | 21 | from .permissions import PermissionConfigStatus, PermissionPolicy |
| 21 | from .reasoning_service import RuntimeReasoningService | 22 | from .reasoning_service import RuntimeReasoningService |
| 22 | from .session import ConversationSession | 23 | from .session import ConversationSession |
@@ -174,7 +175,7 @@ def build_runtime_bootstrap_source(source: RuntimeBootstrapSource | Any) -> Runt | |||
| 174 | _queue_steering_message=source.queue_steering_message, | 175 | _queue_steering_message=source.queue_steering_message, |
| 175 | _drain_steering_messages=source.drain_steering_messages, | 176 | _drain_steering_messages=source.drain_steering_messages, |
| 176 | _refresh_capability_profile=source.refresh_capability_profile, | 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 | from .dod import DefinitionOfDoneStore | 18 | from .dod import DefinitionOfDoneStore |
| 19 | from .events import AgentEvent, TurnSummary | 19 | from .events import AgentEvent, TurnSummary |
| 20 | from .launcher import build_runtime_launcher | 20 | from .launcher import build_runtime_launcher |
| 21 | +from .owner_metadata import build_runtime_owner_metadata | ||
| 21 | from .permissions import PermissionConfigStatus, PermissionMode, PermissionPolicy | 22 | from .permissions import PermissionConfigStatus, PermissionMode, PermissionPolicy |
| 22 | from .prompt_history import PromptSnapshot | 23 | from .prompt_history import PromptSnapshot |
| 23 | from .prompting import build_system_prompt_result | 24 | from .prompting import build_system_prompt_result |
@@ -174,6 +175,8 @@ def create_runtime_session( | |||
| 174 | prompt_format: str | None, | 175 | prompt_format: str | None, |
| 175 | prompt_sections: list[str], | 176 | prompt_sections: list[str], |
| 176 | workflow_mode: str, | 177 | workflow_mode: str, |
| 178 | + runtime_owner_type: str | None, | ||
| 179 | + runtime_owner_path: str | None, | ||
| 177 | rotate_after_bytes: int, | 180 | rotate_after_bytes: int, |
| 178 | auto_compaction_input_tokens_threshold: int, | 181 | auto_compaction_input_tokens_threshold: int, |
| 179 | compaction_keep_last_messages: int, | 182 | compaction_keep_last_messages: int, |
@@ -187,6 +190,8 @@ def create_runtime_session( | |||
| 187 | few_shot_factory=few_shot_factory, | 190 | few_shot_factory=few_shot_factory, |
| 188 | project_root=project_root, | 191 | project_root=project_root, |
| 189 | messages=messages or [], | 192 | messages=messages or [], |
| 193 | + runtime_owner_type=runtime_owner_type, | ||
| 194 | + runtime_owner_path=runtime_owner_path, | ||
| 190 | permission_mode=permission_policy.active_mode.as_str(), | 195 | permission_mode=permission_policy.active_mode.as_str(), |
| 191 | permission_prompting_enabled=permission_policy.prompting_enabled, | 196 | permission_prompting_enabled=permission_policy.prompting_enabled, |
| 192 | permission_rule_counts=_copy_rule_counts(permission_policy.rule_counts()), | 197 | permission_rule_counts=_copy_rule_counts(permission_policy.rule_counts()), |
@@ -211,6 +216,8 @@ def create_runtime_session_install( | |||
| 211 | prompt_format: str | None, | 216 | prompt_format: str | None, |
| 212 | prompt_sections: list[str], | 217 | prompt_sections: list[str], |
| 213 | workflow_mode: str, | 218 | workflow_mode: str, |
| 219 | + runtime_owner_type: str | None, | ||
| 220 | + runtime_owner_path: str | None, | ||
| 214 | rotate_after_bytes: int, | 221 | rotate_after_bytes: int, |
| 215 | auto_compaction_input_tokens_threshold: int, | 222 | auto_compaction_input_tokens_threshold: int, |
| 216 | compaction_keep_last_messages: int, | 223 | compaction_keep_last_messages: int, |
@@ -227,6 +234,8 @@ def create_runtime_session_install( | |||
| 227 | prompt_format=prompt_format, | 234 | prompt_format=prompt_format, |
| 228 | prompt_sections=prompt_sections, | 235 | prompt_sections=prompt_sections, |
| 229 | workflow_mode=workflow_mode, | 236 | workflow_mode=workflow_mode, |
| 237 | + runtime_owner_type=runtime_owner_type, | ||
| 238 | + runtime_owner_path=runtime_owner_path, | ||
| 230 | rotate_after_bytes=rotate_after_bytes, | 239 | rotate_after_bytes=rotate_after_bytes, |
| 231 | auto_compaction_input_tokens_threshold=( | 240 | auto_compaction_input_tokens_threshold=( |
| 232 | auto_compaction_input_tokens_threshold | 241 | auto_compaction_input_tokens_threshold |
@@ -262,6 +271,15 @@ def apply_runtime_session_install( | |||
| 262 | owner.prompt_sections = list(install.restored.prompt_sections) | 271 | owner.prompt_sections = list(install.restored.prompt_sections) |
| 263 | owner.last_turn_summary = install.restored.last_turn_summary | 272 | owner.last_turn_summary = install.restored.last_turn_summary |
| 264 | owner._system_message = None | 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 | def build_fresh_runtime_session_install( | 285 | def build_fresh_runtime_session_install( |
@@ -272,6 +290,7 @@ def build_fresh_runtime_session_install( | |||
| 272 | ) -> RuntimeSessionInstall: | 290 | ) -> RuntimeSessionInstall: |
| 273 | """Build a fresh runtime session install from the current public shell.""" | 291 | """Build a fresh runtime session install from the current public shell.""" |
| 274 | 292 | ||
| 293 | + owner_metadata = build_runtime_owner_metadata(owner) | ||
| 275 | return create_runtime_session_install( | 294 | return create_runtime_session_install( |
| 276 | project_root=owner.project_root, | 295 | project_root=owner.project_root, |
| 277 | messages=messages, | 296 | messages=messages, |
@@ -280,6 +299,8 @@ def build_fresh_runtime_session_install( | |||
| 280 | prompt_format=owner.prompt_format, | 299 | prompt_format=owner.prompt_format, |
| 281 | prompt_sections=list(owner.prompt_sections), | 300 | prompt_sections=list(owner.prompt_sections), |
| 282 | workflow_mode=workflow_mode or owner.workflow_mode, | 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 | rotate_after_bytes=owner.config.session_rotate_after_bytes, | 304 | rotate_after_bytes=owner.config.session_rotate_after_bytes, |
| 284 | auto_compaction_input_tokens_threshold=( | 305 | auto_compaction_input_tokens_threshold=( |
| 285 | owner.config.session_auto_compaction_input_tokens_threshold | 306 | owner.config.session_auto_compaction_input_tokens_threshold |
src/loader/runtime/session.pymodified@@ -24,11 +24,15 @@ from .completion_trace import ( | |||
| 24 | has_canonical_completion_trace, | 24 | has_canonical_completion_trace, |
| 25 | normalize_completion_trace, | 25 | normalize_completion_trace, |
| 26 | ) | 26 | ) |
| 27 | +from .owner_metadata import ( | ||
| 28 | + normalize_runtime_owner_path, | ||
| 29 | + normalize_runtime_owner_type, | ||
| 30 | +) | ||
| 27 | from .prompt_history import PromptSnapshot, normalize_prompt_history | 31 | from .prompt_history import PromptSnapshot, normalize_prompt_history |
| 28 | from .workflow_ledger import WorkflowLedger | 32 | from .workflow_ledger import WorkflowLedger |
| 29 | from .workflow_policy import WorkflowTimelineEntry | 33 | from .workflow_policy import WorkflowTimelineEntry |
| 30 | 34 | ||
| 31 | -SESSION_VERSION = 10 | 35 | +SESSION_VERSION = 11 |
| 32 | DEFAULT_ROTATE_AFTER_BYTES = 256 * 1024 | 36 | DEFAULT_ROTATE_AFTER_BYTES = 256 * 1024 |
| 33 | MAX_ROTATED_FILES = 3 | 37 | MAX_ROTATED_FILES = 3 |
| 34 | _UNSET = object() | 38 | _UNSET = object() |
@@ -173,6 +177,8 @@ class SessionSnapshot: | |||
| 173 | usage: dict[str, int] = field(default_factory=dict) | 177 | usage: dict[str, int] = field(default_factory=dict) |
| 174 | active_dod_path: str | None = None | 178 | active_dod_path: str | None = None |
| 175 | current_task: str | None = None | 179 | current_task: str | None = None |
| 180 | + runtime_owner_type: str | None = None | ||
| 181 | + runtime_owner_path: str | None = None | ||
| 176 | workflow_mode: str = "execute" | 182 | workflow_mode: str = "execute" |
| 177 | permission_mode: str = "workspace-write" | 183 | permission_mode: str = "workspace-write" |
| 178 | permission_prompting_enabled: bool = False | 184 | permission_prompting_enabled: bool = False |
@@ -211,6 +217,8 @@ class SessionSnapshot: | |||
| 211 | "usage": dict(self.usage), | 217 | "usage": dict(self.usage), |
| 212 | "active_dod_path": self.active_dod_path, | 218 | "active_dod_path": self.active_dod_path, |
| 213 | "current_task": self.current_task, | 219 | "current_task": self.current_task, |
| 220 | + "runtime_owner_type": self.runtime_owner_type, | ||
| 221 | + "runtime_owner_path": self.runtime_owner_path, | ||
| 214 | "workflow_mode": self.workflow_mode, | 222 | "workflow_mode": self.workflow_mode, |
| 215 | "permission_mode": self.permission_mode, | 223 | "permission_mode": self.permission_mode, |
| 216 | "permission_prompting_enabled": self.permission_prompting_enabled, | 224 | "permission_prompting_enabled": self.permission_prompting_enabled, |
@@ -265,6 +273,15 @@ class SessionSnapshot: | |||
| 265 | usage=normalize_usage(data.get("usage")), | 273 | usage=normalize_usage(data.get("usage")), |
| 266 | active_dod_path=data.get("active_dod_path"), | 274 | active_dod_path=data.get("active_dod_path"), |
| 267 | current_task=data.get("current_task"), | 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 | workflow_mode=str(data.get("workflow_mode", "execute")), | 285 | workflow_mode=str(data.get("workflow_mode", "execute")), |
| 269 | permission_mode=str(data.get("permission_mode", "workspace-write")), | 286 | permission_mode=str(data.get("permission_mode", "workspace-write")), |
| 270 | permission_prompting_enabled=bool( | 287 | permission_prompting_enabled=bool( |
@@ -434,6 +451,8 @@ class ConversationSession: | |||
| 434 | usage_totals: dict[str, int] = field(default_factory=dict) | 451 | usage_totals: dict[str, int] = field(default_factory=dict) |
| 435 | active_dod_path: str | None = None | 452 | active_dod_path: str | None = None |
| 436 | current_task: str | None = None | 453 | current_task: str | None = None |
| 454 | + runtime_owner_type: str | None = None | ||
| 455 | + runtime_owner_path: str | None = None | ||
| 437 | workflow_mode: str = "execute" | 456 | workflow_mode: str = "execute" |
| 438 | permission_mode: str = "workspace-write" | 457 | permission_mode: str = "workspace-write" |
| 439 | permission_prompting_enabled: bool = False | 458 | permission_prompting_enabled: bool = False |
@@ -568,6 +587,8 @@ class ConversationSession: | |||
| 568 | *, | 587 | *, |
| 569 | active_dod_path: str | None = None, | 588 | active_dod_path: str | None = None, |
| 570 | current_task: str | None = None, | 589 | current_task: str | None = None, |
| 590 | + runtime_owner_type: str | None = None, | ||
| 591 | + runtime_owner_path: str | None = None, | ||
| 571 | workflow_mode: str | None = None, | 592 | workflow_mode: str | None = None, |
| 572 | permission_mode: str | None = None, | 593 | permission_mode: str | None = None, |
| 573 | permission_prompting_enabled: bool | None = None, | 594 | permission_prompting_enabled: bool | None = None, |
@@ -594,6 +615,13 @@ class ConversationSession: | |||
| 594 | self.active_dod_path = active_dod_path | 615 | self.active_dod_path = active_dod_path |
| 595 | if current_task is not None: | 616 | if current_task is not None: |
| 596 | self.current_task = current_task | 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 | if workflow_mode is not None: | 625 | if workflow_mode is not None: |
| 598 | self.workflow_mode = workflow_mode | 626 | self.workflow_mode = workflow_mode |
| 599 | if permission_mode is not None: | 627 | if permission_mode is not None: |
@@ -792,6 +820,8 @@ class ConversationSession: | |||
| 792 | usage=dict(self.usage_totals), | 820 | usage=dict(self.usage_totals), |
| 793 | active_dod_path=self.active_dod_path, | 821 | active_dod_path=self.active_dod_path, |
| 794 | current_task=self.current_task, | 822 | current_task=self.current_task, |
| 823 | + runtime_owner_type=self.runtime_owner_type, | ||
| 824 | + runtime_owner_path=self.runtime_owner_path, | ||
| 795 | workflow_mode=self.workflow_mode, | 825 | workflow_mode=self.workflow_mode, |
| 796 | permission_mode=self.permission_mode, | 826 | permission_mode=self.permission_mode, |
| 797 | permission_prompting_enabled=self.permission_prompting_enabled, | 827 | permission_prompting_enabled=self.permission_prompting_enabled, |
@@ -855,6 +885,8 @@ class ConversationSession: | |||
| 855 | instance.usage_totals = dict(snapshot.usage) | 885 | instance.usage_totals = dict(snapshot.usage) |
| 856 | instance.active_dod_path = snapshot.active_dod_path | 886 | instance.active_dod_path = snapshot.active_dod_path |
| 857 | instance.current_task = snapshot.current_task | 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 | instance.workflow_mode = snapshot.workflow_mode | 890 | instance.workflow_mode = snapshot.workflow_mode |
| 859 | instance.permission_mode = snapshot.permission_mode | 891 | instance.permission_mode = snapshot.permission_mode |
| 860 | instance.permission_prompting_enabled = snapshot.permission_prompting_enabled | 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 | assert context.workflow_mode == agent.workflow_mode | 41 | assert context.workflow_mode == agent.workflow_mode |
| 42 | assert context.prompt_format == agent.prompt_format | 42 | assert context.prompt_format == agent.prompt_format |
| 43 | assert context.prompt_sections == agent.prompt_sections | 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 | def test_sync_runtime_context_refreshes_prompt_and_capability_state( | 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 | assert isinstance(launcher, RuntimeLauncher) | 145 | assert isinstance(launcher, RuntimeLauncher) |
| 143 | assert isinstance(launcher.source, RuntimeBootstrapView) | 146 | assert isinstance(launcher.source, RuntimeBootstrapView) |
| 144 | assert launcher.source is not agent | 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 | assert isinstance(launcher, RuntimeLauncher) | 30 | assert isinstance(launcher, RuntimeLauncher) |
| 31 | assert isinstance(launcher.source, RuntimeBootstrapView) | 31 | assert isinstance(launcher.source, RuntimeBootstrapView) |
| 32 | assert launcher.source is not handle | 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 | assert context.project_root == temp_dir.resolve() | 37 | assert context.project_root == temp_dir.resolve() |
| 35 | assert context.backend is handle.backend | 38 | assert context.backend is handle.backend |
| 36 | assert context.registry is handle.registry | 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 | assert summary.final_response == "Runtime handle reply." | 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 | assert any(event.type == "response" for event in events) | 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 | assert isinstance(launcher, RuntimeLauncher) | 28 | assert isinstance(launcher, RuntimeLauncher) |
| 29 | assert isinstance(launcher.source, RuntimeBootstrapView) | 29 | assert isinstance(launcher.source, RuntimeBootstrapView) |
| 30 | assert launcher.source is not agent | 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 | @pytest.mark.asyncio | 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 | prompt_format="native", | 71 | prompt_format="native", |
| 72 | prompt_sections=["Runtime Config", "Workflow Context"], | 72 | prompt_sections=["Runtime Config", "Workflow Context"], |
| 73 | workflow_mode="execute", | 73 | workflow_mode="execute", |
| 74 | + runtime_owner_type="RuntimeHandle", | ||
| 75 | + runtime_owner_path="runtime-handle", | ||
| 74 | rotate_after_bytes=handle.config.session_rotate_after_bytes, | 76 | rotate_after_bytes=handle.config.session_rotate_after_bytes, |
| 75 | auto_compaction_input_tokens_threshold=( | 77 | auto_compaction_input_tokens_threshold=( |
| 76 | handle.config.session_auto_compaction_input_tokens_threshold | 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 | assert session.permission_mode == handle.active_permission_mode | 85 | assert session.permission_mode == handle.active_permission_mode |
| 84 | assert session.permission_prompting_enabled is handle.permission_policy.prompting_enabled | 86 | assert session.permission_prompting_enabled is handle.permission_policy.prompting_enabled |
| 85 | assert session.permission_rule_counts == handle.permission_policy.rule_counts() | 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 | assert session.prompt_format == "native" | 90 | assert session.prompt_format == "native" |
| 87 | assert session.prompt_sections == ["Runtime Config", "Workflow Context"] | 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 | prompt_format="native", | 378 | prompt_format="native", |
| 375 | prompt_sections=["Runtime Config", "Workflow Context"], | 379 | prompt_sections=["Runtime Config", "Workflow Context"], |
| 376 | workflow_mode="execute", | 380 | workflow_mode="execute", |
| 381 | + runtime_owner_type="RuntimeHandle", | ||
| 382 | + runtime_owner_path="runtime-handle", | ||
| 377 | rotate_after_bytes=handle.config.session_rotate_after_bytes, | 383 | rotate_after_bytes=handle.config.session_rotate_after_bytes, |
| 378 | auto_compaction_input_tokens_threshold=( | 384 | auto_compaction_input_tokens_threshold=( |
| 379 | handle.config.session_auto_compaction_input_tokens_threshold | 385 | handle.config.session_auto_compaction_input_tokens_threshold |
@@ -402,6 +408,8 @@ def test_apply_runtime_session_install_updates_owner_shell_state( | |||
| 402 | prompt_format="native", | 408 | prompt_format="native", |
| 403 | prompt_sections=["Runtime Config", "Workflow Context"], | 409 | prompt_sections=["Runtime Config", "Workflow Context"], |
| 404 | workflow_mode="plan", | 410 | workflow_mode="plan", |
| 411 | + runtime_owner_type="Agent", | ||
| 412 | + runtime_owner_path="public-agent", | ||
| 405 | rotate_after_bytes=handle.config.session_rotate_after_bytes, | 413 | rotate_after_bytes=handle.config.session_rotate_after_bytes, |
| 406 | auto_compaction_input_tokens_threshold=( | 414 | auto_compaction_input_tokens_threshold=( |
| 407 | handle.config.session_auto_compaction_input_tokens_threshold | 415 | handle.config.session_auto_compaction_input_tokens_threshold |
@@ -423,6 +431,8 @@ def test_apply_runtime_session_install_updates_owner_shell_state( | |||
| 423 | assert handle.active_permission_mode == "prompt" | 431 | assert handle.active_permission_mode == "prompt" |
| 424 | assert handle.prompt_format == "native" | 432 | assert handle.prompt_format == "native" |
| 425 | assert handle.prompt_sections == ["Runtime Config", "Workflow Context"] | 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 | def test_build_fresh_runtime_session_install_uses_current_owner_shell_state( | 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 | from loader.runtime.completion_trace import CompletionTraceEntry | 12 | from loader.runtime.completion_trace import CompletionTraceEntry |
| 13 | from loader.runtime.evidence_provenance import EvidenceProvenance | 13 | from loader.runtime.evidence_provenance import EvidenceProvenance |
| 14 | from loader.runtime.prompt_history import PromptSnapshot | 14 | from loader.runtime.prompt_history import PromptSnapshot |
| 15 | +from loader.runtime.runtime_handle import RuntimeHandle | ||
| 15 | from loader.runtime.session import ConversationSession | 16 | from loader.runtime.session import ConversationSession |
| 16 | from loader.runtime.workflow_ledger import WorkflowLedger, WorkflowLedgerItem | 17 | from loader.runtime.workflow_ledger import WorkflowLedger, WorkflowLedgerItem |
| 17 | from loader.runtime.workflow_policy import WorkflowTimelineEntry | 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 | session.update_runtime_state( | 175 | session.update_runtime_state( |
| 175 | current_task="Inspect permission history", | 176 | current_task="Inspect permission history", |
| 177 | + runtime_owner_type="RuntimeHandle", | ||
| 176 | permission_mode="allow", | 178 | permission_mode="allow", |
| 177 | permission_prompting_enabled=True, | 179 | permission_prompting_enabled=True, |
| 178 | permission_rule_counts={"allow": 2, "deny": 1, "ask": 4}, | 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 | assert reloaded.permission_rules_source == str( | 239 | assert reloaded.permission_rules_source == str( |
| 238 | temp_dir / ".loader" / "permission-rules.json" | 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 | assert reloaded.prompt_format == "native" | 244 | assert reloaded.prompt_format == "native" |
| 241 | assert reloaded.prompt_sections == [ | 245 | assert reloaded.prompt_sections == [ |
| 242 | "Runtime Config", | 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 | def test_session_prefers_canonical_workflow_timeline_for_completion_trace( | 310 | def test_session_prefers_canonical_workflow_timeline_for_completion_trace( |
| 278 | temp_dir: Path, | 311 | temp_dir: Path, |
| 279 | ) -> None: | 312 | ) -> None: |