clm-kernel 0.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- clm/__init__.py +230 -0
- clm/__main__.py +25 -0
- clm/cross_cutting/__init__.py +50 -0
- clm/cross_cutting/_test_runner.py +36 -0
- clm/cross_cutting/async_utils.py +25 -0
- clm/cross_cutting/cel_builder.py +123 -0
- clm/cross_cutting/cel_converter.py +697 -0
- clm/cross_cutting/cel_engine.py +252 -0
- clm/cross_cutting/cel_ops.py +72 -0
- clm/cross_cutting/cel_ops_db.py +157 -0
- clm/cross_cutting/cel_ops_files.py +99 -0
- clm/cross_cutting/cel_ops_state.py +71 -0
- clm/cross_cutting/cel_ops_strings.py +64 -0
- clm/cross_cutting/cel_resolver.py +63 -0
- clm/cross_cutting/cel_slices.py +56 -0
- clm/cross_cutting/cel_store.py +330 -0
- clm/cross_cutting/clm_logger.py +140 -0
- clm/cross_cutting/config/__init__.py +155 -0
- clm/cross_cutting/config/config_constants.py +392 -0
- clm/cross_cutting/config/env_parameters.py +90 -0
- clm/cross_cutting/config/logging.py +378 -0
- clm/cross_cutting/config/settings.py +504 -0
- clm/cross_cutting/context.py +5 -0
- clm/cross_cutting/domain_models.py +55 -0
- clm/cross_cutting/domain_types.py +61 -0
- clm/cross_cutting/effects.py +67 -0
- clm/cross_cutting/errors.py +153 -0
- clm/cross_cutting/governance.py +331 -0
- clm/cross_cutting/guards.py +41 -0
- clm/cross_cutting/mcard.py +321 -0
- clm/cross_cutting/native.py +185 -0
- clm/cross_cutting/observability.py +289 -0
- clm/cross_cutting/pcard.py +434 -0
- clm/cross_cutting/registry.py +71 -0
- clm/cross_cutting/resources.py +84 -0
- clm/cross_cutting/secrets.py +44 -0
- clm/cross_cutting/subprocess_utils.py +118 -0
- clm/cross_cutting/telemetry.py +164 -0
- clm/cross_cutting/testing/__init__.py +8 -0
- clm/cross_cutting/testing/comparator.py +187 -0
- clm/cross_cutting/testing/datasets.py +87 -0
- clm/cross_cutting/timeutil.py +20 -0
- clm/cross_cutting/types/new_type_check.py +0 -0
- clm/cross_cutting/utils/__init__.py +1 -0
- clm/cross_cutting/utils/url_safety.py +50 -0
- clm/cross_cutting/vcard.py +560 -0
- clm/cross_cutting/writer.py +118 -0
- clm/layer0/README.md +50 -0
- clm/layer0/__init__.py +223 -0
- clm/layer0/algebra.py +60 -0
- clm/layer0/algebra_baldwin.py +133 -0
- clm/layer0/algebra_baldwin_generators.py +36 -0
- clm/layer0/algebra_equivalence.py +262 -0
- clm/layer0/algebra_ingest.py +306 -0
- clm/layer0/algebra_manifest_io.py +156 -0
- clm/layer0/assembler.py +481 -0
- clm/layer0/cel/__init__.py +23 -0
- clm/layer0/cel_eval.py +491 -0
- clm/layer0/codec.py +106 -0
- clm/layer0/context.py +122 -0
- clm/layer0/db.py +160 -0
- clm/layer0/exceptions.py +73 -0
- clm/layer0/fibration.py +106 -0
- clm/layer0/gates.py +50 -0
- clm/layer0/hash.py +80 -0
- clm/layer0/loader.py +826 -0
- clm/layer0/math_utils.py +163 -0
- clm/layer0/mcard.py +96 -0
- clm/layer0/model/__init__.py +0 -0
- clm/layer0/model/action.py +223 -0
- clm/layer0/model/card.py +280 -0
- clm/layer0/model/card_triad.py +427 -0
- clm/layer0/model/compatibility.py +60 -0
- clm/layer0/model/dictionary.py +46 -0
- clm/layer0/model/dots.py +331 -0
- clm/layer0/model/event_producer.py +88 -0
- clm/layer0/model/g_time.py +89 -0
- clm/layer0/model/handle.py +163 -0
- clm/layer0/model/hash/__init__.py +6 -0
- clm/layer0/model/hash/algorithms/__init__.py +3 -0
- clm/layer0/model/hash/algorithms/custom_hash.py +7 -0
- clm/layer0/model/hash/algorithms/local_sha256.py +178 -0
- clm/layer0/model/hash/constants.py +53 -0
- clm/layer0/model/hash/enums.py +60 -0
- clm/layer0/model/hash/validator.py +307 -0
- clm/layer0/model/interpreter.py +275 -0
- clm/layer0/model/pagination.py +45 -0
- clm/layer0/model/pcard.py +716 -0
- clm/layer0/model/schema.py +44 -0
- clm/layer0/model/utils/__init__.py +1 -0
- clm/layer0/model/utils/content_analyzer.py +152 -0
- clm/layer0/model/validators/__init__.py +1 -0
- clm/layer0/model/validators/base_validator.py +44 -0
- clm/layer0/model/validators/binary_validator.py +66 -0
- clm/layer0/model/validators/text_validator.py +82 -0
- clm/layer0/model/validators/validation_registry.py +49 -0
- clm/layer0/model/vcard.py +823 -0
- clm/layer0/model/vcard_ext/__init__.py +39 -0
- clm/layer0/model/vcard_ext/core.py +60 -0
- clm/layer0/model/vcard_ext/network.py +23 -0
- clm/layer0/model/vcard_ext/observability.py +63 -0
- clm/layer0/model/vcard_ext/storage.py +60 -0
- clm/layer0/model/vcard_ext/vendors.py +211 -0
- clm/layer0/model/vcard_sandwich.py +119 -0
- clm/layer0/model/vcard_vocabulary.py +463 -0
- clm/layer0/model/workflow.py +274 -0
- clm/layer0/narrative.py +431 -0
- clm/layer0/ontology/collection.py +125 -0
- clm/layer0/parser.py +429 -0
- clm/layer0/relations.py +73 -0
- clm/layer0/rule_evaluator.py +251 -0
- clm/layer0/schema/__init__.py +38 -0
- clm/layer0/schema/ddl.py +97 -0
- clm/layer0/templates/abstract.yaml +99 -0
- clm/layer0/templates/balanced.yaml +253 -0
- clm/layer0/templates/concrete.yaml +172 -0
- clm/layer0/type_registry.py +223 -0
- clm/layer0/types.py +204 -0
- clm/layer0/utils.py +886 -0
- clm/layer0/verifier_core.py +262 -0
- clm/layer1/__init__.py +31 -0
- clm/layer1/action_dispatcher.py +275 -0
- clm/layer1/adapter_registry.py +233 -0
- clm/layer1/algebra_baldwin_generators.py +192 -0
- clm/layer1/arrow_compiler.py +525 -0
- clm/layer1/batch_processor.py +152 -0
- clm/layer1/cel/__init__.py +27 -0
- clm/layer1/cel/builder.py +173 -0
- clm/layer1/engine.py +759 -0
- clm/layer1/engine_async.py +89 -0
- clm/layer1/eval_callback.py +49 -0
- clm/layer1/gate_composer.py +229 -0
- clm/layer1/hoare_sandwich.py +298 -0
- clm/layer1/identity_guard.py +348 -0
- clm/layer1/matrix.py +138 -0
- clm/layer1/neural_runtime.py +221 -0
- clm/layer1/operations/__init__.py +123 -0
- clm/layer1/operations/augment.py +156 -0
- clm/layer1/operations/base.py +109 -0
- clm/layer1/operations/builtins.py +338 -0
- clm/layer1/operations/dispatch.py +144 -0
- clm/layer1/operations/exclude.py +107 -0
- clm/layer1/operations/handle.py +197 -0
- clm/layer1/operations/identity.py +139 -0
- clm/layer1/operations/invert.py +66 -0
- clm/layer1/operations/loader.py +116 -0
- clm/layer1/operations/manipulators/__init__.py +23 -0
- clm/layer1/operations/manipulators/document.py +211 -0
- clm/layer1/operations/manipulators/media.py +134 -0
- clm/layer1/operations/manipulators/structured.py +159 -0
- clm/layer1/operations/manipulators/tabular.py +334 -0
- clm/layer1/operations/port.py +65 -0
- clm/layer1/operations/services.py +529 -0
- clm/layer1/operations/split.py +117 -0
- clm/layer1/operations/substitute.py +75 -0
- clm/layer1/petri.py +1052 -0
- clm/layer1/pocketflow_orchestrator.py +408 -0
- clm/layer1/pocketflow_scheduler.py +519 -0
- clm/layer1/protocols.py +197 -0
- clm/layer1/runtime_adapter.py +718 -0
- clm/layer1/runtimes/__init__.py +71 -0
- clm/layer1/runtimes/_generated_assertions.py +125 -0
- clm/layer1/runtimes/_generated_types.py +42 -0
- clm/layer1/runtimes/base.py +265 -0
- clm/layer1/runtimes/binary.py +101 -0
- clm/layer1/runtimes/factory.py +203 -0
- clm/layer1/runtimes/javascript.py +321 -0
- clm/layer1/runtimes/javascript_runtime.js +33 -0
- clm/layer1/runtimes/js_scripts/isomorphism.mjs +22 -0
- clm/layer1/runtimes/js_scripts/loader.mjs +52 -0
- clm/layer1/runtimes/lambda_calc.py +126 -0
- clm/layer1/runtimes/python.py +288 -0
- clm/layer1/runtimes/python_runtime.py +41 -0
- clm/layer1/runtimes/script.py +114 -0
- clm/layer1/sandbox.py +334 -0
- clm/layer1/savepoint_guard.py +136 -0
- clm/layer1/sparse_net.py +122 -0
- clm/layer1/spec_tester.py +165 -0
- clm/layer1/store.py +295 -0
- clm/layer1/test_runner.py +498 -0
- clm/layer1/type_inspector.py +166 -0
- clm/layer1/verifier.py +772 -0
- clm/layer1/vm.py +91 -0
- clm/layer2/__init__.py +78 -0
- clm/layer2/card_collection.py +678 -0
- clm/layer2/classifier.py +479 -0
- clm/layer2/engine/__init__.py +1 -0
- clm/layer2/engine/abstract_sql_engine.py +320 -0
- clm/layer2/engine/base.py +193 -0
- clm/layer2/engine/duckdb_engine.py +576 -0
- clm/layer2/engine/sqlite_engine.py +518 -0
- clm/layer2/fiber/__init__.py +31 -0
- clm/layer2/fiber/context.py +89 -0
- clm/layer2/fiber/disposable.py +163 -0
- clm/layer2/fiber/fiber.py +125 -0
- clm/layer2/fiber/hoare.py +191 -0
- clm/layer2/file_io.py +376 -0
- clm/layer2/file_registrar.py +77 -0
- clm/layer2/ingest.py +202 -0
- clm/layer2/lineage.py +322 -0
- clm/layer2/mcard_fs.py +262 -0
- clm/layer2/merkle.py +11 -0
- clm/layer2/mime.py +244 -0
- clm/layer2/mime_detector.py +245 -0
- clm/layer2/private_collection.py +118 -0
- clm/layer2/shared_store.py +11 -0
- clm/layer2/storage/__init__.py +190 -0
- clm/layer2/storage/db_bitemporal.py +141 -0
- clm/layer2/storage/db_driver.py +331 -0
- clm/layer2/storage/db_hyperlinks.py +214 -0
- clm/layer2/storage/db_pool.py +139 -0
- clm/layer2/storage/db_repository.py +229 -0
- clm/layer2/storage/savepoint.py +78 -0
- clm/layer2/storage/sqlite_wal.py +74 -0
- clm/layer2/tridb.py +245 -0
- clm/layer2/vcard_receipt.py +71 -0
- clm/layer2/vfs.py +17 -0
- clm/layer3/__init__.py +141 -0
- clm/layer3/agency/__init__.py +19 -0
- clm/layer3/agency/config.py +220 -0
- clm/layer3/agency/providers/__init__.py +15 -0
- clm/layer3/agency/providers/base.py +91 -0
- clm/layer3/agency/providers/mlc_llm.py +169 -0
- clm/layer3/agency/providers/ollama.py +203 -0
- clm/layer3/agency/router.py +71 -0
- clm/layer3/agency/runtime.py +524 -0
- clm/layer3/gateway.py +353 -0
- clm/layer3/open_interpreter_adapter.py +159 -0
- clm/layer3/rag/__init__.py +70 -0
- clm/layer3/rag/cli.py +228 -0
- clm/layer3/rag/config.py +153 -0
- clm/layer3/rag/embeddings/__init__.py +16 -0
- clm/layer3/rag/embeddings/base.py +79 -0
- clm/layer3/rag/embeddings/ollama.py +211 -0
- clm/layer3/rag/embeddings/vision.py +290 -0
- clm/layer3/rag/engine.py +276 -0
- clm/layer3/rag/graph/__init__.py +22 -0
- clm/layer3/rag/graph/community.py +198 -0
- clm/layer3/rag/graph/engine.py +433 -0
- clm/layer3/rag/graph/extractor.py +255 -0
- clm/layer3/rag/graph/schema.py +115 -0
- clm/layer3/rag/graph/store.py +641 -0
- clm/layer3/rag/indexer.py +333 -0
- clm/layer3/rag/llm_providers.json +52 -0
- clm/layer3/rag/semantic_versioning.py +313 -0
- clm/layer3/rag/vector/__init__.py +28 -0
- clm/layer3/rag/vector/handle_vector_store.py +749 -0
- clm/layer3/rag/vector/schema.py +151 -0
- clm/layer3/rag/vector/store.py +632 -0
- clm/layer3/reticulum/__init__.py +55 -0
- clm/layer3/reticulum/bridge.py +74 -0
- clm/layer3/reticulum/daemon.py +88 -0
- clm/layer3/reticulum/discovery.py +164 -0
- clm/layer3/reticulum/identity.py +107 -0
- clm/layer3/reticulum/media.py +148 -0
- clm/layer3/reticulum/transport.py +133 -0
- clm/layer3/satori/__init__.py +209 -0
- clm/layer3/satori/alpha_conversion.py +168 -0
- clm/layer3/satori/beta_reduction.py +359 -0
- clm/layer3/satori/continuation.py +38 -0
- clm/layer3/satori/eta_conversion.py +205 -0
- clm/layer3/satori/free_variables.py +163 -0
- clm/layer3/satori/io_effects.py +370 -0
- clm/layer3/satori/lambda_runtime.py +726 -0
- clm/layer3/satori/lambda_term.py +230 -0
- clm/layer3/satori/sheaf_audit.py +261 -0
- clm/layer3/satori/speech_act.py +279 -0
- clm/layer4/README.md +9 -0
- clm/layer4/__init__.py +67 -0
- clm/layer4/_sovereign_executor.py +580 -0
- clm/layer4/action_commands.py +719 -0
- clm/layer4/api.py +236 -0
- clm/layer4/bootstrap/__init__.py +13 -0
- clm/layer4/bootstrap/bootstrapper.py +118 -0
- clm/layer4/bootstrap/cleanup.py +44 -0
- clm/layer4/bootstrap/clm_hooks.py +49 -0
- clm/layer4/bootstrap/genesis.py +77 -0
- clm/layer4/bootstrap/injection.py +125 -0
- clm/layer4/cli.py +549 -0
- clm/layer4/collection_manager.py +553 -0
- clm/layer4/commands/__init__.py +81 -0
- clm/layer4/commands/action_cmd.py +42 -0
- clm/layer4/commands/check_cmd.py +82 -0
- clm/layer4/commands/envelope.py +73 -0
- clm/layer4/commands/evaluate_cmd.py +224 -0
- clm/layer4/commands/mcard_cmd.py +24 -0
- clm/layer4/commands/petri_cmd.py +92 -0
- clm/layer4/commands/tridb_cmd.py +319 -0
- clm/layer4/commands/version_cmd.py +27 -0
- clm/layer4/diff_commands.py +167 -0
- clm/layer4/eoa_sponsor.py +232 -0
- clm/layer4/events.py +69 -0
- clm/layer4/flux_gateway.py +133 -0
- clm/layer4/gateway.py +91 -0
- clm/layer4/loader.py +307 -0
- clm/layer4/mcard_commands.py +166 -0
- clm/layer4/membrane.py +273 -0
- clm/layer4/navigation.py +50 -0
- clm/layer4/protocol/protocol_entry_python.py +108 -0
- clm/layer4/protocol/python_sync.py +126 -0
- clm/layer4/runner.py +150 -0
- clm/layer4/satori.py +262 -0
- clm/layer4/server/python_sync_client.py +150 -0
- clm/layer4/server/python_sync_server.py +139 -0
- clm/layer4/server/service_api.py +315 -0
- clm/layer4/server/start_servers.py +107 -0
- clm/layer4/server/stop_servers.py +24 -0
- clm/layer4/simulated_optimization_fix.py +0 -0
- clm/layer4/storage.py +463 -0
- clm/layer4/tridb_admin.py +268 -0
- clm/layer4/websocket/test_ws_client.py +104 -0
- clm/layer4/websocket/test_ws_server.py +138 -0
- clm/layer4/websocket/ws_server_local.py +160 -0
- clm/layer5/__init__.py +51 -0
- clm/layer5/async_batch_writer.py +133 -0
- clm/layer5/learning.py +276 -0
- clm/layer5/telemetry_hook.py +107 -0
- clm/layer5/type_lattice.py +238 -0
- clm/py.typed +0 -0
- clm/schemas/__init__.py +652 -0
- clm/schemas/abstract_face_schema.yaml +77 -0
- clm/schemas/cel_schema.yaml +416 -0
- clm/schemas/lifecycle/__init__.py +45 -0
- clm/schemas/lifecycle/models.py +123 -0
- clm/schemas/lifecycle/service.py +364 -0
- clm/schemas/mcard_schema.sql +192 -0
- clm/schemas/mcard_vector_schema.sql +283 -0
- clm/schemas/mlp-context-v1.json +91 -0
- clm_kernel-0.0.0.dist-info/METADATA +121 -0
- clm_kernel-0.0.0.dist-info/RECORD +333 -0
- clm_kernel-0.0.0.dist-info/WHEEL +4 -0
- clm_kernel-0.0.0.dist-info/entry_points.txt +3 -0
- clm_kernel-0.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""CEL string, path, and comparison ops (Sprint 133.2).
|
|
2
|
+
|
|
3
|
+
Extracted from ``cel_ops.py``.
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
|
|
8
|
+
from clm.cross_cutting.cel_engine import _resolve_arg
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def op_path_join(cel: dict, ctx: dict):
|
|
12
|
+
parts = [str(_resolve_arg(p, ctx)) for p in cel["parts"]]
|
|
13
|
+
return os.path.join(*parts)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def op_path_basename(cel: dict, ctx: dict):
|
|
17
|
+
value = _resolve_arg(cel["value"], ctx)
|
|
18
|
+
return os.path.basename(str(value))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def op_string_concat(cel: dict, ctx: dict):
|
|
22
|
+
parts = [_resolve_arg(p, ctx) for p in cel["parts"]]
|
|
23
|
+
return "".join(str(p) for p in parts)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def op_string_join(cel: dict, ctx: dict):
|
|
27
|
+
separator = str(_resolve_arg(cel["separator"], ctx))
|
|
28
|
+
parts = [_resolve_arg(p, ctx) for p in cel["parts"]]
|
|
29
|
+
return separator.join(str(p) for p in parts)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def op_replace(cel: dict, ctx: dict):
|
|
33
|
+
value = str(_resolve_arg(cel["value"], ctx))
|
|
34
|
+
old = str(_resolve_arg(cel["old"], ctx))
|
|
35
|
+
new = str(_resolve_arg(cel["new"], ctx))
|
|
36
|
+
return value.replace(old, new)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
_COMPARE_OPS: dict[str, Callable] = {
|
|
40
|
+
"eq": lambda a, b: a == b,
|
|
41
|
+
"ne": lambda a, b: a != b,
|
|
42
|
+
"gt": lambda a, b: a > b,
|
|
43
|
+
"gte": lambda a, b: a >= b,
|
|
44
|
+
"lt": lambda a, b: a < b,
|
|
45
|
+
"lte": lambda a, b: a <= b,
|
|
46
|
+
"in": lambda a, b: (tuple(a) if isinstance(a, list) else a) in b,
|
|
47
|
+
"not_in": lambda a, b: (tuple(a) if isinstance(a, list) else a) not in b,
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def op_compare(cel: dict, ctx: dict):
|
|
52
|
+
left = _resolve_arg(cel["left"], ctx)
|
|
53
|
+
right = _resolve_arg(cel["right"], ctx)
|
|
54
|
+
operator = cel["operator"]
|
|
55
|
+
fn = _COMPARE_OPS.get(operator)
|
|
56
|
+
if fn is None:
|
|
57
|
+
raise ValueError(f"Unknown CEL compare operator: {operator}")
|
|
58
|
+
return fn(left, right)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def op_contains(cel: dict, ctx: dict):
|
|
62
|
+
haystack = _resolve_arg(cel["haystack"], ctx)
|
|
63
|
+
needle = _resolve_arg(cel["needle"], ctx)
|
|
64
|
+
return needle in haystack
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Unified CEL reference resolver (Sprint 133 §7.4).
|
|
2
|
+
|
|
3
|
+
Extracted from ``cel_engine._resolve_ref`` and ``CelStore.select``.
|
|
4
|
+
Both operated on the same dot-notation namespace dispatch but were
|
|
5
|
+
duplicated — one for raw ``dict`` contexts, one for typed ``CelStore``.
|
|
6
|
+
|
|
7
|
+
This module provides a single ``resolve_ref`` function that works against
|
|
8
|
+
either a ``dict`` or a ``CelStore``, eliminating the duplication.
|
|
9
|
+
"""
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def resolve_ref(ref_str: str, source: Any) -> Any:
|
|
14
|
+
"""Resolve a dot-notation reference against a dict or CelStore.
|
|
15
|
+
|
|
16
|
+
Namespaces:
|
|
17
|
+
inputs.<key> -> source["inputs"][key] / source.state.inputs[key]
|
|
18
|
+
outputs.<key> -> source["outputs"][key] / source.state.outputs[key]
|
|
19
|
+
state.<key> -> source["retrieve"](key) / source.state.vars[key]
|
|
20
|
+
env.<key> -> source["tmp_dir"] etc. / source.env.tmp_dir etc.
|
|
21
|
+
services.<name> -> source["db"] etc. / source.services.db etc.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
ref_str: Dot-notation reference (e.g. ``"inputs.schema_path"``).
|
|
25
|
+
source: A ``dict`` (legacy context) or a ``CelStore``.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
The resolved value.
|
|
29
|
+
|
|
30
|
+
Raises:
|
|
31
|
+
ValueError: If the namespace is unknown.
|
|
32
|
+
KeyError: If the key is not found in the namespace.
|
|
33
|
+
"""
|
|
34
|
+
parts = ref_str.split(".", 1)
|
|
35
|
+
namespace = parts[0]
|
|
36
|
+
key = parts[1] if len(parts) > 1 else ""
|
|
37
|
+
|
|
38
|
+
# CelStore path (typed)
|
|
39
|
+
if hasattr(source, "state") and hasattr(source, "env"):
|
|
40
|
+
if namespace == "inputs":
|
|
41
|
+
return source.state.inputs[key]
|
|
42
|
+
if namespace == "outputs":
|
|
43
|
+
return source.state.outputs[key]
|
|
44
|
+
if namespace == "state":
|
|
45
|
+
return source.state.vars[key]
|
|
46
|
+
if namespace == "env":
|
|
47
|
+
env_map = {"tmp_dir": source.env.tmp_dir, "clm_root": source.env.clm_root, "rc": source.state.rc}
|
|
48
|
+
return env_map.get(key, key)
|
|
49
|
+
if namespace == "services":
|
|
50
|
+
return getattr(source.services, key, None)
|
|
51
|
+
raise ValueError(f"Unknown CEL reference namespace: {namespace}")
|
|
52
|
+
|
|
53
|
+
# Dict path (legacy context)
|
|
54
|
+
if namespace == "inputs":
|
|
55
|
+
return source["inputs"][key]
|
|
56
|
+
if namespace == "outputs":
|
|
57
|
+
return source["outputs"][key]
|
|
58
|
+
if namespace == "state":
|
|
59
|
+
return source["retrieve"](key)
|
|
60
|
+
if namespace == "env":
|
|
61
|
+
env_map = {"tmp_dir": "tmp_dir", "clm_root": "clm_root", "rc": "rc"}
|
|
62
|
+
return source[env_map.get(key, key)]
|
|
63
|
+
raise ValueError(f"Unknown CEL reference namespace: {namespace}")
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""CelStore state slices (Sprint 133.2).
|
|
2
|
+
|
|
3
|
+
Extracted from ``cel_store.py``. The three Pydantic slice models that
|
|
4
|
+
structure the store's state: ``StateSlice`` (mutable), ``ServiceSlice``
|
|
5
|
+
(read-only services), ``EnvSlice`` (read-only environment).
|
|
6
|
+
"""
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class StateSlice(BaseModel):
|
|
13
|
+
"""Mutable test state — the data that changes during a test case."""
|
|
14
|
+
inputs: dict[str, Any] = Field(default_factory=dict)
|
|
15
|
+
outputs: dict[str, Any] = Field(default_factory=dict)
|
|
16
|
+
rc: int = 0
|
|
17
|
+
vars: dict[str, Any] = Field(default_factory=dict)
|
|
18
|
+
card_vars: dict[str, str] = Field(default_factory=dict)
|
|
19
|
+
|
|
20
|
+
def get(self, key: str) -> Any:
|
|
21
|
+
return self.vars.get(key)
|
|
22
|
+
|
|
23
|
+
def set(self, key: str, value: Any) -> Any:
|
|
24
|
+
self.vars[key] = value
|
|
25
|
+
self.card_vars.pop(key, None)
|
|
26
|
+
return value
|
|
27
|
+
|
|
28
|
+
def set_card(self, key: str, handle: str, value: Any) -> Any:
|
|
29
|
+
self.vars[key] = value
|
|
30
|
+
self.card_vars[key] = handle
|
|
31
|
+
return value
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ServiceSlice(BaseModel):
|
|
35
|
+
"""Read-only services injected into the store."""
|
|
36
|
+
db: Any = None
|
|
37
|
+
ctx: Any = None
|
|
38
|
+
repo: Any = None
|
|
39
|
+
classify_clm: Any = None
|
|
40
|
+
detect_dialect: Any = None
|
|
41
|
+
modules: dict[str, Any] = Field(default_factory=dict)
|
|
42
|
+
|
|
43
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class EnvSlice(BaseModel):
|
|
47
|
+
"""Read-only environment configuration."""
|
|
48
|
+
clm_root: str = ""
|
|
49
|
+
tmp_dir: str = ""
|
|
50
|
+
os: Any = None
|
|
51
|
+
sys: Any = None
|
|
52
|
+
time: Any = None
|
|
53
|
+
hashlib: Any = None
|
|
54
|
+
str: type = str
|
|
55
|
+
|
|
56
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""CelStore — Flux-pattern store for CEL evaluation state (Sprint 128.1+128.2).
|
|
2
|
+
|
|
3
|
+
Implements the Single Source of Truth principle from the Flux/Redux/Zustand
|
|
4
|
+
pattern. Replaces the 30+ key mutable ``context`` dict ("God Object") with a
|
|
5
|
+
typed store structured into three slices:
|
|
6
|
+
|
|
7
|
+
- ``StateSlice`` — mutable test state (inputs, outputs, rc, vars)
|
|
8
|
+
- ``ServiceSlice`` — read-only services (db, repo, ctx, classifiers)
|
|
9
|
+
- ``EnvSlice`` — read-only environment (clm_root, tmp_dir)
|
|
10
|
+
|
|
11
|
+
Unidirectional data flow:
|
|
12
|
+
|
|
13
|
+
action -> pre-middleware -> reducer -> post-middleware -> action log
|
|
14
|
+
-> notify subscribers
|
|
15
|
+
|
|
16
|
+
Backward compatibility: :meth:`CelStore.to_dict` produces the exact dict shape
|
|
17
|
+
that existing CEL op handlers and tests expect, so handlers continue to
|
|
18
|
+
receive ``dict`` and require no changes during migration.
|
|
19
|
+
|
|
20
|
+
Design references:
|
|
21
|
+
- Sprint 128 spec (docs/sprints/sprint-128-flux-store-architecture.md)
|
|
22
|
+
- Redux (single store, unidirectional flow)
|
|
23
|
+
- Zustand (minimal get/set/subscribe API)
|
|
24
|
+
- nanostores (computed selectors)
|
|
25
|
+
- Thunk (side-effect isolation via EffectRegistry)
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import time
|
|
31
|
+
from collections.abc import Callable
|
|
32
|
+
from typing import Any
|
|
33
|
+
|
|
34
|
+
from pydantic import BaseModel # noqa: F401 — re-exported for backward compat
|
|
35
|
+
|
|
36
|
+
# Sprint 133.2: middleware types (re-exported from cel_builder for backward compat)
|
|
37
|
+
from clm.cross_cutting.cel_builder import ( # noqa: F401
|
|
38
|
+
Listener,
|
|
39
|
+
PostMiddleware,
|
|
40
|
+
PreMiddleware,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Sprint 133.2: slices extracted to cel_slices.py
|
|
44
|
+
from clm.cross_cutting.cel_slices import EnvSlice, ServiceSlice, StateSlice # noqa: F401
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# CelStore (Sprint 128.1 + 128.2)
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
class CelStore:
|
|
51
|
+
"""Flux-pattern store — the Single Source of Truth for CEL evaluation.
|
|
52
|
+
|
|
53
|
+
API (Zustand-inspired minimal surface):
|
|
54
|
+
|
|
55
|
+
store.get_state() -> StateSlice
|
|
56
|
+
store.get_services() -> ServiceSlice
|
|
57
|
+
store.get_env() -> EnvSlice
|
|
58
|
+
store.set(key, value) -> mutate state.vars[key]
|
|
59
|
+
store.get(key) -> read state.vars[key]
|
|
60
|
+
store.select(ref) -> dot-notation selector
|
|
61
|
+
store.dispatch(action) -> unidirectional flow
|
|
62
|
+
store.subscribe(fn) -> listen for state changes
|
|
63
|
+
store.to_dict() -> backward-compat dict adapter
|
|
64
|
+
|
|
65
|
+
The dispatch flow is:
|
|
66
|
+
|
|
67
|
+
1. Run pre-middleware (can modify or reject action by returning None)
|
|
68
|
+
2. Invoke reducer via CelOpRegistry.dispatch()
|
|
69
|
+
3. Run post-middleware (observes result)
|
|
70
|
+
4. Append to action_log
|
|
71
|
+
5. Notify subscribers
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
state: StateSlice | None = None,
|
|
77
|
+
services: ServiceSlice | None = None,
|
|
78
|
+
env: EnvSlice | None = None,
|
|
79
|
+
) -> None:
|
|
80
|
+
self.state: StateSlice = state or StateSlice()
|
|
81
|
+
self.services: ServiceSlice = services or ServiceSlice()
|
|
82
|
+
self.env: EnvSlice = env or EnvSlice()
|
|
83
|
+
self.action_log: list[dict[str, Any]] = []
|
|
84
|
+
self._listeners: list[Listener] = []
|
|
85
|
+
self._pre_middleware: list[PreMiddleware] = []
|
|
86
|
+
self._post_middleware: list[PostMiddleware] = []
|
|
87
|
+
# Lazily-injected side-effect registry (Sprint 128.4)
|
|
88
|
+
self.effects: Any = None
|
|
89
|
+
# Writer monad delegate (Sprint 132.1 — handle_history persistence)
|
|
90
|
+
from clm.cross_cutting.writer import StoreWriter
|
|
91
|
+
self.writer: StoreWriter = StoreWriter(self)
|
|
92
|
+
# Sprint 132.5: cached to_dict() — invalidated on set()/set_persisted()
|
|
93
|
+
self._ctx_cache: dict[str, Any] | None = None
|
|
94
|
+
|
|
95
|
+
def _invalidate_cache(self) -> None:
|
|
96
|
+
"""Invalidate the to_dict() cache (Sprint 132.5)."""
|
|
97
|
+
self._ctx_cache = None
|
|
98
|
+
|
|
99
|
+
# ------------------------------------------------------------------
|
|
100
|
+
# Slice accessors
|
|
101
|
+
# ------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
def get_state(self) -> StateSlice:
|
|
104
|
+
return self.state
|
|
105
|
+
|
|
106
|
+
def get_services(self) -> ServiceSlice:
|
|
107
|
+
return self.services
|
|
108
|
+
|
|
109
|
+
def get_env(self) -> EnvSlice:
|
|
110
|
+
return self.env
|
|
111
|
+
|
|
112
|
+
# ------------------------------------------------------------------
|
|
113
|
+
# State mutation (Zustand-style minimal API)
|
|
114
|
+
# ------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
def get(self, key: str) -> Any:
|
|
117
|
+
return self.state.get(key)
|
|
118
|
+
|
|
119
|
+
def set(self, key: str, value: Any) -> Any:
|
|
120
|
+
self._invalidate_cache()
|
|
121
|
+
return self.state.set(key, value)
|
|
122
|
+
|
|
123
|
+
def select(self, ref: str) -> Any:
|
|
124
|
+
"""Resolve a dot-notation reference against the store.
|
|
125
|
+
|
|
126
|
+
Sprint 133 §7.4: delegates to ``cel_resolver.resolve_ref`` for a
|
|
127
|
+
single implementation shared with ``cel_engine._resolve_ref``.
|
|
128
|
+
"""
|
|
129
|
+
from clm.cross_cutting.cel_resolver import resolve_ref
|
|
130
|
+
return resolve_ref(ref, self)
|
|
131
|
+
|
|
132
|
+
# ------------------------------------------------------------------
|
|
133
|
+
# Handle-backed persistence (Sprint 131.3 — opt-in, backward-compatible)
|
|
134
|
+
# ------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
def set_persisted(self, key: str, value: Any, pillar: str = "execution_log") -> Any:
|
|
137
|
+
"""Delegate to StoreWriter (Writer monad — moved to writer.py in Sprint 132.1)."""
|
|
138
|
+
self._invalidate_cache()
|
|
139
|
+
return self.writer.set_persisted(key, value, pillar)
|
|
140
|
+
|
|
141
|
+
def select_card(self, ref: str) -> Any:
|
|
142
|
+
"""Delegate to StoreWriter (Writer monad — moved to writer.py in Sprint 132.1)."""
|
|
143
|
+
return self.writer.select_card(ref)
|
|
144
|
+
|
|
145
|
+
# ------------------------------------------------------------------
|
|
146
|
+
# Middleware registration
|
|
147
|
+
# ------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
def add_pre_middleware(self, mw: PreMiddleware) -> None:
|
|
150
|
+
self._pre_middleware.append(mw)
|
|
151
|
+
|
|
152
|
+
def add_post_middleware(self, mw: PostMiddleware) -> None:
|
|
153
|
+
self._post_middleware.append(mw)
|
|
154
|
+
|
|
155
|
+
# ------------------------------------------------------------------
|
|
156
|
+
# Subscribers
|
|
157
|
+
# ------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
def subscribe(self, listener: Listener) -> Callable[[], None]:
|
|
160
|
+
"""Subscribe to state changes. Returns an unsubscribe function."""
|
|
161
|
+
self._listeners.append(listener)
|
|
162
|
+
|
|
163
|
+
def unsubscribe() -> None:
|
|
164
|
+
if listener in self._listeners:
|
|
165
|
+
self._listeners.remove(listener)
|
|
166
|
+
|
|
167
|
+
return unsubscribe
|
|
168
|
+
|
|
169
|
+
# ------------------------------------------------------------------
|
|
170
|
+
# Dispatch (Sprint 128.2 — unidirectional data flow)
|
|
171
|
+
# ------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
def dispatch(self, action: dict) -> Any:
|
|
174
|
+
"""Dispatch a CEL action through the middleware pipeline.
|
|
175
|
+
|
|
176
|
+
Flow:
|
|
177
|
+
pre-middleware -> reducer -> post-middleware -> log -> notify
|
|
178
|
+
|
|
179
|
+
If any pre-middleware returns ``None``, the action is rejected
|
|
180
|
+
and ``None`` is returned without invoking the reducer.
|
|
181
|
+
"""
|
|
182
|
+
ctx = self.to_dict()
|
|
183
|
+
|
|
184
|
+
# 1. Pre-middleware (can reject by returning None)
|
|
185
|
+
for mw in self._pre_middleware:
|
|
186
|
+
result = mw(self, action)
|
|
187
|
+
if result is None:
|
|
188
|
+
self._log_action(action, None, rejected=True)
|
|
189
|
+
return None
|
|
190
|
+
action = result
|
|
191
|
+
ctx = self.to_dict()
|
|
192
|
+
|
|
193
|
+
# 2. Reducer (delegate to CelOpRegistry)
|
|
194
|
+
from clm.cross_cutting.cel_engine import _DEFAULT_REGISTRY
|
|
195
|
+
result = _DEFAULT_REGISTRY.dispatch(action, ctx)
|
|
196
|
+
|
|
197
|
+
# 3. Post-middleware
|
|
198
|
+
for mw in self._post_middleware:
|
|
199
|
+
mw(self, action, result)
|
|
200
|
+
|
|
201
|
+
# 4. Action log
|
|
202
|
+
self._log_action(action, result)
|
|
203
|
+
|
|
204
|
+
# 5. Notify subscribers
|
|
205
|
+
for listener in self._listeners:
|
|
206
|
+
listener(self, action, result)
|
|
207
|
+
|
|
208
|
+
return result
|
|
209
|
+
|
|
210
|
+
def _log_action(self, action: dict, result: Any, rejected: bool = False) -> None:
|
|
211
|
+
self.action_log.append({
|
|
212
|
+
"type": "cel_dispatch",
|
|
213
|
+
"op": action.get("op"),
|
|
214
|
+
"action": action,
|
|
215
|
+
"result": repr(result)[:200],
|
|
216
|
+
"rejected": rejected,
|
|
217
|
+
"timestamp": time.time(),
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
# ------------------------------------------------------------------
|
|
221
|
+
# Backward-compatibility adapter (Sprint 128.1)
|
|
222
|
+
# ------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
def to_dict(self) -> dict[str, Any]:
|
|
225
|
+
"""Produce the legacy context dict shape for op handlers.
|
|
226
|
+
|
|
227
|
+
Sprint 132.5: caches the result; invalidated on set()/set_persisted().
|
|
228
|
+
"""
|
|
229
|
+
if self._ctx_cache is not None:
|
|
230
|
+
return self._ctx_cache
|
|
231
|
+
ctx: dict[str, Any] = {
|
|
232
|
+
# State slice
|
|
233
|
+
"inputs": self.state.inputs,
|
|
234
|
+
"outputs": self.state.outputs,
|
|
235
|
+
"rc": self.state.rc,
|
|
236
|
+
"store": self.state.set,
|
|
237
|
+
"retrieve": self.state.get,
|
|
238
|
+
# Service slice
|
|
239
|
+
"db": self.services.db,
|
|
240
|
+
"ctx": self.services.ctx,
|
|
241
|
+
"repo": self.services.repo,
|
|
242
|
+
"classify_clm": self.services.classify_clm,
|
|
243
|
+
"detect_dialect": self.services.detect_dialect,
|
|
244
|
+
# Env slice
|
|
245
|
+
"clm_root": self.env.clm_root,
|
|
246
|
+
"tmp_dir": self.env.tmp_dir,
|
|
247
|
+
"os": self.env.os,
|
|
248
|
+
"sys": self.env.sys,
|
|
249
|
+
"time": self.env.time,
|
|
250
|
+
"hashlib": self.env.hashlib,
|
|
251
|
+
"str": self.env.str,
|
|
252
|
+
}
|
|
253
|
+
# Merge any extra service modules (write_file, make_mcard_content, etc.)
|
|
254
|
+
ctx.update(self.services.modules)
|
|
255
|
+
# Expose the effect registry for side-effect isolation (Sprint 128.4).
|
|
256
|
+
ctx["effects"] = self.effects
|
|
257
|
+
self._ctx_cache = ctx
|
|
258
|
+
return ctx
|
|
259
|
+
|
|
260
|
+
# ------------------------------------------------------------------
|
|
261
|
+
# Monadic operations (Sprint 129.2 — bind/unit)
|
|
262
|
+
# ------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
def bind(self, next_fn: Callable[["CelStore", Any], Any]) -> Any:
|
|
265
|
+
"""Monadic bind (>>= operator).
|
|
266
|
+
|
|
267
|
+
Threads the store through a sequence of computations:
|
|
268
|
+
store.bind(f).bind(g) == store >>= f >>= g
|
|
269
|
+
|
|
270
|
+
Each bind:
|
|
271
|
+
1. If there's a pending action, dispatch it and use the result
|
|
272
|
+
2. Otherwise, use the monadic value from the previous unit()
|
|
273
|
+
3. Pass the value to next_fn
|
|
274
|
+
4. Return the result
|
|
275
|
+
|
|
276
|
+
This satisfies the three monad laws (verified in tests/test_monadic_laws.py):
|
|
277
|
+
Left identity: unit(a).bind(f) == f(store, a)
|
|
278
|
+
Right identity: store.bind(f).bind(unit) == store.bind(f)
|
|
279
|
+
Associativity: store.bind(f).bind(g) == store.bind(lambda s, x: f(s,x).bind(g))
|
|
280
|
+
"""
|
|
281
|
+
result = None
|
|
282
|
+
pending = getattr(self, "_pending_action", None)
|
|
283
|
+
if pending is not None:
|
|
284
|
+
self._pending_action = None
|
|
285
|
+
result = self.dispatch(pending)
|
|
286
|
+
else:
|
|
287
|
+
# Use monadic value from previous unit() call
|
|
288
|
+
result = self.state.get("_monadic_value")
|
|
289
|
+
return next_fn(self, result)
|
|
290
|
+
|
|
291
|
+
def unit(self, value: Any) -> "CelStore":
|
|
292
|
+
"""Monadic unit/return — wraps a value in the store context.
|
|
293
|
+
|
|
294
|
+
Stores the value as a state var, ready for the next bind in the chain.
|
|
295
|
+
"""
|
|
296
|
+
self.set("_monadic_value", value)
|
|
297
|
+
self._pending_action = None
|
|
298
|
+
return self
|
|
299
|
+
|
|
300
|
+
@property
|
|
301
|
+
def pending_action(self) -> dict | None:
|
|
302
|
+
"""Get or set the pending action for deferred dispatch via bind()."""
|
|
303
|
+
return getattr(self, "_pending_action", None)
|
|
304
|
+
|
|
305
|
+
@pending_action.setter
|
|
306
|
+
def pending_action(self, action: dict | None) -> None:
|
|
307
|
+
self._pending_action = action
|
|
308
|
+
|
|
309
|
+
# ------------------------------------------------------------------
|
|
310
|
+
# Flux event store persistence (Sprint 129.3)
|
|
311
|
+
# ------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
def flush_action_log(self, ctx: Any) -> str | None:
|
|
314
|
+
"""Delegate to StoreWriter (Writer monad — moved to writer.py in Sprint 132.1)."""
|
|
315
|
+
return self.writer.flush_action_log(ctx)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
# Sprint 133.2: thunk moved to vcard.py (Sprint 132.1); re-exported here.
|
|
319
|
+
# Sprint 133.2: middleware factories + build_store extracted to cel_builder.py
|
|
320
|
+
from clm.cross_cutting.cel_builder import ( # noqa: E402,F401
|
|
321
|
+
_build_write_file_fn,
|
|
322
|
+
action_log_middleware,
|
|
323
|
+
build_store,
|
|
324
|
+
vcard_post_middleware,
|
|
325
|
+
vcard_pre_middleware,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# Sprint 133.2: EffectRegistry re-exported from effects.py (Sprint 132.1)
|
|
329
|
+
from clm.cross_cutting.effects import EffectRegistry, register_default_effects # noqa: E402,F401
|
|
330
|
+
from clm.cross_cutting.vcard import thunk # noqa: E402,F401
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""CLMLogger — Unified logging for the CLM platform (Sprint 129.1).
|
|
2
|
+
|
|
3
|
+
Replaces all ad-hoc ``print(..., file=sys.stderr)`` calls with a structured,
|
|
4
|
+
leveled, configurable logger. Extends Python's ``logging`` module with
|
|
5
|
+
CLM-specific structured fields (agent_did, run_id, pcard_hash, op).
|
|
6
|
+
|
|
7
|
+
Configuration via environment variables:
|
|
8
|
+
CLM_LOG_LEVEL: DEBUG | INFO | WARNING | ERROR (default: INFO)
|
|
9
|
+
CLM_LOG_FORMAT: text | json (default: text)
|
|
10
|
+
CLM_LOG_FILE: Path to log file (default: stderr only)
|
|
11
|
+
CLM_LOG_STRUCTURED: true | false (default: false; if true, forces JSON)
|
|
12
|
+
|
|
13
|
+
Cross-runtime protocol:
|
|
14
|
+
Rust/WASM/Shell runtimes emit the same JSON schema to stderr,
|
|
15
|
+
enabling unified observability across language boundaries.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import logging
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from clm.cross_cutting.timeutil import default_agent_did, now_g_time
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CLMJsonFormatter(logging.Formatter):
|
|
30
|
+
"""JSON formatter for cross-runtime log compatibility."""
|
|
31
|
+
|
|
32
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
33
|
+
log_entry: dict[str, Any] = {
|
|
34
|
+
"timestamp": now_g_time(),
|
|
35
|
+
"level": record.levelname,
|
|
36
|
+
"logger": record.name,
|
|
37
|
+
"message": record.getMessage(),
|
|
38
|
+
}
|
|
39
|
+
for attr in ("agent_did", "run_id", "pcard_hash", "op"):
|
|
40
|
+
if hasattr(record, attr):
|
|
41
|
+
log_entry[attr] = getattr(record, attr)
|
|
42
|
+
return json.dumps(log_entry)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class DynamicStreamHandler(logging.StreamHandler):
|
|
46
|
+
"""A StreamHandler that resolves sys.stderr dynamically on every write,
|
|
47
|
+
allowing unit tests to redirect stderr using sys.stderr = StringIO().
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def stream(self) -> Any:
|
|
52
|
+
return sys.stderr
|
|
53
|
+
|
|
54
|
+
@stream.setter
|
|
55
|
+
def stream(self, value: Any) -> None:
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class CLMLogger:
|
|
60
|
+
"""Unified logger for the CLM platform.
|
|
61
|
+
|
|
62
|
+
Extends Python's logging.Logger with CLM-specific structured fields:
|
|
63
|
+
- agent_did: The DID of the executing agent
|
|
64
|
+
- run_id: The current CLM run ID
|
|
65
|
+
- pcard_hash: The PCard being executed (if applicable)
|
|
66
|
+
- op: The CEL operation being dispatched (if applicable)
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
_instance: CLMLogger | None = None
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def get(cls, name: str = "clm") -> CLMLogger:
|
|
73
|
+
"""Get the singleton CLMLogger instance."""
|
|
74
|
+
if cls._instance is None:
|
|
75
|
+
cls._instance = cls(name)
|
|
76
|
+
return cls._instance
|
|
77
|
+
|
|
78
|
+
def __init__(self, name: str = "clm") -> None:
|
|
79
|
+
self._logger = logging.getLogger(name)
|
|
80
|
+
self._agent_did = default_agent_did()
|
|
81
|
+
self._run_id = os.environ.get("CLM_RUN_ID", "")
|
|
82
|
+
self._configure()
|
|
83
|
+
|
|
84
|
+
def _configure(self) -> None:
|
|
85
|
+
level = os.environ.get("CLM_LOG_LEVEL", "INFO").upper()
|
|
86
|
+
fmt = os.environ.get("CLM_LOG_FORMAT", "text")
|
|
87
|
+
log_file = os.environ.get("CLM_LOG_FILE", "")
|
|
88
|
+
|
|
89
|
+
self._logger.setLevel(getattr(logging, level, logging.INFO))
|
|
90
|
+
|
|
91
|
+
if self._logger.handlers:
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
if fmt == "json" or os.environ.get("CLM_LOG_STRUCTURED", "").lower() == "true":
|
|
95
|
+
handler: logging.Handler = DynamicStreamHandler()
|
|
96
|
+
handler.setFormatter(CLMJsonFormatter())
|
|
97
|
+
else:
|
|
98
|
+
handler = DynamicStreamHandler()
|
|
99
|
+
handler.setFormatter(logging.Formatter(
|
|
100
|
+
"%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
101
|
+
datefmt="%Y-%m-%dT%H:%M:%S%z",
|
|
102
|
+
))
|
|
103
|
+
|
|
104
|
+
self._logger.addHandler(handler)
|
|
105
|
+
|
|
106
|
+
if log_file:
|
|
107
|
+
file_handler = logging.FileHandler(log_file)
|
|
108
|
+
file_handler.setFormatter(handler.formatter)
|
|
109
|
+
self._logger.addHandler(file_handler)
|
|
110
|
+
|
|
111
|
+
def _extra(self, **kwargs: Any) -> dict:
|
|
112
|
+
extra: dict[str, Any] = {"agent_did": self._agent_did, "run_id": self._run_id}
|
|
113
|
+
extra.update(kwargs)
|
|
114
|
+
return extra
|
|
115
|
+
|
|
116
|
+
def debug(self, msg: str, **kwargs: Any) -> None:
|
|
117
|
+
exc_info = kwargs.pop("exc_info", None)
|
|
118
|
+
self._logger.debug(msg, extra=self._extra(**kwargs), exc_info=exc_info)
|
|
119
|
+
|
|
120
|
+
def info(self, msg: str, **kwargs: Any) -> None:
|
|
121
|
+
exc_info = kwargs.pop("exc_info", None)
|
|
122
|
+
self._logger.info(msg, extra=self._extra(**kwargs), exc_info=exc_info)
|
|
123
|
+
|
|
124
|
+
def warning(self, msg: str, **kwargs: Any) -> None:
|
|
125
|
+
exc_info = kwargs.pop("exc_info", None)
|
|
126
|
+
self._logger.warning(msg, extra=self._extra(**kwargs), exc_info=exc_info)
|
|
127
|
+
|
|
128
|
+
def error(self, msg: str, **kwargs: Any) -> None:
|
|
129
|
+
exc_info = kwargs.pop("exc_info", None)
|
|
130
|
+
self._logger.error(msg, extra=self._extra(**kwargs), exc_info=exc_info)
|
|
131
|
+
|
|
132
|
+
def critical(self, msg: str, **kwargs: Any) -> None:
|
|
133
|
+
exc_info = kwargs.pop("exc_info", None)
|
|
134
|
+
self._logger.critical(msg, extra=self._extra(**kwargs), exc_info=exc_info)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def get_logger(name: str = "clm") -> CLMLogger:
|
|
139
|
+
"""Convenience function to get the CLMLogger singleton."""
|
|
140
|
+
return CLMLogger.get(name)
|