lisa-harness-kernel 0.1.0__tar.gz

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 (203) hide show
  1. lisa_harness_kernel-0.1.0/.gitignore +33 -0
  2. lisa_harness_kernel-0.1.0/.instructions.md +12 -0
  3. lisa_harness_kernel-0.1.0/PKG-INFO +22 -0
  4. lisa_harness_kernel-0.1.0/README.md +137 -0
  5. lisa_harness_kernel-0.1.0/pyproject.toml +47 -0
  6. lisa_harness_kernel-0.1.0/src/kernel/__init__.py +5 -0
  7. lisa_harness_kernel-0.1.0/src/kernel/approval.py +417 -0
  8. lisa_harness_kernel-0.1.0/src/kernel/capability.py +153 -0
  9. lisa_harness_kernel-0.1.0/src/kernel/capability_binding.py +75 -0
  10. lisa_harness_kernel-0.1.0/src/kernel/capability_manifest.py +232 -0
  11. lisa_harness_kernel-0.1.0/src/kernel/config.py +475 -0
  12. lisa_harness_kernel-0.1.0/src/kernel/content_roots.py +265 -0
  13. lisa_harness_kernel-0.1.0/src/kernel/context/__init__.py +34 -0
  14. lisa_harness_kernel-0.1.0/src/kernel/context/ambient.py +89 -0
  15. lisa_harness_kernel-0.1.0/src/kernel/context/content_registry.py +33 -0
  16. lisa_harness_kernel-0.1.0/src/kernel/context/context_budget.py +357 -0
  17. lisa_harness_kernel-0.1.0/src/kernel/context/context_mapper.py +926 -0
  18. lisa_harness_kernel-0.1.0/src/kernel/context/event_bus.py +58 -0
  19. lisa_harness_kernel-0.1.0/src/kernel/context/extension_item.py +23 -0
  20. lisa_harness_kernel-0.1.0/src/kernel/context/input_guard.py +25 -0
  21. lisa_harness_kernel-0.1.0/src/kernel/context/mcp_catalog.py +514 -0
  22. lisa_harness_kernel-0.1.0/src/kernel/context/mcp_content_registry.py +76 -0
  23. lisa_harness_kernel-0.1.0/src/kernel/context/mcp_guard.py +132 -0
  24. lisa_harness_kernel-0.1.0/src/kernel/context/model_policy.py +44 -0
  25. lisa_harness_kernel-0.1.0/src/kernel/context/models.py +161 -0
  26. lisa_harness_kernel-0.1.0/src/kernel/context/output_guard.py +25 -0
  27. lisa_harness_kernel-0.1.0/src/kernel/context/provenance.py +181 -0
  28. lisa_harness_kernel-0.1.0/src/kernel/context/request_budget.py +152 -0
  29. lisa_harness_kernel-0.1.0/src/kernel/context/sub_turn_contract.py +211 -0
  30. lisa_harness_kernel-0.1.0/src/kernel/context/tests/test_evidence_freshness_regression.py +37 -0
  31. lisa_harness_kernel-0.1.0/src/kernel/context/tests/test_pack_scoped_items.py +135 -0
  32. lisa_harness_kernel-0.1.0/src/kernel/context/tests/test_provenance.py +143 -0
  33. lisa_harness_kernel-0.1.0/src/kernel/delegation.py +26 -0
  34. lisa_harness_kernel-0.1.0/src/kernel/execution_ledger.py +118 -0
  35. lisa_harness_kernel-0.1.0/src/kernel/execution_policy.py +120 -0
  36. lisa_harness_kernel-0.1.0/src/kernel/extension_names.py +11 -0
  37. lisa_harness_kernel-0.1.0/src/kernel/extension_runtime.py +32 -0
  38. lisa_harness_kernel-0.1.0/src/kernel/function_tools.py +250 -0
  39. lisa_harness_kernel-0.1.0/src/kernel/host_command_discovery.py +63 -0
  40. lisa_harness_kernel-0.1.0/src/kernel/host_commands.py +65 -0
  41. lisa_harness_kernel-0.1.0/src/kernel/intake/__init__.py +28 -0
  42. lisa_harness_kernel-0.1.0/src/kernel/intake/connector_discovery.py +70 -0
  43. lisa_harness_kernel-0.1.0/src/kernel/intake/connectors.py +129 -0
  44. lisa_harness_kernel-0.1.0/src/kernel/intake/contracts.py +111 -0
  45. lisa_harness_kernel-0.1.0/src/kernel/ipsar.py +85 -0
  46. lisa_harness_kernel-0.1.0/src/kernel/ipsar_fsm.py +2357 -0
  47. lisa_harness_kernel-0.1.0/src/kernel/mcp_launch/__init__.py +37 -0
  48. lisa_harness_kernel-0.1.0/src/kernel/mcp_launch/compiler.py +304 -0
  49. lisa_harness_kernel-0.1.0/src/kernel/mcp_launch/policy.py +427 -0
  50. lisa_harness_kernel-0.1.0/src/kernel/mcp_launch/server_manifest.py +765 -0
  51. lisa_harness_kernel-0.1.0/src/kernel/mcp_launch/tool_filter.py +27 -0
  52. lisa_harness_kernel-0.1.0/src/kernel/model_budgets.py +233 -0
  53. lisa_harness_kernel-0.1.0/src/kernel/model_manifest.py +681 -0
  54. lisa_harness_kernel-0.1.0/src/kernel/model_router_config_discovery.py +89 -0
  55. lisa_harness_kernel-0.1.0/src/kernel/model_router_discovery.py +105 -0
  56. lisa_harness_kernel-0.1.0/src/kernel/models.py +2569 -0
  57. lisa_harness_kernel-0.1.0/src/kernel/operations/__init__.py +1 -0
  58. lisa_harness_kernel-0.1.0/src/kernel/operations/declarative.py +504 -0
  59. lisa_harness_kernel-0.1.0/src/kernel/operations/registry.py +1082 -0
  60. lisa_harness_kernel-0.1.0/src/kernel/operations/structured_output.py +381 -0
  61. lisa_harness_kernel-0.1.0/src/kernel/provider_contracts.py +446 -0
  62. lisa_harness_kernel-0.1.0/src/kernel/provider_gateway_discovery.py +87 -0
  63. lisa_harness_kernel-0.1.0/src/kernel/proxy_launch/__init__.py +20 -0
  64. lisa_harness_kernel-0.1.0/src/kernel/proxy_launch/compiler.py +213 -0
  65. lisa_harness_kernel-0.1.0/src/kernel/proxy_launch/manifest.py +283 -0
  66. lisa_harness_kernel-0.1.0/src/kernel/proxy_launch/policy.py +101 -0
  67. lisa_harness_kernel-0.1.0/src/kernel/proxy_launch/pool.py +191 -0
  68. lisa_harness_kernel-0.1.0/src/kernel/py.typed +0 -0
  69. lisa_harness_kernel-0.1.0/src/kernel/recovery.py +135 -0
  70. lisa_harness_kernel-0.1.0/src/kernel/routing_vocabulary.py +372 -0
  71. lisa_harness_kernel-0.1.0/src/kernel/sandbox_floor.py +611 -0
  72. lisa_harness_kernel-0.1.0/src/kernel/seams/__init__.py +7 -0
  73. lisa_harness_kernel-0.1.0/src/kernel/seams/host_adapter.py +196 -0
  74. lisa_harness_kernel-0.1.0/src/kernel/seams/protocols.py +466 -0
  75. lisa_harness_kernel-0.1.0/src/kernel/session_plan.py +710 -0
  76. lisa_harness_kernel-0.1.0/src/kernel/slice.py +296 -0
  77. lisa_harness_kernel-0.1.0/src/kernel/slice_provider_discovery.py +107 -0
  78. lisa_harness_kernel-0.1.0/src/kernel/state_providers.py +133 -0
  79. lisa_harness_kernel-0.1.0/src/kernel/system_segments.py +146 -0
  80. lisa_harness_kernel-0.1.0/src/kernel/telemetry.py +28 -0
  81. lisa_harness_kernel-0.1.0/src/kernel/testing/__init__.py +1 -0
  82. lisa_harness_kernel-0.1.0/src/kernel/testing/endpoint_registry.py +118 -0
  83. lisa_harness_kernel-0.1.0/src/kernel/testing/extension_harness.py +200 -0
  84. lisa_harness_kernel-0.1.0/src/kernel/testing/fixtures/prompts/sub_turn/budget_exhausted.md +3 -0
  85. lisa_harness_kernel-0.1.0/src/kernel/testing/fixtures/prompts/sub_turn/delegate_contract.md +8 -0
  86. lisa_harness_kernel-0.1.0/src/kernel/testing/fixtures/prompts/sub_turn/fork_contract.md +8 -0
  87. lisa_harness_kernel-0.1.0/src/kernel/testing/fixtures/prompts/sub_turn/midpoint_write_nudge.md +6 -0
  88. lisa_harness_kernel-0.1.0/src/kernel/testing/gateway_stub.py +145 -0
  89. lisa_harness_kernel-0.1.0/src/kernel/testing/import_boundaries.py +63 -0
  90. lisa_harness_kernel-0.1.0/src/kernel/testing/routing_vocabulary.py +45 -0
  91. lisa_harness_kernel-0.1.0/src/kernel/testing/stubs.py +22 -0
  92. lisa_harness_kernel-0.1.0/src/kernel/token_usage.py +302 -0
  93. lisa_harness_kernel-0.1.0/src/kernel/tools/__init__.py +1 -0
  94. lisa_harness_kernel-0.1.0/src/kernel/tools/mcp_client.py +2422 -0
  95. lisa_harness_kernel-0.1.0/src/kernel/tools/output_stream.py +91 -0
  96. lisa_harness_kernel-0.1.0/src/kernel/tools/registry.py +607 -0
  97. lisa_harness_kernel-0.1.0/src/kernel/tools/schema_translator.py +282 -0
  98. lisa_harness_kernel-0.1.0/src/kernel/tools/synthetic.py +113 -0
  99. lisa_harness_kernel-0.1.0/src/kernel/turn_continuation.py +118 -0
  100. lisa_harness_kernel-0.1.0/src/kernel/turn_hooks.py +388 -0
  101. lisa_harness_kernel-0.1.0/src/kernel/turn_phases/__init__.py +4 -0
  102. lisa_harness_kernel-0.1.0/src/kernel/turn_phases/_child_turn.py +442 -0
  103. lisa_harness_kernel-0.1.0/src/kernel/turn_phases/_completion_contract.py +301 -0
  104. lisa_harness_kernel-0.1.0/src/kernel/turn_phases/_dispatch.py +870 -0
  105. lisa_harness_kernel-0.1.0/src/kernel/turn_phases/_entry.py +251 -0
  106. lisa_harness_kernel-0.1.0/src/kernel/turn_phases/_fork.py +496 -0
  107. lisa_harness_kernel-0.1.0/src/kernel/turn_phases/_lifecycle.py +1040 -0
  108. lisa_harness_kernel-0.1.0/src/kernel/turn_phases/_post_turn.py +356 -0
  109. lisa_harness_kernel-0.1.0/src/kernel/turn_phases/_routing.py +282 -0
  110. lisa_harness_kernel-0.1.0/src/kernel/turn_phases/_routing_hint.py +1502 -0
  111. lisa_harness_kernel-0.1.0/src/kernel/turn_phases/_tool_schemas.py +425 -0
  112. lisa_harness_kernel-0.1.0/src/kernel/turn_phases/_turn_state.py +906 -0
  113. lisa_harness_kernel-0.1.0/src/kernel/turn_runner/__init__.py +10374 -0
  114. lisa_harness_kernel-0.1.0/src/kernel/turn_runner/_defaults.py +497 -0
  115. lisa_harness_kernel-0.1.0/src/kernel/turn_runner/_helpers.py +222 -0
  116. lisa_harness_kernel-0.1.0/src/kernel/turn_runner/_result.py +156 -0
  117. lisa_harness_kernel-0.1.0/src/kernel/turn_runner/_seams.py +109 -0
  118. lisa_harness_kernel-0.1.0/src/kernel/turn_runner/_streaming.py +66 -0
  119. lisa_harness_kernel-0.1.0/src/kernel/workflow_position.py +593 -0
  120. lisa_harness_kernel-0.1.0/src/kernel/workflow_view.py +524 -0
  121. lisa_harness_kernel-0.1.0/tests/conftest.py +336 -0
  122. lisa_harness_kernel-0.1.0/tests/conftest_catalog.py +139 -0
  123. lisa_harness_kernel-0.1.0/tests/test_approval.py +401 -0
  124. lisa_harness_kernel-0.1.0/tests/test_budget_completion_rail.py +124 -0
  125. lisa_harness_kernel-0.1.0/tests/test_capability_manifest.py +171 -0
  126. lisa_harness_kernel-0.1.0/tests/test_capability_resolution.py +318 -0
  127. lisa_harness_kernel-0.1.0/tests/test_catalog_loader.py +1013 -0
  128. lisa_harness_kernel-0.1.0/tests/test_child_turn_approval.py +43 -0
  129. lisa_harness_kernel-0.1.0/tests/test_completion_contract.py +257 -0
  130. lisa_harness_kernel-0.1.0/tests/test_config_env_prefix.py +87 -0
  131. lisa_harness_kernel-0.1.0/tests/test_connector_discovery.py +126 -0
  132. lisa_harness_kernel-0.1.0/tests/test_content_roots.py +172 -0
  133. lisa_harness_kernel-0.1.0/tests/test_context_mapper.py +582 -0
  134. lisa_harness_kernel-0.1.0/tests/test_control_channel.py +446 -0
  135. lisa_harness_kernel-0.1.0/tests/test_effect_receipts.py +2407 -0
  136. lisa_harness_kernel-0.1.0/tests/test_endpoint_registry.py +156 -0
  137. lisa_harness_kernel-0.1.0/tests/test_execution_ledger.py +71 -0
  138. lisa_harness_kernel-0.1.0/tests/test_execution_policy.py +42 -0
  139. lisa_harness_kernel-0.1.0/tests/test_extension_harness.py +94 -0
  140. lisa_harness_kernel-0.1.0/tests/test_guardrails.py +213 -0
  141. lisa_harness_kernel-0.1.0/tests/test_handoff_user_context_refresh.py +197 -0
  142. lisa_harness_kernel-0.1.0/tests/test_import_boundaries.py +57 -0
  143. lisa_harness_kernel-0.1.0/tests/test_lifecycle_resolver.py +911 -0
  144. lisa_harness_kernel-0.1.0/tests/test_mcp_adapters.py +232 -0
  145. lisa_harness_kernel-0.1.0/tests/test_mcp_bare_name_precompute.py +232 -0
  146. lisa_harness_kernel-0.1.0/tests/test_mcp_catalog.py +82 -0
  147. lisa_harness_kernel-0.1.0/tests/test_mcp_in_process.py +144 -0
  148. lisa_harness_kernel-0.1.0/tests/test_mcp_launch.py +648 -0
  149. lisa_harness_kernel-0.1.0/tests/test_mcp_warm_pool.py +266 -0
  150. lisa_harness_kernel-0.1.0/tests/test_mode_instructions.py +136 -0
  151. lisa_harness_kernel-0.1.0/tests/test_model_budgets.py +274 -0
  152. lisa_harness_kernel-0.1.0/tests/test_model_router.py +214 -0
  153. lisa_harness_kernel-0.1.0/tests/test_model_router_config_discovery.py +120 -0
  154. lisa_harness_kernel-0.1.0/tests/test_openai_function_tools.py +143 -0
  155. lisa_harness_kernel-0.1.0/tests/test_operation_model_router.py +137 -0
  156. lisa_harness_kernel-0.1.0/tests/test_operation_result_status.py +81 -0
  157. lisa_harness_kernel-0.1.0/tests/test_operation_schemaless_advertisement.py +159 -0
  158. lisa_harness_kernel-0.1.0/tests/test_operations_declarative.py +332 -0
  159. lisa_harness_kernel-0.1.0/tests/test_operations_structured_output.py +246 -0
  160. lisa_harness_kernel-0.1.0/tests/test_pack_configuration.py +208 -0
  161. lisa_harness_kernel-0.1.0/tests/test_per_step_budget.py +424 -0
  162. lisa_harness_kernel-0.1.0/tests/test_prompt_cache_key.py +23 -0
  163. lisa_harness_kernel-0.1.0/tests/test_provider_continuation.py +111 -0
  164. lisa_harness_kernel-0.1.0/tests/test_proxy_launch.py +487 -0
  165. lisa_harness_kernel-0.1.0/tests/test_proxy_policy.py +546 -0
  166. lisa_harness_kernel-0.1.0/tests/test_ratio_cap.py +214 -0
  167. lisa_harness_kernel-0.1.0/tests/test_read_loop_budget_nudge.py +44 -0
  168. lisa_harness_kernel-0.1.0/tests/test_request_context_budget.py +207 -0
  169. lisa_harness_kernel-0.1.0/tests/test_routing_vocabulary.py +214 -0
  170. lisa_harness_kernel-0.1.0/tests/test_sandbox_floor.py +336 -0
  171. lisa_harness_kernel-0.1.0/tests/test_server_manifest.py +816 -0
  172. lisa_harness_kernel-0.1.0/tests/test_session_plan.py +418 -0
  173. lisa_harness_kernel-0.1.0/tests/test_slice_provider_discovery.py +91 -0
  174. lisa_harness_kernel-0.1.0/tests/test_streaming.py +1296 -0
  175. lisa_harness_kernel-0.1.0/tests/test_synthetic_result_map.py +144 -0
  176. lisa_harness_kernel-0.1.0/tests/test_synthetic_tools.py +638 -0
  177. lisa_harness_kernel-0.1.0/tests/test_system_segments.py +108 -0
  178. lisa_harness_kernel-0.1.0/tests/test_token_usage.py +229 -0
  179. lisa_harness_kernel-0.1.0/tests/test_token_usage_publish.py +116 -0
  180. lisa_harness_kernel-0.1.0/tests/test_tool_call_escape_repair.py +208 -0
  181. lisa_harness_kernel-0.1.0/tests/test_tool_context_budget.py +23 -0
  182. lisa_harness_kernel-0.1.0/tests/test_tool_output_stream.py +193 -0
  183. lisa_harness_kernel-0.1.0/tests/test_tool_wrapper_timeout.py +473 -0
  184. lisa_harness_kernel-0.1.0/tests/test_truncated_tool_arguments.py +121 -0
  185. lisa_harness_kernel-0.1.0/tests/test_turn_completion_budget.py +281 -0
  186. lisa_harness_kernel-0.1.0/tests/test_turn_continuation.py +343 -0
  187. lisa_harness_kernel-0.1.0/tests/test_turn_decision.py +184 -0
  188. lisa_harness_kernel-0.1.0/tests/test_turn_runner.py +7731 -0
  189. lisa_harness_kernel-0.1.0/tests/test_turn_runner_approval.py +807 -0
  190. lisa_harness_kernel-0.1.0/tests/test_turn_runner_approval_handoff.py +259 -0
  191. lisa_harness_kernel-0.1.0/tests/test_turn_runner_guards_context.py +518 -0
  192. lisa_harness_kernel-0.1.0/tests/test_turn_runner_operations.py +623 -0
  193. lisa_harness_kernel-0.1.0/tests/test_turn_runner_prompt_segments.py +244 -0
  194. lisa_harness_kernel-0.1.0/tests/test_turn_runner_schema_builder.py +855 -0
  195. lisa_harness_kernel-0.1.0/tests/test_turn_runner_tool_dispatch.py +1150 -0
  196. lisa_harness_kernel-0.1.0/tests/test_turn_runner_transitions.py +1028 -0
  197. lisa_harness_kernel-0.1.0/tests/test_turn_runner_workflow_position.py +446 -0
  198. lisa_harness_kernel-0.1.0/tests/test_turn_scenario_matrix.py +331 -0
  199. lisa_harness_kernel-0.1.0/tests/test_turn_transcript.py +117 -0
  200. lisa_harness_kernel-0.1.0/tests/test_workflow_position.py +535 -0
  201. lisa_harness_kernel-0.1.0/tests/test_workflow_position_progress.py +71 -0
  202. lisa_harness_kernel-0.1.0/tests/test_workflow_view.py +567 -0
  203. lisa_harness_kernel-0.1.0/tests/turn_hooks_fold.py +132 -0
@@ -0,0 +1,33 @@
1
+ # Python
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ *.egg-info/
6
+ dist/
7
+ build/
8
+
9
+
10
+ # Virtual environments
11
+ .venv/
12
+
13
+ # IDE
14
+ .vscode/
15
+
16
+ # OS
17
+ .DS_Store
18
+ Thumbs.db
19
+
20
+ # Testing
21
+ .import_linter_cache/
22
+ .pytest_cache/
23
+ .ruff_cache/
24
+ .mypy_cache/
25
+ .lisa-tmp/
26
+ coverage/
27
+ .coverage
28
+ .coverage.*
29
+ htmlcov/
30
+
31
+ # Outputs
32
+ artifacts/
33
+ .e2e/
@@ -0,0 +1,12 @@
1
+ ---
2
+ name: Kernel Package
3
+ applyTo: "kernel/**/*.py"
4
+ description: Package-specific guidance for kernel work.
5
+ ---
6
+
7
+ # Kernel package guidance
8
+
9
+ - Keep the kernel runtime-neutral. Do not introduce host- or SDK-specific imports into kernel modules.
10
+ - Preserve the existing contract-style abstractions in kernel modules rather than falling back to ad hoc state bags.
11
+ - Follow the slice and turn-runner patterns already established in the package when changing orchestration behavior.
12
+ - Keep handoff, approval, and routing logic explicit and testable.
@@ -0,0 +1,22 @@
1
+ Metadata-Version: 2.5
2
+ Name: lisa-harness-kernel
3
+ Version: 0.1.0
4
+ Summary: Lisa kernel — runtime-neutral FSM engine, contracts, operation registry, and telemetry primitives
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: httpx>=0.27.0
7
+ Requires-Dist: mcp>=2.0.0
8
+ Requires-Dist: opentelemetry-api<2.0.0,>=1.40.0
9
+ Requires-Dist: opentelemetry-sdk<2.0.0,>=1.40.0
10
+ Requires-Dist: pydantic-settings>=2.3.0
11
+ Requires-Dist: pydantic>=2.7.0
12
+ Requires-Dist: python-dotenv>=1.0.0
13
+ Requires-Dist: pyyaml>=6.0
14
+ Requires-Dist: tzdata>=2024.1; platform_system == 'Windows'
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
17
+ Requires-Dist: pytest-cov>=6.0.0; extra == 'dev'
18
+ Requires-Dist: pytest-mock>=3.14.0; extra == 'dev'
19
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
20
+ Requires-Dist: types-pyyaml>=6.0; extra == 'dev'
21
+ Provides-Extra: memory
22
+ Requires-Dist: sqlite-vec>=0.1.9; extra == 'memory'
@@ -0,0 +1,137 @@
1
+ # Kernel
2
+
3
+ `lisa-harness-kernel` owns runtime-neutral typed contracts and the model/tool
4
+ turn loop. It does not depend on another in-repository package. Hosts inject
5
+ wiring through `TurnHooks` and `ContextSeams`; packs provide concrete
6
+ capabilities.
7
+
8
+ Start with [architecture and terminology](../docs/BOUNDARIES.md).
9
+ Most application authors should use the [shared runtime](../runtime/README.md)
10
+ rather than build a new composition root directly against kernel internals.
11
+
12
+ ## Module map
13
+
14
+ Paths below are relative to [src/kernel](src/kernel/).
15
+
16
+ | Module | Responsibility |
17
+ | --- | --- |
18
+ | [turn_runner](src/kernel/turn_runner/) | Shared completion/dispatch loop, transitions, continuations, outcomes |
19
+ | [turn_phases](src/kernel/turn_phases/) | Lifecycle, prompts/schemas, routing hints, fork/delegate and completion contracts |
20
+ | [turn_hooks](src/kernel/turn_hooks.py) | Host-injected callbacks and context seams |
21
+ | [models](src/kernel/models.py) | Execution contexts/results, modes/stages, receipts, state/evidence, budgets |
22
+ | [ipsar](src/kernel/ipsar.py) / [ipsar_fsm](src/kernel/ipsar_fsm.py) | Mode command parsing, catalogs, workflow/routing transitions |
23
+ | [session_plan](src/kernel/session_plan.py) | Strict v2 schema and deterministic compilation |
24
+ | [context](src/kernel/context/) | Evidence mapping, lanes, provenance, request fitting, public seam defaults |
25
+ | [operations](src/kernel/operations/) | Runtime/declarative operation discovery and execution |
26
+ | [tools](src/kernel/tools/) | Pack-tool registry, MCP pool, schema translation and synthetic aliases |
27
+ | [seams](src/kernel/seams/) | Host-facing protocols and adapters |
28
+ | [model_manifest](src/kernel/model_manifest.py) | Model roster validation and resolution |
29
+ | [provider_contracts](src/kernel/provider_contracts.py) | Completion/embedding contracts and provider-neutral usage |
30
+ | [execution_policy](src/kernel/execution_policy.py) / [execution_ledger](src/kernel/execution_ledger.py) | Resource bounds and consumed-work accounting |
31
+ | [approval](src/kernel/approval.py) / [turn_continuation](src/kernel/turn_continuation.py) | Exact-action decisions and saved dispatch state |
32
+ | [recovery](src/kernel/recovery.py) | Write-ahead work-item state and retry policy |
33
+ | [workflow_position](src/kernel/workflow_position.py) / [workflow_view](src/kernel/workflow_view.py) | Typed planned/ad-hoc progress and thread-derived workflow state |
34
+ | [slice](src/kernel/slice.py) | Pressure-derived opportunistic housekeeping budgets |
35
+ | [intake](src/kernel/intake/) | Signal collector and processing contracts |
36
+ | [testing](src/kernel/testing/) | Public stubs, extension harness, import-boundary support |
37
+
38
+ Provider implementations live in the SDK; the kernel defines/discovers
39
+ their gateway contracts. Content scanning lives in the SDK/catalog adapter;
40
+ the kernel consumes catalogs and imports selected implementation modules.
41
+
42
+ ## Turn contract
43
+
44
+ `run_turn` accepts a query, resolved mode/model context, budgets, hooks,
45
+ and identity. The runtime normally prepares those inputs.
46
+
47
+ 1. Guard input and establish mode/workflow context.
48
+ 2. Run configured entry/pre-turn behavior, assemble instructions/evidence,
49
+ and prepare the allowed tool schemas.
50
+ 3. Resolve each iteration's model, fit the complete request, call it, and
51
+ account for provider usage.
52
+ 4. Dispatch authorized operations, pack tools, MCP tools, or intercepted
53
+ lifecycle actions; record outcomes and effect receipts.
54
+ 5. Evaluate explicit lifecycle policy: continue, transition, fork/delegate,
55
+ ask/yield, approval, pause/terminate, or complete.
56
+ 6. Guard output and run completion/persistence/post-turn hooks as applicable.
57
+
58
+ Do not equate an answer string with successful action completion.
59
+ `TurnResult` carries blocks, handoffs, stage transitions, child outcomes,
60
+ approvals/yields, usage, receipts, and evidence needed by the host.
61
+
62
+ See [mode/workflow routing](../docs/ROUTER_DESIGN.md),
63
+ [context lifecycle](../docs/CONTEXT_LIFECYCLE.md),
64
+ [token budgets](../docs/TOKEN_BUDGET.md), and the
65
+ [invariant registry](../docs/lessons/turn-loop-invariants.md).
66
+
67
+ ## TurnHooks and ContextSeams
68
+
69
+ [TurnHooks](src/kernel/turn_hooks.py) supplies model resolution/completion,
70
+ prompt assembly, authorized operation/tool execution, catalogs/pool access,
71
+ state/services/identity, workflow updates, persistence, and slices.
72
+ Callbacks are integration seams, not permission to bypass registry policy.
73
+
74
+ [`ContextSeams`](src/kernel/turn_runner/_seams.py) groups content registry, model policy, provider gateways,
75
+ event bus, extension-item builder, and input/output guards.
76
+ Public defaults live in [kernel.context](src/kernel/context/).
77
+ Default content guards are **pass-through**. Applications needing filtering
78
+ must install appropriate implementations.
79
+
80
+ Input blocks prevent model work. Output guards can replace unsafe output;
81
+ hosts must persist/deliver the permitted result, not leak the rejected text.
82
+ Privacy also depends on authenticated identity, storage/retrieval filters,
83
+ and delivery routing; it cannot be implemented by a prompt alone.
84
+
85
+ For a custom composition, follow [host integration](../docs/HOST_INTEGRATION.md).
86
+ Keep MCP lifecycle startup/use/shutdown in one task and event loop.
87
+
88
+ ## Dispatch and operation contracts
89
+
90
+ Operations use `OperationExecutionContext` and return `OperationResult`.
91
+ They can be Python (`.yml` + `.py`) or declarative (`.operation.md`).
92
+ Pack tools use `ToolExecutionContext` / `ToolResult`.
93
+ Operations interact with the host through `ctx.host`; do not use retired
94
+ `ctx.runtime` examples.
95
+
96
+ The kernel authorizes catalog capabilities, validates effects/limits, and
97
+ uses declared tool surfaces for operation dispatch. Exposing a capability
98
+ as an ambient default does not make it a required completion effect.
99
+ Markdown operation definitions take precedence over a colocated Python
100
+ definition; the Python file can supply the response model.
101
+
102
+ External tools use the MCP pool; catalog/content access uses the content
103
+ seams. In-process Python tools remain trusted code, not sandboxed merely
104
+ because they were discovered through MCP.
105
+
106
+ Use [operation authoring](../sdk/AUTHORING.md),
107
+ [pack authoring](../docs/PACK_AUTHORING_GUIDE.md), and
108
+ [approval policy](../docs/POLICY_GUIDE.md) for metadata and examples.
109
+
110
+ ## Slices and intake
111
+
112
+ Slices are opportunistic, budgeted work. Budget grows with the elapsed gap
113
+ since the previous slice:
114
+
115
+ ```text
116
+ base_budget_ms + max_deferred_ms * (1 - 0.5^(gap_seconds / pressure_half_life_seconds))
117
+ ```
118
+
119
+ The result is capped. Policies select pre-turn, post-turn, explicit, or
120
+ disabled execution. This is not a continuously running scheduler.
121
+ Hosts can separately schedule the one-shot intake process for ambient
122
+ signal collection and processing; no cron/timer belongs inside the turn loop.
123
+
124
+ ## Testing and contribution
125
+
126
+ From the workspace root:
127
+
128
+ ```sh
129
+ uv run pytest kernel/tests kernel/src/kernel/context/tests -m "not live" -q
130
+ ```
131
+
132
+ The kernel test support is public for pack/extension authors. Use injected
133
+ fakes and isolated configuration; do not import private app modules or live
134
+ external packs into kernel unit tests.
135
+
136
+ See [testing](../docs/TESTING.md) for the full CI matrix/gates and
137
+ [contributing](../CONTRIBUTING.md).
@@ -0,0 +1,47 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [tool.hatch.build.targets.wheel]
6
+ packages = ["src/kernel"]
7
+
8
+ [project]
9
+ name = "lisa-harness-kernel"
10
+ version = "0.1.0"
11
+ description = "Lisa kernel — runtime-neutral FSM engine, contracts, operation registry, and telemetry primitives"
12
+ requires-python = ">=3.11"
13
+
14
+ dependencies = [
15
+ "pydantic>=2.7.0",
16
+ "pydantic-settings>=2.3.0",
17
+ "pyyaml>=6.0",
18
+ "opentelemetry-api>=1.40.0,<2.0.0",
19
+ "opentelemetry-sdk>=1.40.0,<2.0.0",
20
+ "httpx>=0.27.0",
21
+ "python-dotenv>=1.0.0",
22
+ "mcp>=2.0.0",
23
+ # On Windows, Python's zoneinfo has no system tz database, so the
24
+ # tzdata package is required for IANA timezone resolution.
25
+ "tzdata>=2024.1; platform_system == 'Windows'",
26
+ ]
27
+
28
+ [project.optional-dependencies]
29
+ dev = [
30
+ "pytest>=8.0.0",
31
+ "pytest-asyncio>=0.23.0",
32
+ "pytest-cov>=6.0.0",
33
+ "pytest-mock>=3.14.0",
34
+ "types-PyYAML>=6.0",
35
+ ]
36
+ memory = [
37
+ "sqlite-vec>=0.1.9",
38
+ ]
39
+
40
+ [tool.pytest.ini_options]
41
+ pythonpath = ["src"]
42
+ testpaths = ["tests"]
43
+ markers = [
44
+ "live: hits real external signal or provider APIs and prints a tester-visible summary",
45
+ "integration: multi-module in-process test; may require local services",
46
+ "eval: deterministic agent regression evals (no model calls, no I/O)",
47
+ ]
@@ -0,0 +1,5 @@
1
+ """Lisa kernel — runtime-neutral engine for conversational AI agents.
2
+
3
+ The kernel owns all contracts, abstractions, and core built-ins. It imports
4
+ nothing from host apps or higher-layer SDK packages.
5
+ """
@@ -0,0 +1,417 @@
1
+ """Exact-action approval contracts for catalog-backed effects.
2
+
3
+ The kernel owns digest construction and receipt validation. Hosts own durable
4
+ storage and approval UX through the :class:`ApprovalService` protocol.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import hashlib
11
+ import json
12
+ import uuid
13
+ from collections.abc import Mapping
14
+ from datetime import UTC, datetime, timedelta
15
+ from typing import Any, Literal, Protocol, runtime_checkable
16
+
17
+ from pydantic import BaseModel, ConfigDict, Field
18
+
19
+ __all__ = [
20
+ "ApprovalDecision",
21
+ "ApprovalIdentity",
22
+ "ApprovalInvocation",
23
+ "ApprovalReceipt",
24
+ "ApprovalRecord",
25
+ "ApprovalRequired",
26
+ "ApprovalService",
27
+ "ApprovalState",
28
+ "EffectKind",
29
+ "InMemoryApprovalService",
30
+ "build_approval_invocation",
31
+ "canonical_json",
32
+ "is_expired",
33
+ "render_approval_request",
34
+ ]
35
+
36
+ EffectKind = Literal["read", "write", "execute", "external_write"]
37
+ ApprovalState = Literal["pending", "approved", "denied", "consumed", "expired"]
38
+
39
+ _DEFAULT_APPROVAL_TTL = timedelta(minutes=30)
40
+
41
+
42
+ class _FrozenApprovalModel(BaseModel):
43
+ """Base contract for immutable approval values."""
44
+
45
+ model_config = ConfigDict(frozen=True)
46
+
47
+
48
+ class ApprovalIdentity(_FrozenApprovalModel):
49
+ """Identity and session boundaries that scope an approval receipt."""
50
+
51
+ workspace_id: str = "default"
52
+ user_id: str = ""
53
+ user_slug: str = "default"
54
+ surface_type: str = "personal_dm"
55
+ surface_id: str = ""
56
+ memory_scope: str = "personal"
57
+ mode_id: str = ""
58
+ session_plan_digest: str = ""
59
+
60
+
61
+ class ApprovalInvocation(_FrozenApprovalModel):
62
+ """A canonical effectful invocation that can be approved exactly once."""
63
+
64
+ request_id: str
65
+ target_kind: Literal["tool", "operation"]
66
+ target_id: str
67
+ pack_id: str = ""
68
+ effect: EffectKind
69
+ parameters: dict[str, Any] = Field(default_factory=dict)
70
+ identity: ApprovalIdentity
71
+ pack_config_fingerprint: str
72
+ session_plan_digest: str
73
+ action_digest: str
74
+ safe_preview: str
75
+ created_at: datetime
76
+ expires_at: datetime
77
+
78
+
79
+ class ApprovalReceipt(_FrozenApprovalModel):
80
+ """Evidence that one exact pending invocation was approved."""
81
+
82
+ request_id: str
83
+ action_digest: str
84
+ identity: ApprovalIdentity
85
+ approved_at: datetime
86
+ expires_at: datetime
87
+
88
+
89
+ class ApprovalDecision(_FrozenApprovalModel):
90
+ """An operator decision recorded against an approval request."""
91
+
92
+ request_id: str
93
+ state: ApprovalState
94
+ decided_at: datetime
95
+ identity: ApprovalIdentity
96
+ reason: str = ""
97
+
98
+
99
+ class ApprovalRecord(BaseModel):
100
+ """Durable state for one requested action, owned by a host service."""
101
+
102
+ invocation: ApprovalInvocation
103
+ state: ApprovalState = "pending"
104
+ receipt: ApprovalReceipt | None = None
105
+ decision: ApprovalDecision | None = None
106
+
107
+
108
+ class ApprovalUnavailable(RuntimeError):
109
+ """The receipt failed authoritative consumption; do not continue its saved batch."""
110
+
111
+
112
+ class ApprovalRequired(RuntimeError):
113
+ """Interrupt execution until the host has approved the exact action."""
114
+
115
+ def __init__(self, invocation: ApprovalInvocation) -> None:
116
+ self.invocation = invocation
117
+ super().__init__(f"Approval required: {invocation.request_id}")
118
+
119
+
120
+ @runtime_checkable
121
+ class ApprovalService(Protocol):
122
+ """Host-owned durable approval service used by execution registries."""
123
+
124
+ async def request(self, invocation: ApprovalInvocation) -> ApprovalInvocation:
125
+ """Persist a new pending invocation and return it."""
126
+
127
+ async def approve(
128
+ self,
129
+ request_id: str,
130
+ identity: ApprovalIdentity,
131
+ ) -> ApprovalReceipt | None:
132
+ """Approve a matching pending invocation, or return ``None``."""
133
+
134
+ async def deny(
135
+ self,
136
+ request_id: str,
137
+ identity: ApprovalIdentity,
138
+ *,
139
+ reason: str = "",
140
+ ) -> bool:
141
+ """Deny a matching pending invocation."""
142
+
143
+ async def consume(
144
+ self,
145
+ request_id: str,
146
+ invocation: ApprovalInvocation,
147
+ ) -> ApprovalReceipt | None:
148
+ """Atomically consume a matching approved invocation once."""
149
+
150
+ async def get(self, request_id: str) -> ApprovalRecord | None:
151
+ """Return the persisted request state without changing it."""
152
+
153
+ async def list_pending(self, identity: ApprovalIdentity) -> list[ApprovalRecord]:
154
+ """Return non-expired pending records owned by ``identity``.
155
+
156
+ Hosts use this to detect affirmative-resume conditions (e.g. the
157
+ user typing "proceed" while a paused action awaits approval) and
158
+ to surface pending approvals in UIs. The identity match is exact.
159
+ """
160
+
161
+
162
+ def _find_pending_for_action(
163
+ records: Mapping[str, ApprovalRecord],
164
+ invocation: ApprovalInvocation,
165
+ *,
166
+ now: datetime,
167
+ ) -> ApprovalRecord | None:
168
+ """Return a non-expired pending record matching action_digest + identity.
169
+
170
+ The exact-action contract approves one canonical invocation once. When
171
+ a caller re-issues the same effectful action (same target, parameters,
172
+ pack config fingerprint, and owner identity) before the user has
173
+ decided, the service must not mint a fresh ``request_id`` — that would
174
+ strand the original pending request and force the user to re-approve a
175
+ new id on every retry. Returning the existing pending record preserves
176
+ the id the user already saw so a prior or future ``/approve <id>`` still
177
+ satisfies it. Only the ``pending`` state is idempotent; once approved or
178
+ consumed, a genuinely new request mints a new id.
179
+ """
180
+
181
+ for record in records.values():
182
+ if record.state != "pending":
183
+ continue
184
+ if is_expired(record.invocation, now=now):
185
+ continue
186
+ if record.invocation.action_digest == invocation.action_digest and _identity_matches(
187
+ record.invocation.identity, invocation.identity
188
+ ):
189
+ return record
190
+ return None
191
+
192
+
193
+ def _canonicalize(value: Any) -> Any:
194
+ """Normalize supported JSON-like values into stable, safe digest input."""
195
+
196
+ if isinstance(value, BaseModel):
197
+ return _canonicalize(value.model_dump(mode="json"))
198
+ if isinstance(value, Mapping):
199
+ return {str(key): _canonicalize(item) for key, item in sorted(value.items())}
200
+ if isinstance(value, list | tuple):
201
+ return [_canonicalize(item) for item in value]
202
+ if isinstance(value, set | frozenset):
203
+ normalized = [_canonicalize(item) for item in value]
204
+ return sorted(normalized, key=lambda item: json.dumps(item, sort_keys=True))
205
+ return _canonicalize_scalar(value)
206
+
207
+
208
+ def _canonicalize_scalar(value: Any) -> Any:
209
+ """Normalize a scalar leaf (datetime / None / str / int / float / bool)."""
210
+ if isinstance(value, datetime):
211
+ return value.astimezone(UTC).isoformat()
212
+ if value is None or isinstance(value, str | int | float | bool):
213
+ return value
214
+ return str(value)
215
+
216
+
217
+ def canonical_json(value: Any) -> str:
218
+ """Return a deterministic JSON representation suitable for SHA-256 input."""
219
+
220
+ return json.dumps(
221
+ _canonicalize(value),
222
+ ensure_ascii=True,
223
+ sort_keys=True,
224
+ separators=(",", ":"),
225
+ )
226
+
227
+
228
+ def _digest(value: Any) -> str:
229
+ return hashlib.sha256(canonical_json(value).encode("utf-8")).hexdigest()
230
+
231
+
232
+ def build_approval_invocation(
233
+ *,
234
+ target_kind: Literal["tool", "operation"],
235
+ target_id: str,
236
+ pack_id: str,
237
+ effect: EffectKind,
238
+ parameters: Mapping[str, Any],
239
+ identity: ApprovalIdentity,
240
+ pack_config: Mapping[str, Any],
241
+ safe_preview: str = "",
242
+ now: datetime | None = None,
243
+ ttl: timedelta = _DEFAULT_APPROVAL_TTL,
244
+ ) -> ApprovalInvocation:
245
+ """Build an exact-action request from canonical validated inputs."""
246
+
247
+ created_at = now or datetime.now(UTC)
248
+ normalized_parameters = _canonicalize(parameters)
249
+ if not isinstance(normalized_parameters, dict):
250
+ normalized_parameters = {}
251
+ config_fingerprint = _digest(pack_config)
252
+ action_payload = {
253
+ "target_kind": target_kind,
254
+ "target_id": target_id,
255
+ "pack_id": pack_id,
256
+ "effect": effect,
257
+ "parameters": normalized_parameters,
258
+ "identity": identity,
259
+ "pack_config_fingerprint": config_fingerprint,
260
+ "session_plan_digest": identity.session_plan_digest,
261
+ }
262
+ return ApprovalInvocation(
263
+ request_id=uuid.uuid4().hex,
264
+ target_kind=target_kind,
265
+ target_id=target_id,
266
+ pack_id=pack_id,
267
+ effect=effect,
268
+ parameters=normalized_parameters,
269
+ identity=identity,
270
+ pack_config_fingerprint=config_fingerprint,
271
+ session_plan_digest=identity.session_plan_digest,
272
+ action_digest=_digest(action_payload),
273
+ safe_preview=safe_preview.strip() or f"{effect.replace('_', ' ')} {target_id}",
274
+ created_at=created_at,
275
+ expires_at=created_at + ttl,
276
+ )
277
+
278
+
279
+ def is_expired(invocation: ApprovalInvocation, *, now: datetime | None = None) -> bool:
280
+ """Return whether an approval request has passed its expiry time."""
281
+
282
+ return (now or datetime.now(UTC)) >= invocation.expires_at
283
+
284
+
285
+ def render_approval_request(invocation: ApprovalInvocation) -> str:
286
+ """Render the host-neutral user prompt for a paused action.
287
+
288
+ The kernel states the situation without command syntax: which action is
289
+ paused and that an operator decision is pending. Hosts own the *how* —
290
+ the runtime builds the structured ``data["approval"]`` payload (with
291
+ ``request_id``) for panel transports and appends the installed command
292
+ hints to the text for plain transports.
293
+ """
294
+
295
+ return (
296
+ f"Approval required for {invocation.safe_preview}.\n\n"
297
+ f"This action is paused until you approve or deny it "
298
+ f"(request {invocation.request_id})."
299
+ )
300
+
301
+
302
+ def _identity_matches(expected: ApprovalIdentity, actual: ApprovalIdentity) -> bool:
303
+ return expected == actual
304
+
305
+
306
+ class InMemoryApprovalService:
307
+ """Lock-protected approval service used by kernel tests and lightweight hosts."""
308
+
309
+ def __init__(self) -> None:
310
+ self._records: dict[str, ApprovalRecord] = {}
311
+ self._lock = asyncio.Lock()
312
+
313
+ async def request(self, invocation: ApprovalInvocation) -> ApprovalInvocation:
314
+ async with self._lock:
315
+ existing = _find_pending_for_action(self._records, invocation, now=datetime.now(UTC))
316
+ if existing is not None:
317
+ return existing.invocation
318
+ self._records.setdefault(invocation.request_id, ApprovalRecord(invocation=invocation))
319
+ return self._records[invocation.request_id].invocation
320
+
321
+ async def approve(
322
+ self,
323
+ request_id: str,
324
+ identity: ApprovalIdentity,
325
+ ) -> ApprovalReceipt | None:
326
+ async with self._lock:
327
+ record = self._records.get(request_id)
328
+ if record is None or not _identity_matches(record.invocation.identity, identity):
329
+ return None
330
+ if is_expired(record.invocation):
331
+ record.state = "expired"
332
+ return None
333
+ if record.state != "pending":
334
+ return None
335
+ receipt = ApprovalReceipt(
336
+ request_id=request_id,
337
+ action_digest=record.invocation.action_digest,
338
+ identity=identity,
339
+ approved_at=datetime.now(UTC),
340
+ expires_at=record.invocation.expires_at,
341
+ )
342
+ record.state = "approved"
343
+ record.receipt = receipt
344
+ record.decision = ApprovalDecision(
345
+ request_id=request_id,
346
+ state="approved",
347
+ decided_at=receipt.approved_at,
348
+ identity=identity,
349
+ )
350
+ return receipt
351
+
352
+ async def deny(
353
+ self,
354
+ request_id: str,
355
+ identity: ApprovalIdentity,
356
+ *,
357
+ reason: str = "",
358
+ ) -> bool:
359
+ async with self._lock:
360
+ record = self._records.get(request_id)
361
+ if record is None or not _identity_matches(record.invocation.identity, identity):
362
+ return False
363
+ if record.state != "pending":
364
+ return False
365
+ record.state = "denied"
366
+ record.decision = ApprovalDecision(
367
+ request_id=request_id,
368
+ state="denied",
369
+ decided_at=datetime.now(UTC),
370
+ identity=identity,
371
+ reason=reason.strip(),
372
+ )
373
+ return True
374
+
375
+ async def consume(
376
+ self,
377
+ request_id: str,
378
+ invocation: ApprovalInvocation,
379
+ ) -> ApprovalReceipt | None:
380
+ async with self._lock:
381
+ record = self._records.get(request_id)
382
+ if record is None:
383
+ return None
384
+ if is_expired(record.invocation):
385
+ record.state = "expired"
386
+ return None
387
+ if (
388
+ record.state != "approved"
389
+ or record.receipt is None
390
+ or record.receipt.action_digest != invocation.action_digest
391
+ or not _identity_matches(record.invocation.identity, invocation.identity)
392
+ ):
393
+ return None
394
+ receipt = record.receipt
395
+ record.state = "consumed"
396
+ record.decision = ApprovalDecision(
397
+ request_id=request_id,
398
+ state="consumed",
399
+ decided_at=datetime.now(UTC),
400
+ identity=invocation.identity,
401
+ )
402
+ return receipt
403
+
404
+ async def get(self, request_id: str) -> ApprovalRecord | None:
405
+ async with self._lock:
406
+ return self._records.get(request_id)
407
+
408
+ async def list_pending(self, identity: ApprovalIdentity) -> list[ApprovalRecord]:
409
+ async with self._lock:
410
+ now = datetime.now(UTC)
411
+ return [
412
+ record
413
+ for record in self._records.values()
414
+ if record.state == "pending"
415
+ and not is_expired(record.invocation, now=now)
416
+ and _identity_matches(record.invocation.identity, identity)
417
+ ]