commit b2b99bcb5fb3f6250eedac5fbbe61db8ff8be6dd
parent 122c10d1a03848af81fd309a49f21e7dca37aa72
Author: triesap <tyson@radroots.org>
Date: Wed, 8 Apr 2026 17:40:54 +0000
core: add shared hyf context and provenance models
- add typed request-context defaults and semantic validation in hyf_core
- add transport-agnostic provenance and core result error structs
- extend wire requests with optional shared context parsed through core
- expose request-context feature discovery through the stdio control plane
Diffstat:
6 files changed, 347 insertions(+), 10 deletions(-)
diff --git a/src/hyf_core/errors.mojo b/src/hyf_core/errors.mojo
@@ -1,3 +1,43 @@
-def core_error_module_name() -> String:
- return "hyf_core.errors"
+from std.collections import Optional
+from mojson import Value
+
+from hyf_core.provenance import CoreResponseMeta
+
+
+@fieldwise_init
+struct CoreError(Copyable, Movable):
+ var code: String
+ var message: String
+ var retryable: Bool
+
+
+@fieldwise_init
+struct CapabilitySuccess(Copyable, Movable):
+ var output: Value
+ var meta: Optional[CoreResponseMeta]
+
+
+@fieldwise_init
+struct CapabilityFailure(Copyable, Movable):
+ var error: CoreError
+
+
+def invalid_context_error(message: String) -> CoreError:
+ return CoreError(code="invalid_context", message=message, retryable=False)
+
+
+def capability_not_implemented_error(capability: String) -> CoreError:
+ return CoreError(
+ code="capability_not_implemented",
+ message="core capability '" + capability + "' is not implemented yet",
+ retryable=False,
+ )
+
+
+def backend_unavailable_error(backend: String) -> CoreError:
+ return CoreError(
+ code="backend_unavailable",
+ message="backend '" + backend + "' is unavailable",
+ retryable=True,
+ )
diff --git a/src/hyf_core/provenance.mojo b/src/hyf_core/provenance.mojo
@@ -1,3 +1,39 @@
-def provenance_module_name() -> String:
- return "hyf_core.provenance"
+from std.collections import List, Optional
+
+@fieldwise_init
+struct ProvenanceSourceRef(Copyable, Movable):
+ var source_kind: String
+ var source_ref: String
+
+
+@fieldwise_init
+struct ProvenanceFallback(Copyable, Movable):
+ var fallback_kind: String
+ var reason: String
+
+
+@fieldwise_init
+struct ExecutionProvenance(Copyable, Movable):
+ var kind: String
+ var signal_tags: List[String]
+ var source_refs: List[ProvenanceSourceRef]
+ var fallback: Optional[ProvenanceFallback]
+ var evidence_set_id: Optional[String]
+
+
+@fieldwise_init
+struct CoreResponseMeta(Copyable, Movable):
+ var mode: String
+ var backend: String
+ var latency_ms: Int
+ var provenance: Optional[ExecutionProvenance]
+
+
+def deterministic_response_meta() -> CoreResponseMeta:
+ return CoreResponseMeta(
+ mode="a",
+ backend="heuristic",
+ latency_ms=0,
+ provenance=None,
+ )
diff --git a/src/hyf_core/request_context.mojo b/src/hyf_core/request_context.mojo
@@ -1,3 +1,235 @@
-def request_context_module_name() -> String:
- return "hyf_core.request_context"
+from std.collections import List, Optional
+from mojson import Value
+from mojson.deserialize import get_bool, get_int, get_string
+
+
+def _has_key(value: Value, key: String) -> Bool:
+ for candidate in value.object_keys():
+ if candidate == key:
+ return True
+ return False
+
+
+def _require_object(value: Value, context: String) raises:
+ if not value.is_object():
+ raise Error(context + " must be a JSON object")
+
+
+def _require_allowed_keys(
+ value: Value, allowed_keys: List[String], context: String
+) raises:
+ for key in value.object_keys():
+ var allowed = False
+ for allowed_key in allowed_keys:
+ if key == allowed_key:
+ allowed = True
+ break
+ if not allowed:
+ raise Error(context + " contains unexpected field '" + key + "'")
+
+
+def _require_non_empty(value: String, context: String) raises:
+ if value == "":
+ raise Error(context + " must not be empty")
+
+
+def _parse_string_list(value: Value, context: String) raises -> List[String]:
+ if value.is_null():
+ return List[String]()
+ if not value.is_array():
+ raise Error(context + " must be a JSON array")
+
+ var items = List[String]()
+ for item in value.array_items():
+ if not item.is_string():
+ raise Error(context + " must contain only strings")
+ _require_non_empty(item.string_value(), context + " item")
+ items.append(item.string_value())
+ return items^
+
+
+@fieldwise_init
+struct RequestScope(Copyable, Movable):
+ var listing_ids: List[String]
+ var farm_ids: List[String]
+ var account_ids: List[String]
+ var platform_ids: List[String]
+ var object_filters: Optional[Value]
+
+
+@fieldwise_init
+struct TimeRange(Copyable, Movable):
+ var start: String
+ var end: String
+
+
+@fieldwise_init
+struct RequestContext(Copyable, Movable):
+ var consumer: String
+ var mode_preference: String
+ var deadline_ms: Int
+ var scope: Optional[RequestScope]
+ var time_range: Optional[TimeRange]
+ var evidence_limit: Int
+ var consistency: String
+ var return_provenance: Bool
+ var explain_plan: Bool
+
+
+def request_context_feature_names() -> List[String]:
+ var features = List[String]()
+ features.append("consumer")
+ features.append("mode_preference")
+ features.append("deadline_ms")
+ features.append("scope")
+ features.append("time_range")
+ features.append("evidence_limit")
+ features.append("consistency")
+ features.append("return_provenance")
+ features.append("explain_plan")
+ return features^
+
+
+def default_request_context() -> RequestContext:
+ return RequestContext(
+ consumer="unknown",
+ mode_preference="a",
+ deadline_ms=2500,
+ scope=None,
+ time_range=None,
+ evidence_limit=10,
+ consistency="default",
+ return_provenance=False,
+ explain_plan=False,
+ )
+
+
+def _parse_scope(json: Value) raises -> RequestScope:
+ _require_object(json, "request context scope")
+
+ var allowed_keys = List[String]()
+ allowed_keys.append("listing_ids")
+ allowed_keys.append("farm_ids")
+ allowed_keys.append("account_ids")
+ allowed_keys.append("platform_ids")
+ allowed_keys.append("object_filters")
+ _require_allowed_keys(json, allowed_keys, "request context scope")
+
+ var object_filters: Optional[Value] = None
+ if _has_key(json, "object_filters"):
+ var raw_filters = json["object_filters"].clone()
+ if not raw_filters.is_null():
+ object_filters = raw_filters^
+
+ var listing_ids_json = Value(None)
+ if _has_key(json, "listing_ids"):
+ listing_ids_json = json["listing_ids"].clone()
+
+ var farm_ids_json = Value(None)
+ if _has_key(json, "farm_ids"):
+ farm_ids_json = json["farm_ids"].clone()
+
+ var account_ids_json = Value(None)
+ if _has_key(json, "account_ids"):
+ account_ids_json = json["account_ids"].clone()
+
+ var platform_ids_json = Value(None)
+ if _has_key(json, "platform_ids"):
+ platform_ids_json = json["platform_ids"].clone()
+
+ return RequestScope(
+ listing_ids=_parse_string_list(
+ listing_ids_json, "request context scope listing_ids"
+ ),
+ farm_ids=_parse_string_list(
+ farm_ids_json, "request context scope farm_ids"
+ ),
+ account_ids=_parse_string_list(
+ account_ids_json, "request context scope account_ids"
+ ),
+ platform_ids=_parse_string_list(
+ platform_ids_json, "request context scope platform_ids"
+ ),
+ object_filters=object_filters^,
+ )
+
+
+def _parse_time_range(json: Value) raises -> TimeRange:
+ _require_object(json, "request context time_range")
+
+ var allowed_keys = List[String]()
+ allowed_keys.append("start")
+ allowed_keys.append("end")
+ _require_allowed_keys(json, allowed_keys, "request context time_range")
+
+ var start = get_string(json, "start")
+ _require_non_empty(start, "request context time_range start")
+
+ var end = get_string(json, "end")
+ _require_non_empty(end, "request context time_range end")
+
+ return TimeRange(start=start, end=end)
+
+
+def parse_request_context(json: Value) raises -> RequestContext:
+ if json.is_null():
+ return default_request_context()
+
+ _require_object(json, "request context")
+
+ var allowed_keys = request_context_feature_names()
+ _require_allowed_keys(json, allowed_keys, "request context")
+
+ var context = default_request_context()
+
+ if _has_key(json, "consumer"):
+ context.consumer = get_string(json, "consumer")
+ _require_non_empty(context.consumer, "request context consumer")
+
+ if _has_key(json, "mode_preference"):
+ context.mode_preference = get_string(json, "mode_preference")
+ if context.mode_preference != "a" and context.mode_preference != "b":
+ raise Error(
+ "request context mode_preference must be 'a' or 'b'"
+ )
+
+ if _has_key(json, "deadline_ms"):
+ context.deadline_ms = get_int(json, "deadline_ms")
+ if context.deadline_ms <= 0:
+ raise Error("request context deadline_ms must be greater than zero")
+
+ if _has_key(json, "scope"):
+ var scope_json = json["scope"].clone()
+ if not scope_json.is_null():
+ context.scope = _parse_scope(scope_json)
+
+ if _has_key(json, "time_range"):
+ var time_range_json = json["time_range"].clone()
+ if not time_range_json.is_null():
+ context.time_range = _parse_time_range(time_range_json)
+
+ if _has_key(json, "evidence_limit"):
+ context.evidence_limit = get_int(json, "evidence_limit")
+ if context.evidence_limit <= 0:
+ raise Error(
+ "request context evidence_limit must be greater than zero"
+ )
+
+ if _has_key(json, "consistency"):
+ context.consistency = get_string(json, "consistency")
+ if (
+ context.consistency != "default"
+ and context.consistency != "strong"
+ ):
+ raise Error(
+ "request context consistency must be 'default' or 'strong'"
+ )
+
+ if _has_key(json, "return_provenance"):
+ context.return_provenance = get_bool(json, "return_provenance")
+
+ if _has_key(json, "explain_plan"):
+ context.explain_plan = get_bool(json, "explain_plan")
+
+ return context^
diff --git a/src/hyf_stdio/control/capabilities.mojo b/src/hyf_stdio/control/capabilities.mojo
@@ -3,6 +3,7 @@ from std.collections import List
from mojson import Value, loads
from hyf_core.capabilities.registry import canonical_business_capabilities
+from hyf_core.request_context import request_context_feature_names
def _string_array(values: List[String]) raises -> Value:
@@ -48,5 +49,8 @@ def build_capabilities_output() raises -> Value:
output.set("business_capabilities", capabilities)
output.set("backend_assisted_capabilities", loads("[]"))
- output.set("request_context_features", loads("[]"))
+ output.set(
+ "request_context_features",
+ _string_array(request_context_feature_names()),
+ )
return output^
diff --git a/src/hyf_stdio/control/status.mojo b/src/hyf_stdio/control/status.mojo
@@ -7,6 +7,7 @@ from hyf_core.capabilities.registry import (
bootstrap_enabled_capabilities,
deferred_capabilities,
)
+from hyf_core.request_context import request_context_feature_names
def _string_array(values: List[String]) raises -> Value:
@@ -52,7 +53,10 @@ def build_status_output() raises -> Value:
var limits = loads("{}")
limits.set("max_requests_per_process", Value(1))
- limits.set("request_context_features", loads("[]"))
+ limits.set(
+ "request_context_features",
+ _string_array(request_context_feature_names()),
+ )
output.set("limits", limits)
return output^
diff --git a/src/hyf_stdio/envelope.mojo b/src/hyf_stdio/envelope.mojo
@@ -1,6 +1,7 @@
from mojson import Value, loads
from mojson.deserialize import Deserializable, get_string
+from hyf_core.request_context import RequestContext, parse_request_context
from hyf_stdio.errors import WireError
@@ -18,16 +19,29 @@ def _require_non_empty(value: String, field_name: String) raises:
def _require_request_keys(value: Value) raises:
for key in value.object_keys():
- if key != "request_id" and key != "capability" and key != "input":
+ if (
+ key != "request_id"
+ and key != "capability"
+ and key != "context"
+ and key != "input"
+ ):
raise Error(
- "request envelope contains unexpected field '" + key + "'"
+ "request envelope contains unexpected field '" + key + "'"
)
+def _has_key(value: Value, key: String) -> Bool:
+ for candidate in value.object_keys():
+ if candidate == key:
+ return True
+ return False
+
+
@fieldwise_init
struct WireRequest(Deserializable, Copyable, Movable):
var request_id: String
var capability: String
+ var context: RequestContext
var input: Value
@staticmethod
@@ -41,9 +55,16 @@ struct WireRequest(Deserializable, Copyable, Movable):
var capability = get_string(json, "capability")
_require_non_empty(capability, "capability")
+ var context_json = Value(None)
+ if _has_key(json, "context"):
+ context_json = json["context"].clone()
+
+ var context = parse_request_context(context_json)
+
return Self(
request_id=request_id,
capability=capability,
+ context=context^,
input=json["input"].clone(),
)