tryvira-sdk 0.1.1__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.
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.4
2
+ Name: tryvira-sdk
3
+ Version: 0.1.1
4
+ Summary: Typed Python client for the Vira authority boundary
5
+ Author: Vira
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Repository, https://github.com/hrpering/vira_beh_redesign
8
+ Project-URL: Documentation, https://github.com/hrpering/vira_beh_redesign/tree/main/sdks/python
9
+ Keywords: ai-governance,observability,agent-runtime
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+
15
+ # Vira Python SDK
16
+
17
+ Package name: `tryvira-sdk`.
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ python -m pip install tryvira-sdk
23
+ ```
24
+
25
+ For a repository checkout use `python -m pip install ./sdks/python`. The client is a thin
26
+ HTTP client of Vira's authority boundary; it does not execute tools locally.
27
+
28
+ For maintainers, build with `python -m build sdks/python`. Public CI uses PyPI Trusted
29
+ Publishing; do not put a token in this repository or in an example.
30
+
31
+ The SDK is a thin client of the Vira authority boundary. It does not execute
32
+ tools or actions locally, invent stable semantic keys, or create a second
33
+ telemetry store. Runtime requests are fail-closed; observation telemetry can be
34
+ configured fail-open with `failure_mode="open"`.
35
+
36
+ ```python
37
+ import os
38
+ from vira import ViraClient
39
+
40
+ with ViraClient(
41
+ "http://127.0.0.1:4185",
42
+ api_key=os.environ.get("VIRA_API_KEY"),
43
+ ) as vira:
44
+ connected = vira.connect_once(
45
+ "my-agent", "custom-agent",
46
+ {"organizationId": "org", "projectId": "project", "environmentId": "local"},
47
+ "custom-python", "1",
48
+ idempotency_key="connect-001",
49
+ )
50
+ system = connected.body["system"]
51
+ connection = connected.body["connection"]
52
+ vira.observe({
53
+ "eventId": "evt-1", "eventType": "model.response",
54
+ "aiSystemId": system["id"], "connectionId": connection["id"],
55
+ "payload": {"message": "A model response was produced."},
56
+ }, idempotency_key="evt-1")
57
+ semantic = vira.resolve_semantic({"input": "answer a customer billing question"})
58
+ print(semantic.body)
59
+ ```
60
+
61
+ Runtime execution requires a released revision, a valid scope and an idempotency key;
62
+ semantic resolution is not authorization. See [`docs/SDK_GUIDE.md`](../../docs/SDK_GUIDE.md)
63
+ for the shared JS/Python lifecycle.
64
+
65
+ The package metadata is intentionally dependency-free.
66
+
@@ -0,0 +1,52 @@
1
+ # Vira Python SDK
2
+
3
+ Package name: `tryvira-sdk`.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ python -m pip install tryvira-sdk
9
+ ```
10
+
11
+ For a repository checkout use `python -m pip install ./sdks/python`. The client is a thin
12
+ HTTP client of Vira's authority boundary; it does not execute tools locally.
13
+
14
+ For maintainers, build with `python -m build sdks/python`. Public CI uses PyPI Trusted
15
+ Publishing; do not put a token in this repository or in an example.
16
+
17
+ The SDK is a thin client of the Vira authority boundary. It does not execute
18
+ tools or actions locally, invent stable semantic keys, or create a second
19
+ telemetry store. Runtime requests are fail-closed; observation telemetry can be
20
+ configured fail-open with `failure_mode="open"`.
21
+
22
+ ```python
23
+ import os
24
+ from vira import ViraClient
25
+
26
+ with ViraClient(
27
+ "http://127.0.0.1:4185",
28
+ api_key=os.environ.get("VIRA_API_KEY"),
29
+ ) as vira:
30
+ connected = vira.connect_once(
31
+ "my-agent", "custom-agent",
32
+ {"organizationId": "org", "projectId": "project", "environmentId": "local"},
33
+ "custom-python", "1",
34
+ idempotency_key="connect-001",
35
+ )
36
+ system = connected.body["system"]
37
+ connection = connected.body["connection"]
38
+ vira.observe({
39
+ "eventId": "evt-1", "eventType": "model.response",
40
+ "aiSystemId": system["id"], "connectionId": connection["id"],
41
+ "payload": {"message": "A model response was produced."},
42
+ }, idempotency_key="evt-1")
43
+ semantic = vira.resolve_semantic({"input": "answer a customer billing question"})
44
+ print(semantic.body)
45
+ ```
46
+
47
+ Runtime execution requires a released revision, a valid scope and an idempotency key;
48
+ semantic resolution is not authorization. See [`docs/SDK_GUIDE.md`](../../docs/SDK_GUIDE.md)
49
+ for the shared JS/Python lifecycle.
50
+
51
+ The package metadata is intentionally dependency-free.
52
+
@@ -0,0 +1,63 @@
1
+ """Authority-free agent-native observation intents for the Python SDK."""
2
+ from __future__ import annotations
3
+ import hashlib, json
4
+ from typing import Any, Mapping
5
+
6
+ _FORBIDDEN = {'projectId','applicationId','integrationId','userRef','subjectRef','privacyClass','retentionClass','residencyClass','authorityLane','scopeKeys','integrityAttestation'}
7
+ _LIFECYCLE = {'workflow.started','workflow.step','workflow.completed','action.proposed','action.completed','checkpoint.created','compensation.completed'}
8
+ def _canonical(value: Any) -> bytes: return json.dumps(value, sort_keys=True, separators=(',',':'), ensure_ascii=False).encode()
9
+ def _digest(value: Any) -> str: return hashlib.sha256(_canonical(value)).hexdigest()
10
+ def _verify_payload(payload: Mapping[str, Any]) -> None:
11
+ injected=sorted(_FORBIDDEN.intersection(payload.keys()))
12
+ if injected: raise ValueError(f"AGENT_NATIVE_AUTHORITY_FIELD_FORBIDDEN:{','.join(injected)}")
13
+ def build_observation_intent(payload: Mapping[str, Any]) -> dict[str, Any]:
14
+ _verify_payload(payload)
15
+ value={'schemaVersion':2,**dict(payload)}
16
+ for key in ('eventId','kind','occurredAt'):
17
+ if not str(value.get(key,'')).strip(): raise ValueError(f"AGENT_NATIVE_{key.upper()}_REQUIRED")
18
+ if not isinstance(value.get('payload',{}),dict): raise ValueError('AGENT_NATIVE_PAYLOAD_OBJECT_REQUIRED')
19
+ return {**value,'intentDigest':_digest(value)}
20
+ def build_agent_native_observation_intent(payload: Mapping[str, Any]) -> dict[str, Any]: return build_observation_intent(payload)
21
+ def verify_observation_intent(intent: Mapping[str, Any]) -> dict[str, Any]:
22
+ value=dict(intent); _verify_payload(value); received=value.pop('intentDigest',None)
23
+ if not received or _digest(value) != received: raise ValueError('AGENT_NATIVE_OBSERVATION_INTENT_DIGEST_MISMATCH')
24
+ return {**value,'intentDigest':received}
25
+ def verify_agent_native_observation_intent(intent: Mapping[str, Any]) -> dict[str, Any]: return verify_observation_intent(intent)
26
+ def build_lifecycle_intent(kind: str, payload: Mapping[str, Any]) -> dict[str, Any]:
27
+ if kind not in _LIFECYCLE: raise ValueError('BEHAVIOR_LIFECYCLE_KIND_INVALID')
28
+ return build_observation_intent({**dict(payload),'kind':kind})
29
+ def build_behavior_lifecycle_observation_intent(kind: str, payload: Mapping[str, Any]) -> dict[str, Any]: return build_lifecycle_intent(kind, payload)
30
+ def build_workflow_start_intent(payload: Mapping[str, Any]) -> dict[str, Any]: return build_lifecycle_intent('workflow.started', payload)
31
+ def build_workflow_step_intent(payload: Mapping[str, Any]) -> dict[str, Any]: return build_lifecycle_intent('workflow.step', payload)
32
+ def build_workflow_complete_intent(payload: Mapping[str, Any]) -> dict[str, Any]: return build_lifecycle_intent('workflow.completed', payload)
33
+ def build_action_proposed_intent(payload: Mapping[str, Any]) -> dict[str, Any]: return build_lifecycle_intent('action.proposed', payload)
34
+ def build_action_completed_intent(payload: Mapping[str, Any]) -> dict[str, Any]: return build_lifecycle_intent('action.completed', payload)
35
+ def build_checkpoint_created_intent(payload: Mapping[str, Any]) -> dict[str, Any]: return build_lifecycle_intent('checkpoint.created', payload)
36
+ def build_compensation_completed_intent(payload: Mapping[str, Any]) -> dict[str, Any]: return build_lifecycle_intent('compensation.completed', payload)
37
+ def build_preference_intent(payload: Mapping[str, Any]) -> dict[str, Any]: return build_observation_intent({**dict(payload),'kind':'human.preference'})
38
+ def build_handoff_intent(payload: Mapping[str, Any]) -> dict[str, Any]:
39
+ value=dict(payload); parent=value.pop('parentEventId',None); source=value.pop('fromAgentRef',None); target=value.pop('toAgentRef',None)
40
+ if not parent: raise ValueError('AGENT_NATIVE_HANDOFF_PARENT_EVENT_REQUIRED')
41
+ if not source or not target or source == target: raise ValueError('AGENT_NATIVE_HANDOFF_AGENT_IDENTITY_INVALID')
42
+ nested=value.pop('payload',{});
43
+ if not isinstance(nested,dict): raise ValueError('AGENT_NATIVE_PAYLOAD_OBJECT_REQUIRED')
44
+ handoff={'fromAgentRef':source,'toAgentRef':target}
45
+ for key in ('delegationRef','decisionOwnerAgentRef','toolOwnerAgentRef','toolRef'):
46
+ if key in value: handoff[key]=value.pop(key)
47
+ return build_observation_intent({**value,'kind':'agent.handoff','parentEventId':parent,'agentRef':source,'payload':{**nested,'viraTrajectoryV2':{'schemaVersion':2,'handoff':handoff}}})
48
+ def create_online_delivery(*, client_instance_id: str, client_sequence: int, intent: Mapping[str, Any]) -> dict[str, Any]:
49
+ if client_sequence < 0: raise ValueError('AGENT_NATIVE_CLIENT_SEQUENCE_INVALID')
50
+ return {'schemaVersion':2,'clientInstanceId':client_instance_id,'clientSequence':client_sequence,'mode':'online','intent':verify_observation_intent(intent)}
51
+ def offline_signing_digest(*, client_instance_id: str, client_sequence: int, queued_at: str, intent: Mapping[str, Any], key_ref: str, signed_at: str) -> str:
52
+ if client_sequence < 0: raise ValueError('AGENT_NATIVE_CLIENT_SEQUENCE_INVALID')
53
+ verified=verify_observation_intent(intent)
54
+ return _digest({'schemaVersion':2,'clientInstanceId':client_instance_id,'clientSequence':client_sequence,'queuedAt':queued_at,'intentDigest':verified['intentDigest'],'keyRef':key_ref,'signedAt':signed_at})
55
+ def create_offline_delivery(*, client_instance_id: str, client_sequence: int, queued_at: str, intent: Mapping[str, Any], key_ref: str, signed_at: str, signature: str) -> dict[str, Any]:
56
+ if client_sequence < 0: raise ValueError('AGENT_NATIVE_CLIENT_SEQUENCE_INVALID')
57
+ if not key_ref.strip() or not signature.strip(): raise ValueError('AGENT_NATIVE_OFFLINE_PROOF_REQUIRED')
58
+ return {'schemaVersion':2,'clientInstanceId':client_instance_id,'clientSequence':client_sequence,'mode':'offline','queuedAt':queued_at,'intent':verify_observation_intent(intent),'offlineProof':{'scheme':'ed25519','keyRef':key_ref,'signature':signature,'signedAt':signed_at}}
59
+ def create_online_observation_delivery(**kwargs: Any) -> dict[str, Any]: return create_online_delivery(**kwargs)
60
+ def create_offline_observation_delivery(**kwargs: Any) -> dict[str, Any]: return create_offline_delivery(**kwargs)
61
+ def utc_now_iso() -> str:
62
+ from datetime import datetime, timezone
63
+ return datetime.now(timezone.utc).isoformat(timespec='milliseconds').replace('+00:00','Z')
@@ -0,0 +1,10 @@
1
+ from __future__ import annotations
2
+ import hashlib
3
+ import json
4
+ from typing import Any
5
+
6
+ def canonical_json(value: Any) -> str:
7
+ return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False, allow_nan=False)
8
+
9
+ def sha256_canonical(value: Any) -> str:
10
+ return hashlib.sha256(canonical_json(value).encode("utf-8")).hexdigest()
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass
3
+ from typing import Any, Literal
4
+ from digest import sha256_canonical
5
+
6
+ BoundaryFamily = Literal['agent', 'model-step', 'tool', 'action', 'workflow', 'retrieval']
7
+ IdentityStrength = Literal['server-derived', 'adapter-attested', 'registered', 'host-asserted', 'observed-only']
8
+
9
+ @dataclass(frozen=True)
10
+ class ExecutionBoundaryIdentityV2:
11
+ project_id: str
12
+ application_id: str
13
+ environment: Literal['dev', 'staging', 'production']
14
+ integration_id: str
15
+ framework: str
16
+ adapter_id: str
17
+ adapter_version: str
18
+ family: BoundaryFamily
19
+ operation: str
20
+ execution_id: str
21
+ stable_ref: str
22
+ component_type: str
23
+ identity_strength: IdentityStrength
24
+ registration_version: int = 1
25
+ resource: dict[str, str] | None = None
26
+ trajectory_id: str | None = None
27
+ parent_execution_id: str | None = None
28
+
29
+ def __post_init__(self) -> None:
30
+ for field in ('project_id', 'application_id', 'integration_id', 'framework', 'adapter_id', 'adapter_version', 'operation', 'execution_id', 'stable_ref', 'component_type'):
31
+ if not getattr(self, field).strip():
32
+ raise ValueError(f'{field.upper()}_REQUIRED')
33
+ if self.family == 'action' and self.identity_strength == 'observed-only':
34
+ raise ValueError('RUNTIME_IDENTITY_TOO_WEAK_FOR_EFFECT')
35
+ if self.registration_version < 1:
36
+ raise ValueError('REGISTRATION_VERSION_INVALID')
37
+
38
+ def to_dict_without_digest(self) -> dict[str, Any]:
39
+ value: dict[str, Any] = {'projectId': self.project_id, 'applicationId': self.application_id, 'environment': self.environment, 'integration': {'integrationId': self.integration_id, 'framework': self.framework, 'adapterId': self.adapter_id, 'adapterVersion': self.adapter_version}, 'execution': {'family': self.family, 'operation': self.operation, 'executionId': self.execution_id}, 'component': {'stableRef': self.stable_ref, 'componentType': self.component_type, 'identityStrength': self.identity_strength, 'registrationVersion': self.registration_version}}
40
+ if self.resource is not None: value['resource'] = dict(self.resource)
41
+ if self.trajectory_id is not None: value['execution']['trajectoryId'] = self.trajectory_id
42
+ if self.parent_execution_id is not None: value['execution']['parentExecutionId'] = self.parent_execution_id
43
+ return value
44
+
45
+ def to_dict(self) -> dict[str, Any]:
46
+ value = self.to_dict_without_digest()
47
+ value['boundaryDigest'] = sha256_canonical(value)
48
+ return value
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass, field
3
+ from typing import Any, Callable, Literal, Mapping
4
+ from runtime import BehaviorExecutionContext, LifecycleCallback, RuntimeLifecycle
5
+
6
+ PythonFramework = Literal['crewai','pydantic-ai','autogen','semantic-kernel','custom']
7
+ IntegrationMode = Literal['observe','guide','rerank','repair','escalate','strict']
8
+ IntegrationOperation = Literal['generate','stream','tool','action']
9
+ @dataclass(frozen=True)
10
+ class RuntimeIntegrationManifest:
11
+ framework: PythonFramework; adapter_version: str='2'; generate: bool=False; stream: bool=False; tool: bool=False; action: bool=False; stream_enforcement: Literal['unsupported','pass-through','buffer-until-verified']='unsupported'; post_model_finalization: bool=False; tool_interception: bool=False; action_interception: bool=False; signal_attestation: Literal['none','host-asserted','external-verified','server-derived']='none'; trace_provenance: bool=True; model_target: bool=True; projection_artifact: bool=False; unsupported: tuple[str,...]=field(default_factory=tuple)
12
+ def to_dict(self) -> dict[str,object]: return {'schemaVersion':2,'integrationId':f'vira.{self.framework}','framework':self.framework,'adapterVersion':self.adapter_version,'operations':{'generate':self.generate,'stream':self.stream,'tool':self.tool,'action':self.action},'streamEnforcement':self.stream_enforcement,'postModelFinalization':self.post_model_finalization,'toolInterception':self.tool_interception,'actionInterception':self.action_interception,'signalAttestation':self.signal_attestation,'traceProvenance':self.trace_provenance,'modelTarget':self.model_target,'projectionArtifact':self.projection_artifact,'knownUnsupportedGuarantees':list(self.unsupported)}
13
+ def operation_supported(self, operation: IntegrationOperation) -> bool: return {'generate': self.generate, 'stream': self.stream, 'tool': self.tool, 'action': self.action}[operation]
14
+ def assess_runtime_integration_manifest(manifest: Mapping[str,object], *, mode: IntegrationMode, operation: IntegrationOperation, requires_trusted_signals: bool=False) -> dict[str,object]:
15
+ value=dict(manifest); operations=value.get('operations') if isinstance(value.get('operations'),Mapping) else {}; reasons=[]
16
+ if operations.get(operation) is not True: reasons.append(f'operation:{operation}:unsupported')
17
+ if value.get('traceProvenance') is not True: reasons.append('trace-provenance:missing')
18
+ if value.get('modelTarget') is not True: reasons.append('model-target:missing')
19
+ if requires_trusted_signals and value.get('signalAttestation') == 'none': reasons.append('signal-attestation:missing')
20
+ enforcing=mode != 'observe'
21
+ if enforcing and operation not in ('tool','action') and value.get('postModelFinalization') is not True: reasons.append('post-model-finalization:missing')
22
+ if operation == 'stream' and enforcing and value.get('streamEnforcement') != 'buffer-until-verified': reasons.append('stream-buffer-until-verified:missing')
23
+ if operation == 'tool' and enforcing and value.get('toolInterception') is not True: reasons.append('tool-interception:missing')
24
+ if operation == 'action' and enforcing and value.get('actionInterception') is not True: reasons.append('action-interception:missing')
25
+ if mode != 'observe' and value.get('projectionArtifact') is not True: reasons.append('projection-artifact:missing')
26
+ reasons.extend(f'declared-unsupported:{item}' for item in value.get('knownUnsupportedGuarantees',[]) if isinstance(item,str))
27
+ hard=any(reason.startswith(('operation:','trace-provenance:','model-target:','signal-attestation:','post-model-finalization:','stream-buffer-until-verified:','tool-interception:','action-interception:','projection-artifact:')) for reason in reasons)
28
+ status='unsupported' if not operations.get(operation) else 'partial' if hard or (mode != 'observe' and reasons) else 'supported'
29
+ return {'status':status,'enforcementGuaranteed':status == 'supported' and enforcing,'reasons':reasons}
30
+ def assess_runtime_integration(manifest: RuntimeIntegrationManifest|Mapping[str,object], *, mode: IntegrationMode, operation: IntegrationOperation, requires_trusted_signals: bool=False) -> dict[str,object]:
31
+ value = manifest.to_dict() if hasattr(manifest, 'to_dict') else manifest
32
+ return assess_runtime_integration_manifest(value, mode=mode, operation=operation, requires_trusted_signals=requires_trusted_signals)
33
+
34
+ class PythonFrameworkBridge:
35
+ def __init__(self, context: BehaviorExecutionContext, manifest: RuntimeIntegrationManifest, callback: LifecycleCallback | None = None):
36
+ self.context, self.manifest, self.lifecycle = context, manifest, RuntimeLifecycle(context, callback)
37
+ def before_execution(self, payload: dict[str, object]) -> dict[str, object]: self.lifecycle.emit('execution:start', eventType='model.response', framework=self.manifest.framework); return self.lifecycle.resolve_payload(payload)
38
+ def after_execution(self, output: object) -> dict[str, object]: return self.lifecycle.emit('execution:complete', eventType='model.response', framework=self.manifest.framework, output=output)
39
+ def before_tool(self, tool_name: str) -> dict[str, object]: return self.lifecycle.emit('tool:start', eventType='tool.started', framework=self.manifest.framework, toolName=tool_name)
40
+ def after_tool(self, tool_name: str) -> dict[str, object]: return self.lifecycle.emit('tool:complete', eventType='tool.completed', framework=self.manifest.framework, toolName=tool_name)
41
+ def before_action(self, action_name: str) -> dict[str, object]: return self.lifecycle.emit('action:start', eventType='action.proposed', framework=self.manifest.framework, actionName=action_name)
42
+ def after_action(self, action_name: str) -> dict[str, object]: return self.lifecycle.emit('action:complete', eventType='action.executed', framework=self.manifest.framework, actionName=action_name)
43
+
44
+ def create_framework_bridge(framework: PythonFramework, context: BehaviorExecutionContext, *, callback: LifecycleCallback | None = None, manifest: RuntimeIntegrationManifest | None = None) -> PythonFrameworkBridge:
45
+ declared = manifest or RuntimeIntegrationManifest(framework=framework, generate=True, unsupported=('no effect-boundary enforcement is implied until native framework hooks are wired',))
46
+ if declared.framework != framework: raise ValueError('RUNTIME_INTEGRATION_FRAMEWORK_MISMATCH')
47
+ return PythonFrameworkBridge(context, declared, callback)
48
+ def create_crewai_bridge(context: BehaviorExecutionContext, **kwargs: object) -> PythonFrameworkBridge: return create_framework_bridge('crewai', context, **kwargs)
49
+ def create_pydantic_ai_bridge(context: BehaviorExecutionContext, **kwargs: object) -> PythonFrameworkBridge: return create_framework_bridge('pydantic-ai', context, **kwargs)
50
+ def create_autogen_bridge(context: BehaviorExecutionContext, **kwargs: object) -> PythonFrameworkBridge: return create_framework_bridge('autogen', context, **kwargs)
51
+ def create_semantic_kernel_bridge(context: BehaviorExecutionContext, **kwargs: object) -> PythonFrameworkBridge: return create_framework_bridge('semantic-kernel', context, **kwargs)
@@ -0,0 +1,26 @@
1
+ from agent_native import (
2
+ build_action_completed_intent, build_action_proposed_intent,
3
+ build_agent_native_observation_intent, build_behavior_lifecycle_observation_intent,
4
+ build_checkpoint_created_intent, build_compensation_completed_intent,
5
+ build_handoff_intent, build_preference_intent, build_workflow_complete_intent,
6
+ build_workflow_start_intent, build_workflow_step_intent,
7
+ create_offline_observation_delivery, create_online_observation_delivery,
8
+ offline_signing_digest, utc_now_iso, verify_agent_native_observation_intent,
9
+ )
10
+
11
+ def build_action_completed_intent(payload): return __import__('agent_native').build_action_completed_intent(payload)
12
+ def build_action_proposed_intent(payload): return __import__('agent_native').build_action_proposed_intent(payload)
13
+ def build_agent_native_observation_intent(payload): return __import__('agent_native').build_agent_native_observation_intent(payload)
14
+ def build_behavior_lifecycle_observation_intent(kind, payload): return __import__('agent_native').build_behavior_lifecycle_observation_intent(kind, payload)
15
+ def build_checkpoint_created_intent(payload): return __import__('agent_native').build_checkpoint_created_intent(payload)
16
+ def build_compensation_completed_intent(payload): return __import__('agent_native').build_compensation_completed_intent(payload)
17
+ def build_handoff_intent(payload): return __import__('agent_native').build_handoff_intent(payload)
18
+ def build_preference_intent(payload): return __import__('agent_native').build_preference_intent(payload)
19
+ def build_workflow_complete_intent(payload): return __import__('agent_native').build_workflow_complete_intent(payload)
20
+ def build_workflow_start_intent(payload): return __import__('agent_native').build_workflow_start_intent(payload)
21
+ def build_workflow_step_intent(payload): return __import__('agent_native').build_workflow_step_intent(payload)
22
+ def create_offline_observation_delivery(**kwargs): return __import__('agent_native').create_offline_observation_delivery(**kwargs)
23
+ def create_online_observation_delivery(**kwargs): return __import__('agent_native').create_online_observation_delivery(**kwargs)
24
+ def offline_signing_digest(**kwargs): return __import__('agent_native').offline_signing_digest(**kwargs)
25
+ def utc_now_iso(): return __import__('agent_native').utc_now_iso()
26
+ def verify_agent_native_observation_intent(intent): return __import__('agent_native').verify_agent_native_observation_intent(intent)
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "tryvira-sdk"
7
+ version = "0.1.1"
8
+ description = "Typed Python client for the Vira authority boundary"
9
+ requires-python = ">=3.10"
10
+ readme = "README.md"
11
+ license = "Apache-2.0"
12
+ authors = [{ name = "Vira" }]
13
+ keywords = ["ai-governance", "observability", "agent-runtime"]
14
+ classifiers = ["Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only"]
15
+
16
+ [project.urls]
17
+ Repository = "https://github.com/hrpering/vira_beh_redesign"
18
+ Documentation = "https://github.com/hrpering/vira_beh_redesign/tree/main/sdks/python"
19
+
20
+ [tool.setuptools]
21
+ packages = ["vira"]
22
+ include-package-data = true
23
+ py-modules = ["vira_client", "agent_native", "digest", "observation_v2", "execution_boundary_v2", "runtime", "runtime_v2", "integration"]
24
+
25
+ [tool.setuptools.package-data]
26
+ vira = ["py.typed"]
27
+
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass
3
+ from datetime import datetime, timezone
4
+ from typing import Any, Callable, Literal
5
+
6
+ BehaviorExecutionKind = Literal['chat', 'agent', 'copilot', 'workflow']
7
+ BehaviorExecutionOperation = Literal['generate', 'stream', 'tool', 'action']
8
+ ModelProtocol = Literal['openai-chat', 'anthropic-messages', 'generic-chat', 'custom']
9
+
10
+ def _required(value: str, field: str) -> str:
11
+ if not isinstance(value, str) or not value.strip(): raise ValueError(f'{field.upper()}_REQUIRED')
12
+ return value
13
+
14
+ @dataclass(frozen=True)
15
+ class ModelTarget:
16
+ provider: str
17
+ model: str | None = None
18
+ model_id: str | None = None
19
+ model_family: str | None = None
20
+ protocol: ModelProtocol | None = None
21
+ capabilities: dict[str, bool] | None = None
22
+ def __post_init__(self) -> None:
23
+ _required(self.provider, 'model_target_provider')
24
+ identities = [value for value in (self.model, self.model_id, self.model_family) if value is not None]
25
+ if not identities or any(not isinstance(value, str) or not value.strip() for value in identities): raise ValueError('MODEL_TARGET_IDENTITY_REQUIRED')
26
+ if self.model is not None and self.model_id is not None and self.model != self.model_id: raise ValueError('MODEL_TARGET_IDENTITY_CONFLICT')
27
+ if self.protocol is not None and self.protocol not in ('openai-chat', 'anthropic-messages', 'generic-chat', 'custom'): raise ValueError('MODEL_TARGET_PROTOCOL_INVALID')
28
+ if self.capabilities is not None and (set(self.capabilities) - {'tools', 'structuredOutput', 'reasoning', 'streaming'} or any(not isinstance(value, bool) for value in self.capabilities.values())): raise ValueError('MODEL_TARGET_CAPABILITY_INVALID')
29
+ def to_dict(self) -> dict[str, Any]:
30
+ return {key: value for key, value in {'provider': self.provider, 'model': self.model, 'modelId': self.model_id, 'modelFamily': self.model_family, 'protocol': self.protocol, 'capabilities': dict(self.capabilities) if self.capabilities is not None else None}.items() if value is not None}
31
+
32
+ @dataclass(frozen=True)
33
+ class RuntimeHost:
34
+ name: str
35
+ version: str | None = None
36
+ runtime: str | None = None
37
+ def __post_init__(self) -> None: _required(self.name, 'runtime_host_name')
38
+ def to_dict(self) -> dict[str, str]: return {key: value for key, value in {'name': self.name, 'version': self.version, 'runtime': self.runtime}.items() if value is not None}
39
+
40
+ @dataclass(frozen=True)
41
+ class BehaviorExecutionContext:
42
+ kind: BehaviorExecutionKind
43
+ operation: BehaviorExecutionOperation
44
+ host: RuntimeHost
45
+ model_target: ModelTarget
46
+ execution_id: str | None = None
47
+ def __post_init__(self) -> None:
48
+ if self.kind not in ('chat', 'agent', 'copilot', 'workflow'): raise ValueError('BEHAVIOR_EXECUTION_KIND_INVALID')
49
+ if self.operation not in ('generate', 'stream', 'tool', 'action'): raise ValueError('BEHAVIOR_EXECUTION_OPERATION_INVALID')
50
+ def to_dict(self) -> dict[str, Any]:
51
+ return {**{'kind': self.kind, 'operation': self.operation, 'host': self.host.to_dict(), 'modelTarget': self.model_target.to_dict()}, **({'executionId': self.execution_id} if self.execution_id is not None else {})}
52
+
53
+ LifecycleCallback = Callable[[dict[str, Any]], None]
54
+ class RuntimeLifecycle:
55
+ def __init__(self, context: BehaviorExecutionContext, callback: LifecycleCallback | None = None): self.context, self.callback = context, callback
56
+ def emit(self, stage: str, **provenance: Any) -> dict[str, Any]:
57
+ event = {'stage': stage, 'context': self.context.to_dict(), **provenance, 'at': datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z')}
58
+ if self.callback: self.callback(event)
59
+ return event
60
+ def resolve_payload(self, payload: dict[str, Any]) -> dict[str, Any]: return {**payload, 'executionContext': self.context.to_dict()}
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+ import re
3
+ from typing import Any, Mapping
4
+
5
+ _DIGEST = re.compile(r'^[0-9a-f]{64}$')
6
+ _REQUIRED_PLAN = {'schemaVersion', 'traceId', 'revisions', 'effectiveProgramDigest', 'artifactDigests', 'capsuleDigest', 'matchedModuleKeys', 'preModel', 'toolPolicy', 'postModel', 'execution', 'provenance'}
7
+ _REQUIRED_ENVELOPE = {'schemaVersion', 'legacyProjection', 'executionPlanV2', 'modelTarget', 'projectionArtifact', 'integration'}
8
+
9
+ class ViraRuntimeCompatibilityError(ValueError):
10
+ def __init__(self, code: str, message: str, details: Mapping[str, Any] | None = None):
11
+ super().__init__(message); self.code, self.details = code, dict(details or {})
12
+
13
+ def _invalid(message: str, field: str) -> ViraRuntimeCompatibilityError:
14
+ return ViraRuntimeCompatibilityError('VIRA_RUNTIME_V2_ENVELOPE_INVALID', message, {'field': field})
15
+ def _object(value: Any, field: str) -> Mapping[str, Any]:
16
+ if not isinstance(value, Mapping): raise _invalid(f'{field} must be an object', field)
17
+ return value
18
+ def _required_string(value: Any, field: str) -> str:
19
+ if not isinstance(value, str) or not value.strip(): raise _invalid(f'{field} is required', field)
20
+ return value
21
+ def _digest(value: Any, field: str) -> str:
22
+ result = _required_string(value, field)
23
+ if not _DIGEST.fullmatch(result): raise _invalid(f'{field} must be a lowercase sha256 digest', field)
24
+ return result
25
+
26
+ def parse_runtime_v2_compatibility_envelope(input: Any) -> dict[str, Any]:
27
+ envelope = _object(input, 'envelope')
28
+ missing = sorted(_REQUIRED_ENVELOPE - set(envelope))
29
+ if missing: raise _invalid(f'envelope is missing required fields: {", ".join(missing)}', 'envelope')
30
+ if envelope['schemaVersion'] != 2: raise _invalid('schemaVersion must be 2', 'schemaVersion')
31
+ _required_string(envelope['legacyProjection'], 'legacyProjection')
32
+ plan = _object(envelope['executionPlanV2'], 'executionPlanV2')
33
+ plan_missing = sorted(_REQUIRED_PLAN - set(plan))
34
+ if plan_missing: raise _invalid(f'executionPlanV2 is missing required fields: {", ".join(plan_missing)}', 'executionPlanV2')
35
+ if plan['schemaVersion'] != 2: raise _invalid('executionPlanV2.schemaVersion must be 2', 'executionPlanV2.schemaVersion')
36
+ _required_string(plan['traceId'], 'executionPlanV2.traceId')
37
+ _digest(plan['effectiveProgramDigest'], 'executionPlanV2.effectiveProgramDigest')
38
+ _digest(plan['capsuleDigest'], 'executionPlanV2.capsuleDigest')
39
+ for key in ('artifactDigests', 'matchedModuleKeys'):
40
+ if not isinstance(plan[key], list): raise _invalid(f'executionPlanV2.{key} must be a list', f'executionPlanV2.{key}')
41
+ _object(plan['preModel'], 'executionPlanV2.preModel'); _object(plan['toolPolicy'], 'executionPlanV2.toolPolicy'); _object(plan['postModel'], 'executionPlanV2.postModel'); _object(plan['execution'], 'executionPlanV2.execution'); _object(plan['provenance'], 'executionPlanV2.provenance')
42
+ _object(envelope['modelTarget'], 'modelTarget'); integration = _object(envelope['integration'], 'integration')
43
+ if 'manifest' in integration:
44
+ manifest = _object(integration['manifest'], 'integration.manifest')
45
+ for key in ('operations', 'traceProvenance', 'modelTarget', 'postModelFinalization', 'knownUnsupportedGuarantees'):
46
+ if key not in manifest: raise _invalid(f'integration.manifest is missing {key}', f'integration.manifest.{key}')
47
+ if not isinstance(manifest['operations'], Mapping) or not isinstance(manifest['knownUnsupportedGuarantees'], list): raise _invalid('integration.manifest capability shape is invalid', 'integration.manifest')
48
+ assessment = _object(integration.get('assessment'), 'integration.assessment')
49
+ if assessment.get('enforcementGuaranteed') is True and manifest.get('postModelFinalization') is not True: raise _invalid('integration assessment is forged', 'integration.assessment')
50
+ interoperability = integration.get('interoperability') or manifest.get('interoperability')
51
+ if isinstance(interoperability, Mapping) and interoperability.get('mode') == 'lossy' and not any(isinstance(item, str) and 'lossy' in item for item in manifest['knownUnsupportedGuarantees']): raise _invalid('lossy interoperability requires an explicit unsupported guarantee', 'integration.manifest.knownUnsupportedGuarantees')
52
+ if envelope['projectionArtifact'] is not None: _object(envelope['projectionArtifact'], 'projectionArtifact')
53
+ return dict(envelope)
54
+
55
+ def require_runtime_v2_enforcement(input: Any) -> dict[str, Any]:
56
+ envelope = parse_runtime_v2_compatibility_envelope(input)
57
+ integration = envelope['integration']; assessment = integration.get('assessment') if isinstance(integration, Mapping) else None
58
+ guaranteed = assessment.get('enforcementGuaranteed') if isinstance(assessment, Mapping) else integration.get('enforcementGuaranteed')
59
+ if guaranteed is not True: raise ViraRuntimeCompatibilityError('VIRA_RUNTIME_V2_ENFORCEMENT_UNAVAILABLE', 'Runtime V2 enforcement was not negotiated', {'status': assessment.get('status') if isinstance(assessment, Mapping) else 'partial'})
60
+ return envelope
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.4
2
+ Name: tryvira-sdk
3
+ Version: 0.1.1
4
+ Summary: Typed Python client for the Vira authority boundary
5
+ Author: Vira
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Repository, https://github.com/hrpering/vira_beh_redesign
8
+ Project-URL: Documentation, https://github.com/hrpering/vira_beh_redesign/tree/main/sdks/python
9
+ Keywords: ai-governance,observability,agent-runtime
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+
15
+ # Vira Python SDK
16
+
17
+ Package name: `tryvira-sdk`.
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ python -m pip install tryvira-sdk
23
+ ```
24
+
25
+ For a repository checkout use `python -m pip install ./sdks/python`. The client is a thin
26
+ HTTP client of Vira's authority boundary; it does not execute tools locally.
27
+
28
+ For maintainers, build with `python -m build sdks/python`. Public CI uses PyPI Trusted
29
+ Publishing; do not put a token in this repository or in an example.
30
+
31
+ The SDK is a thin client of the Vira authority boundary. It does not execute
32
+ tools or actions locally, invent stable semantic keys, or create a second
33
+ telemetry store. Runtime requests are fail-closed; observation telemetry can be
34
+ configured fail-open with `failure_mode="open"`.
35
+
36
+ ```python
37
+ import os
38
+ from vira import ViraClient
39
+
40
+ with ViraClient(
41
+ "http://127.0.0.1:4185",
42
+ api_key=os.environ.get("VIRA_API_KEY"),
43
+ ) as vira:
44
+ connected = vira.connect_once(
45
+ "my-agent", "custom-agent",
46
+ {"organizationId": "org", "projectId": "project", "environmentId": "local"},
47
+ "custom-python", "1",
48
+ idempotency_key="connect-001",
49
+ )
50
+ system = connected.body["system"]
51
+ connection = connected.body["connection"]
52
+ vira.observe({
53
+ "eventId": "evt-1", "eventType": "model.response",
54
+ "aiSystemId": system["id"], "connectionId": connection["id"],
55
+ "payload": {"message": "A model response was produced."},
56
+ }, idempotency_key="evt-1")
57
+ semantic = vira.resolve_semantic({"input": "answer a customer billing question"})
58
+ print(semantic.body)
59
+ ```
60
+
61
+ Runtime execution requires a released revision, a valid scope and an idempotency key;
62
+ semantic resolution is not authorization. See [`docs/SDK_GUIDE.md`](../../docs/SDK_GUIDE.md)
63
+ for the shared JS/Python lifecycle.
64
+
65
+ The package metadata is intentionally dependency-free.
66
+
@@ -0,0 +1,16 @@
1
+ README.md
2
+ agent_native.py
3
+ digest.py
4
+ execution_boundary_v2.py
5
+ integration.py
6
+ observation_v2.py
7
+ pyproject.toml
8
+ runtime.py
9
+ runtime_v2.py
10
+ vira_client.py
11
+ tryvira_sdk.egg-info/PKG-INFO
12
+ tryvira_sdk.egg-info/SOURCES.txt
13
+ tryvira_sdk.egg-info/dependency_links.txt
14
+ tryvira_sdk.egg-info/top_level.txt
15
+ vira/__init__.py
16
+ vira/py.typed
@@ -0,0 +1,9 @@
1
+ agent_native
2
+ digest
3
+ execution_boundary_v2
4
+ integration
5
+ observation_v2
6
+ runtime
7
+ runtime_v2
8
+ vira
9
+ vira_client
@@ -0,0 +1,32 @@
1
+ """Packaged Python SDK facade for Vira."""
2
+
3
+ from vira_client import ViraApiError, ViraClient, ViraResponse
4
+ from agent_native import (
5
+ build_observation_intent,
6
+ build_lifecycle_intent,
7
+ build_handoff_intent,
8
+ create_online_delivery,
9
+ create_offline_delivery,
10
+ verify_observation_intent,
11
+ build_agent_native_observation_intent, build_behavior_lifecycle_observation_intent,
12
+ build_preference_intent, build_workflow_start_intent, build_workflow_step_intent,
13
+ build_workflow_complete_intent, build_action_proposed_intent, build_action_completed_intent,
14
+ build_checkpoint_created_intent, build_compensation_completed_intent,
15
+ create_online_observation_delivery, create_offline_observation_delivery,
16
+ offline_signing_digest, utc_now_iso, verify_agent_native_observation_intent,
17
+ )
18
+ from digest import canonical_json, sha256_canonical
19
+ from runtime import BehaviorExecutionContext, ModelTarget, RuntimeHost, RuntimeLifecycle
20
+ from integration import PythonFrameworkBridge, RuntimeIntegrationManifest, create_framework_bridge
21
+ from execution_boundary_v2 import ExecutionBoundaryIdentityV2
22
+ from runtime_v2 import ViraRuntimeCompatibilityError, parse_runtime_v2_compatibility_envelope, require_runtime_v2_enforcement
23
+
24
+ __all__ = [
25
+ "ViraApiError", "ViraClient", "ViraResponse",
26
+ "build_observation_intent", "build_lifecycle_intent", "build_handoff_intent", "build_agent_native_observation_intent",
27
+ "build_behavior_lifecycle_observation_intent", "build_preference_intent", "build_workflow_start_intent", "build_workflow_step_intent", "build_workflow_complete_intent",
28
+ "build_action_proposed_intent", "build_action_completed_intent", "build_checkpoint_created_intent", "build_compensation_completed_intent",
29
+ "create_online_delivery", "create_offline_delivery", "create_online_observation_delivery", "create_offline_observation_delivery", "offline_signing_digest", "utc_now_iso", "verify_observation_intent", "verify_agent_native_observation_intent",
30
+ "canonical_json", "sha256_canonical", "BehaviorExecutionContext", "ModelTarget", "RuntimeHost", "RuntimeLifecycle", "PythonFrameworkBridge", "RuntimeIntegrationManifest", "create_framework_bridge", "ExecutionBoundaryIdentityV2", "ViraRuntimeCompatibilityError", "parse_runtime_v2_compatibility_envelope", "require_runtime_v2_enforcement",
31
+ ]
32
+
File without changes
@@ -0,0 +1,386 @@
1
+ """Typed Python SDK boundary for the versioned Vira control API.
2
+
3
+ The client deliberately reuses the same admitted-observation authority as the
4
+ JavaScript SDK; it does not create a second telemetry or learning store.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+ import json
10
+ import time
11
+ from typing import Any, Callable, Mapping, Sequence
12
+ from urllib.error import HTTPError, URLError
13
+ from urllib.request import Request, urlopen
14
+
15
+ Json = Any
16
+ Transport = Callable[[str, str, Mapping[str, str], bytes | None, float], tuple[int, Mapping[str, str], bytes]]
17
+
18
+
19
+ class ViraApiError(RuntimeError):
20
+ def __init__(self, status: int, body: Json, request_id: str = "") -> None:
21
+ super().__init__(f"VIRA_API_{status}")
22
+ self.status, self.body, self.request_id = status, body, request_id
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class ViraResponse:
27
+ status: int
28
+ body: Json
29
+ request_id: str = ""
30
+
31
+
32
+ def _default_transport(url: str, method: str, headers: Mapping[str, str], body: bytes | None, timeout: float) -> tuple[int, Mapping[str, str], bytes]:
33
+ try:
34
+ with urlopen(Request(url, data=body, headers=dict(headers), method=method), timeout=timeout) as response:
35
+ return response.status, response.headers, response.read()
36
+ except HTTPError as error:
37
+ return error.code, error.headers, error.read()
38
+
39
+
40
+ class ViraClient:
41
+ def __init__(self, base_url: str, *, api_key: str | None = None, timeout: float = 15.0, retries: int = 2,
42
+ failure_mode: str = "throw", transport: Transport | None = None) -> None:
43
+ if not base_url.strip(): raise ValueError("SDK_BASE_URL_REQUIRED")
44
+ if failure_mode not in {"throw", "open"}: raise ValueError("SDK_FAILURE_MODE_INVALID")
45
+ self.base_url, self.api_key = base_url.rstrip("/"), api_key
46
+ self.timeout, self.retries, self.failure_mode = max(1.0, min(timeout, 120.0)), max(0, min(retries, 5)), failure_mode
47
+ self.transport = transport or _default_transport
48
+
49
+ def _request(self, method: str, path: str, *, query: Mapping[str, str] | None = None,
50
+ body: Mapping[str, Json] | None = None, idempotency_key: str | None = None,
51
+ enforcing: bool = False) -> ViraResponse | None:
52
+ from urllib.parse import urlencode
53
+ url = self.base_url + path + (("?" + urlencode(query)) if query else "")
54
+ headers = {"accept": "application/json"}
55
+ if self.api_key: headers["authorization"] = f"Bearer {self.api_key}"
56
+ if idempotency_key: headers["idempotency-key"] = idempotency_key
57
+ encoded = json.dumps(body, separators=(",", ":")).encode() if body is not None else None
58
+ if encoded is not None: headers["content-type"] = "application/json"
59
+ for attempt in range(self.retries + 1):
60
+ try:
61
+ status, response_headers, raw = self.transport(url, method, headers, encoded, self.timeout)
62
+ try: parsed = json.loads(raw) if raw else None
63
+ except json.JSONDecodeError: parsed = raw.decode(errors="replace")
64
+ request_id = response_headers.get("x-request-id", "")
65
+ if status >= 400:
66
+ if status >= 500 and attempt < self.retries: time.sleep(0.025 * (2 ** attempt)); continue
67
+ if self.failure_mode == "open" and not enforcing and status >= 500: return None
68
+ raise ViraApiError(status, parsed, request_id)
69
+ return ViraResponse(status, parsed, request_id)
70
+ except (TimeoutError, URLError, OSError) as error:
71
+ if attempt < self.retries: time.sleep(0.025 * (2 ** attempt)); continue
72
+ if self.failure_mode == "open" and not enforcing: return None
73
+ raise error
74
+ raise RuntimeError("SDK_REQUEST_EXHAUSTED")
75
+
76
+ def connect(self, name: str, system_type: str, scope: Mapping[str, str], *, idempotency_key: str | None = None) -> ViraResponse | None:
77
+ return self._request("POST", "/v1/ai-systems", body={"name": name, "systemType": system_type, **dict(scope)}, idempotency_key=idempotency_key)
78
+
79
+ def connect_adapter(self, system_id: str, adapter_type: str, adapter_version: str, *, capabilities: list[str] | None = None, config: Json | None = None, idempotency_key: str | None = None) -> ViraResponse | None:
80
+ if not str(system_id).strip(): raise ValueError("SDK_SYSTEM_ID_REQUIRED")
81
+ return self._request("POST", f"/v1/ai-systems/{system_id}/connections", body={"adapterType": adapter_type, "adapterVersion": adapter_version, "capabilities": list(capabilities or []), "config": config or {}}, idempotency_key=idempotency_key)
82
+
83
+ def connect_once(self, name: str, system_type: str, scope: Mapping[str, str], adapter_type: str, adapter_version: str, *, capabilities: list[str] | None = None, config: Json | None = None, idempotency_key: str | None = None) -> ViraResponse | None:
84
+ system = self.connect(name, system_type, scope, idempotency_key=idempotency_key)
85
+ if system is None: return None
86
+ system_id = str(system.body.get("system", {}).get("id", ""))
87
+ connection = self.connect_adapter(system_id, adapter_type, adapter_version, capabilities=capabilities, config=config, idempotency_key=idempotency_key)
88
+ if connection is None: return None
89
+ return ViraResponse(connection.status, {"system": system.body.get("system"), "connection": connection.body.get("connection"), "handshake": connection.body.get("handshake")}, connection.request_id)
90
+
91
+ def observe(self, event: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
92
+ required = ("eventId", "eventType", "aiSystemId", "connectionId")
93
+ if any(not str(event.get(key, "")).strip() for key in required): raise ValueError("SDK_OBSERVATION_FIELD_REQUIRED")
94
+ return self._request("POST", "/v1/observations", body=dict(event), idempotency_key=idempotency_key or str(event["eventId"]))
95
+
96
+ def observe_batch(self, events: list[Mapping[str, Json]], *, idempotency_key: str | None = None) -> ViraResponse | None:
97
+ if not 1 <= len(events) <= 100: raise ValueError("SDK_OBSERVATION_BATCH_INVALID")
98
+ return self._request("POST", "/v1/observations/batch", body={"events": events}, idempotency_key=idempotency_key)
99
+
100
+ def trace(self, event: Mapping[str, Json], **kwargs: Any) -> ViraResponse | None: return self.observe({**event, "eventType": "model.response"}, **kwargs)
101
+ def outcome(self, event: Mapping[str, Json], **kwargs: Any) -> ViraResponse | None: return self.observe({**event, "eventType": "business.outcome"}, **kwargs)
102
+ def feedback(self, event: Mapping[str, Json], **kwargs: Any) -> ViraResponse | None: return self.observe({**event, "eventType": "feedback.user"}, **kwargs)
103
+
104
+ def lifecycle(self, kind: str, payload: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
105
+ """Emit an agent-native lifecycle observation with a client-owned digest."""
106
+ from agent_native import build_lifecycle_intent, create_online_delivery
107
+ intent = build_lifecycle_intent(kind, payload)
108
+ return self._request("POST", "/v1/observations/sdk", body=create_online_delivery(
109
+ client_instance_id=str(payload.get("clientInstanceId", "python-sdk")),
110
+ client_sequence=int(payload.get("clientSequence", 0)), intent=intent,
111
+ ), idempotency_key=idempotency_key or str(intent["eventId"]))
112
+
113
+ def preference(self, payload: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
114
+ return self.lifecycle("workflow.step", {**payload, "payload": {"preference": dict(payload)}} , idempotency_key=idempotency_key)
115
+
116
+ def handoff(self, payload: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
117
+ """Admit an agent handoff as an authority-free observation intent."""
118
+ from agent_native import build_handoff_intent, create_online_delivery
119
+ intent = build_handoff_intent(payload)
120
+ return self._request("POST", "/v1/observations/sdk", body=create_online_delivery(
121
+ client_instance_id=str(payload.get("clientInstanceId", "python-sdk")),
122
+ client_sequence=int(payload.get("clientSequence", 0)), intent=intent,
123
+ ), idempotency_key=idempotency_key or str(intent["eventId"]))
124
+
125
+ def workflow_start(self, payload: Mapping[str, Json], **kwargs: Any) -> ViraResponse | None:
126
+ return self.lifecycle("workflow.started", payload, **kwargs)
127
+
128
+ def workflow_step(self, payload: Mapping[str, Json], **kwargs: Any) -> ViraResponse | None:
129
+ return self.lifecycle("workflow.step", payload, **kwargs)
130
+
131
+ def workflow_complete(self, payload: Mapping[str, Json], **kwargs: Any) -> ViraResponse | None:
132
+ return self.lifecycle("workflow.completed", payload, **kwargs)
133
+
134
+ def action_proposed(self, payload: Mapping[str, Json], **kwargs: Any) -> ViraResponse | None:
135
+ return self.lifecycle("action.proposed", payload, **kwargs)
136
+
137
+ def action_completed(self, payload: Mapping[str, Json], **kwargs: Any) -> ViraResponse | None:
138
+ return self.lifecycle("action.completed", payload, **kwargs)
139
+
140
+ def checkpoint_created(self, payload: Mapping[str, Json], **kwargs: Any) -> ViraResponse | None:
141
+ return self.lifecycle("checkpoint.created", payload, **kwargs)
142
+
143
+ def compensation_completed(self, payload: Mapping[str, Json], **kwargs: Any) -> ViraResponse | None:
144
+ return self.lifecycle("compensation.completed", payload, **kwargs)
145
+
146
+ def execute_runtime(self, request: Mapping[str, Json]) -> ViraResponse | None:
147
+ return self._request("POST", "/v1/runtime/execute", body=dict(request), idempotency_key=str(request.get("idempotencyKey", "")), enforcing=True)
148
+
149
+ def evaluate_quality(self, request: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
150
+ """Rank bounded chatbot candidates through Vira's quality/JEV gate."""
151
+ return self._request("POST", "/v1/quality/evaluate", body=dict(request), idempotency_key=idempotency_key, enforcing=True)
152
+
153
+ def create_signal_discovery_proposal(self, request: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
154
+ return self._request("POST", "/v1/signals/discovery-proposals", body=dict(request), idempotency_key=idempotency_key or str(request.get("signalStableKey", "")))
155
+
156
+ def review_signal_discovery_proposal(self, proposal_id: str, decision: str, *, idempotency_key: str | None = None) -> ViraResponse | None:
157
+ if not proposal_id.strip(): raise ValueError("SDK_SIGNAL_PROPOSAL_ID_REQUIRED")
158
+ if decision not in {"APPROVE", "REJECT"}: raise ValueError("SDK_SIGNAL_DECISION_INVALID")
159
+ return self._request("POST", f"/v1/signals/discovery-proposals/{proposal_id}/review", body={"decision": decision}, idempotency_key=idempotency_key or f"signal-review:{proposal_id}")
160
+
161
+ def materialize_signal_discovery(self, proposal_id: str, request: Mapping[str, Json] | None = None, *, idempotency_key: str | None = None) -> ViraResponse | None:
162
+ if not proposal_id.strip(): raise ValueError("SDK_SIGNAL_PROPOSAL_ID_REQUIRED")
163
+ return self._request("POST", f"/v1/signals/discovery-proposals/{proposal_id}/materialize", body=dict(request or {}), idempotency_key=idempotency_key or f"signal-materialize:{proposal_id}")
164
+
165
+ def create_signal_mapping(self, request: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
166
+ return self._request("POST", "/v1/signals/mappings", body=dict(request), idempotency_key=idempotency_key or str(request.get("signalStableKey", "")))
167
+
168
+ def get_signal_mapping(self, mapping_id: str) -> ViraResponse | None:
169
+ if not mapping_id.strip(): raise ValueError("SDK_SIGNAL_MAPPING_ID_REQUIRED")
170
+ return self._request("GET", f"/v1/signals/mappings/{mapping_id}")
171
+
172
+ def review_signal_mapping(self, mapping_id: str, request: Mapping[str, Json] | None = None, *, idempotency_key: str | None = None) -> ViraResponse | None:
173
+ if not mapping_id.strip(): raise ValueError("SDK_SIGNAL_MAPPING_ID_REQUIRED")
174
+ return self._request("POST", f"/v1/signals/mappings/{mapping_id}/review", body=dict(request or {}), idempotency_key=idempotency_key or f"signal-mapping-review:{mapping_id}")
175
+
176
+ def compile_behavior_capsule(self, request: Mapping[str, Json]) -> ViraResponse | None:
177
+ if not str(request.get("programId", "")).strip() or not str(request.get("input", "")).strip():
178
+ raise ValueError("SDK_BEHAVIOR_CAPSULE_INPUT_REQUIRED")
179
+ return self._request("POST", "/v1/runtime/behavior-capsule", body=dict(request), idempotency_key=str(request.get("idempotencyKey", "")))
180
+
181
+ def serve_behavior_projection(self, request: Mapping[str, Json]) -> ViraResponse | None:
182
+ if not str(request.get("input", "")).strip() or not str(request.get("provider", "")).strip():
183
+ raise ValueError("SDK_BEHAVIOR_PROJECTION_INPUT_REQUIRED")
184
+ return self._request("POST", "/v1/runtime/behavior-projection", body=dict(request), idempotency_key=str(request.get("idempotencyKey", "")))
185
+
186
+ def resolve(self, request: Mapping[str, Json]) -> ViraResponse | None:
187
+ """Backward-compatible alias for the explicit runtime resolver."""
188
+ return self.resolve_runtime(request)
189
+
190
+ def resolve_runtime(self, request: Mapping[str, Json]) -> ViraResponse | None:
191
+ """Resolve a persisted route through the governed runtime boundary.
192
+
193
+ A caller may provide either an explicit behaviorPoint or a canonical
194
+ semanticContext produced by semantic activation. The latter is the
195
+ boundary-only path used by framework integrations.
196
+ """
197
+ boundary = request.get("executionBoundary")
198
+ nested = boundary.get("behaviorPoint") if isinstance(boundary, Mapping) else None
199
+ semantic = request.get("semanticContext")
200
+ selected = semantic.get("behaviorPoint") if isinstance(semantic, Mapping) else None
201
+ if not str(request.get("behaviorPoint") or nested or selected or "").strip(): raise ValueError("SDK_RUNTIME_ROUTE_REQUIRED")
202
+ return self._request("POST", "/v1/runtime/resolve", body=dict(request), idempotency_key=str(request.get("idempotencyKey", "")), enforcing=True)
203
+
204
+ def resolve_boundary(self, execution_boundary: Mapping[str, Json], *, semantic_context: Mapping[str, Json] | None = None, idempotency_key: str | None = None) -> ViraResponse | None:
205
+ """Resolve a route without requiring a caller-selected BehaviorPoint."""
206
+ if not isinstance(execution_boundary, Mapping) or not execution_boundary:
207
+ raise ValueError("SDK_EXECUTION_BOUNDARY_REQUIRED")
208
+ body: dict[str, Json] = {"executionBoundary": dict(execution_boundary)}
209
+ if semantic_context is not None: body["semanticContext"] = dict(semantic_context)
210
+ if idempotency_key: body["idempotencyKey"] = idempotency_key
211
+ return self.resolve_runtime(body)
212
+
213
+ def resolve_semantic(self, request: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
214
+ if not str(request.get("input", "")).strip(): raise ValueError("SDK_SEMANTIC_INPUT_REQUIRED")
215
+ return self._request("POST", "/v1/semantic/resolve", body=dict(request), idempotency_key=idempotency_key or f"sdk:semantic:{request['input']}")
216
+
217
+ def resolve_semantic_runtime(self, request: Mapping[str, Json]) -> ViraResponse | None:
218
+ """Resolve free-form user intent, then resolve its governed runtime behavior."""
219
+ if not str(request.get("input", "")).strip(): raise ValueError("SDK_SEMANTIC_INPUT_REQUIRED")
220
+ semantic = self.resolve_semantic(request, idempotency_key=str(request.get("idempotencyKey", "")) or None)
221
+ if not semantic: return None
222
+ body = semantic.body if isinstance(semantic.body, Mapping) else {}
223
+ receipt = body.get("receipt") if isinstance(body.get("receipt"), Mapping) else {}
224
+ if receipt.get("outcome") != "select" or not str(receipt.get("taskKey", "")).strip():
225
+ return ViraResponse(status=semantic.status, body={"semantic": semantic.body, "runtime": None}, request_id=semantic.request_id)
226
+ capsules = body.get("capsules") if isinstance(body.get("capsules"), list) else []
227
+ capsule = capsules[0] if capsules and isinstance(capsules[0], Mapping) else {}
228
+ behavior_point = str(capsule.get("behaviorKey") or receipt.get("taskKey") or "").strip()
229
+ if not behavior_point:
230
+ return ViraResponse(status=semantic.status, body={"semantic": semantic.body, "runtime": None}, request_id=semantic.request_id)
231
+ runtime_request: dict[str, Json] = {"semanticContext": {"taskKey": str(receipt.get("taskKey")), "behaviorPoint": behavior_point, "capsule": dict(capsule)}, "idempotencyKey": f"sdk:runtime:{request.get('idempotencyKey') or receipt.get('requestId') or request['input']}"}
232
+ if isinstance(request.get("executionBoundary"), Mapping): runtime_request["executionBoundary"] = dict(request["executionBoundary"])
233
+ runtime = self.resolve_runtime(runtime_request)
234
+ return ViraResponse(status=runtime.status, body={"semantic": semantic.body, "runtime": runtime.body}, request_id=runtime.request_id)
235
+
236
+ def list_behavior_programs(self) -> ViraResponse | None: return self._request("GET", "/v1/behavior-programs")
237
+ def list_behavior_strategies(self) -> ViraResponse | None: return self._request("GET", "/v1/behavior-strategies")
238
+ def get_behavior_strategy(self, strategy_id: str) -> ViraResponse | None:
239
+ if not strategy_id.strip(): raise ValueError("SDK_STRATEGY_ID_REQUIRED")
240
+ from urllib.parse import quote
241
+ return self._request("GET", f"/v1/behavior-strategies/{quote(strategy_id, safe='')}")
242
+ def create_behavior_strategy(self, definition: Json, *, strategy_id: str | None = None) -> ViraResponse | None:
243
+ if not definition: raise ValueError("SDK_STRATEGY_DEFINITION_REQUIRED")
244
+ return self._request("POST", "/v1/behavior-strategies", body={"definition": definition, **({"id": strategy_id} if strategy_id else {})})
245
+ def validate_behavior_strategy(self, strategy_id: str) -> ViraResponse | None:
246
+ if not strategy_id.strip(): raise ValueError("SDK_STRATEGY_ID_REQUIRED")
247
+ from urllib.parse import quote
248
+ return self._request("POST", f"/v1/behavior-strategies/{quote(strategy_id, safe='')}/validate", body={})
249
+ def simulate_behavior_strategy(self, strategy_id: str, initial_state: Mapping[str, Json] | None = None) -> ViraResponse | None:
250
+ if not strategy_id.strip(): raise ValueError("SDK_STRATEGY_ID_REQUIRED")
251
+ from urllib.parse import quote
252
+ return self._request("POST", f"/v1/behavior-strategies/{quote(strategy_id, safe='')}/simulate", body={"initialState": dict(initial_state or {})})
253
+ def start_strategy_execution(self, artifact_digest: str, *, organization_id: str, project_id: str, environment_id: str, revision_id: str, idempotency_key: str, nonce: str, expires_at: int | float, decision_plan_id: str, candidates: Sequence[Mapping[str, Json]], policy: str, behavior_point: str | None = None, execution_boundary: Mapping[str, Json] | None = None, allowed_ids: Sequence[str] | None = None, calibration_id: str | None = None, post_model_judge: Json | None = None, shadow: Json | None = None, trace_id: str | None = None, execution_id: str | None = None, state: Mapping[str, Json] | None = None) -> ViraResponse | None:
254
+ if not artifact_digest.strip(): raise ValueError("SDK_ARTIFACT_DIGEST_REQUIRED")
255
+ for value in (organization_id, project_id, environment_id, revision_id, idempotency_key, nonce, decision_plan_id, policy):
256
+ if not isinstance(value, str) or not value.strip(): raise ValueError("SDK_STRATEGY_START_FIELD_REQUIRED")
257
+ if not isinstance(expires_at, (int, float)): raise ValueError("SDK_EXPIRY_REQUIRED")
258
+ if not candidates: raise ValueError("SDK_STRATEGY_CANDIDATES_REQUIRED")
259
+ if behavior_point is not None and not behavior_point.strip(): raise ValueError("SDK_BEHAVIOR_POINT_REQUIRED")
260
+ body: Json = {"artifactDigest": artifact_digest, "organizationId": organization_id, "projectId": project_id, "environmentId": environment_id, "revisionId": revision_id, "idempotencyKey": idempotency_key, "nonce": nonce, "expiresAt": expires_at, "decisionPlanId": decision_plan_id, "candidates": [dict(candidate) for candidate in candidates], "policy": policy, **({"behaviorPoint": behavior_point} if behavior_point else {}), **({"executionBoundary": dict(execution_boundary)} if execution_boundary is not None else {}), **({"allowedIds": list(allowed_ids)} if allowed_ids is not None else {}), **({"calibrationId": calibration_id} if calibration_id else {}), **({"postModelJudge": post_model_judge} if post_model_judge is not None else {}), **({"shadow": shadow} if shadow is not None else {}), **({"traceId": trace_id} if trace_id else {}), **({"executionId": execution_id} if execution_id else {}), **({"state": dict(state)} if state is not None else {})}
261
+ return self._request("POST", "/v1/runtime/strategy-executions", body=body, idempotency_key=idempotency_key)
262
+ def get_strategy_execution(self, execution_id: str) -> ViraResponse | None:
263
+ if not execution_id.strip(): raise ValueError("SDK_EXECUTION_ID_REQUIRED")
264
+ from urllib.parse import quote
265
+ return self._request("GET", f"/v1/runtime/strategy-executions/{quote(execution_id, safe='')}")
266
+ def get_strategy_execution_trace(self, execution_id: str) -> ViraResponse | None:
267
+ if not execution_id.strip(): raise ValueError("SDK_EXECUTION_ID_REQUIRED")
268
+ from urllib.parse import quote
269
+ return self._request("GET", f"/v1/runtime/strategy-executions/{quote(execution_id, safe='')}/trace")
270
+ def continue_strategy_execution(self, execution_id: str, authority_digest: str | None = None, *, state: Mapping[str, Json] | None = None, references: Mapping[str, Json] | None = None) -> ViraResponse | None:
271
+ if not execution_id.strip(): raise ValueError("SDK_EXECUTION_ID_REQUIRED")
272
+ from urllib.parse import quote
273
+ body: dict[str, Json] = {}
274
+ if authority_digest: body["authorityDigest"] = authority_digest
275
+ if state is not None: body["state"] = dict(state)
276
+ if references is not None: body["references"] = dict(references)
277
+ return self._request("POST", f"/v1/runtime/strategy-executions/{quote(execution_id, safe='')}/continue", body=body)
278
+ def cancel_strategy_execution(self, execution_id: str) -> ViraResponse | None:
279
+ if not execution_id.strip(): raise ValueError("SDK_EXECUTION_ID_REQUIRED")
280
+ from urllib.parse import quote
281
+ return self._request("POST", f"/v1/runtime/strategy-executions/{quote(execution_id, safe='')}/cancel", body={})
282
+ def list_packs(self) -> ViraResponse | None: return self._request("GET", "/v1/packs")
283
+ def import_pack(self, manifest: Json | None = None, objects: Mapping[str, Json] | None = None, *, idempotency_key: str | None = None, archive_base64: str | None = None, file_name: str | None = None) -> ViraResponse | None:
284
+ if manifest is None and not archive_base64: raise ValueError("SDK_PACK_MANIFEST_OR_ARCHIVE_REQUIRED")
285
+ if archive_base64 and not file_name: raise ValueError("SDK_PACK_FILE_NAME_REQUIRED")
286
+ body: dict[str, Json] = {"objects": dict(objects or {})}
287
+ if manifest is not None: body["manifest"] = manifest
288
+ if archive_base64: body.update({"archiveBase64": archive_base64, "fileName": file_name})
289
+ return self._request("POST", "/v1/packs/import", body=body, idempotency_key=idempotency_key)
290
+ def install_pack(self, pack_id: str, variables: Mapping[str, Json] | None = None, *, idempotency_key: str | None = None) -> ViraResponse | None:
291
+ if not pack_id.strip(): raise ValueError("SDK_PACK_ID_REQUIRED")
292
+ from urllib.parse import quote
293
+ return self._request("POST", f"/v1/packs/{quote(pack_id, safe='')}/install", body={"variables": dict(variables or {})}, idempotency_key=idempotency_key or f"sdk:pack:install:{pack_id}")
294
+ def evaluate_pack(self, pack_id: str, request: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
295
+ if not pack_id.strip(): raise ValueError("SDK_PACK_ID_REQUIRED")
296
+ from urllib.parse import quote
297
+ return self._request("POST", f"/v1/packs/{quote(pack_id, safe='')}/evaluate", body=dict(request), idempotency_key=idempotency_key or f"sdk:pack:evaluate:{pack_id}")
298
+ def release_pack(self, pack_id: str, *, idempotency_key: str | None = None) -> ViraResponse | None:
299
+ if not pack_id.strip(): raise ValueError("SDK_PACK_ID_REQUIRED")
300
+ from urllib.parse import quote
301
+ return self._request("POST", f"/v1/packs/{quote(pack_id, safe='')}/release", body={}, idempotency_key=idempotency_key or f"sdk:pack:release:{pack_id}")
302
+ def approve_pack(self, pack_id: str, *, idempotency_key: str | None = None) -> ViraResponse | None:
303
+ if not pack_id.strip(): raise ValueError("SDK_PACK_ID_REQUIRED")
304
+ from urllib.parse import quote
305
+ return self._request("POST", f"/v1/packs/{quote(pack_id, safe='')}/approve", body={}, idempotency_key=idempotency_key or f"sdk:pack:approve:{pack_id}")
306
+ def preview_pack_upgrade(self, pack_id: str, incoming: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
307
+ if not pack_id.strip(): raise ValueError("SDK_PACK_ID_REQUIRED")
308
+ if not isinstance(incoming, Mapping): raise ValueError("SDK_PACK_UPGRADE_INPUT_REQUIRED")
309
+ from urllib.parse import quote
310
+ return self._request("POST", f"/v1/packs/{quote(pack_id, safe='')}/upgrade-preview", body={"incoming": dict(incoming)}, idempotency_key=idempotency_key or f"sdk:pack:upgrade-preview:{pack_id}")
311
+ def get_behavior_program(self, program_id: str) -> ViraResponse | None:
312
+ if not program_id.strip(): raise ValueError("SDK_PROGRAM_ID_REQUIRED")
313
+ from urllib.parse import quote
314
+ return self._request("GET", f"/v1/behavior-programs/{quote(program_id, safe='')}")
315
+ def create_behavior_program(self, program: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
316
+ if not isinstance(program, Mapping) or not str(program.get("id", "")).strip(): raise ValueError("SDK_PROGRAM_REQUIRED")
317
+ return self._request("POST", "/v1/behavior-programs", body=dict(program), idempotency_key=idempotency_key or f"sdk:program:{program['id']}")
318
+
319
+ def list_discovery_candidates(self, review_state: str | None = None) -> ViraResponse | None:
320
+ return self._request("GET", "/v1/discovery/candidates", query={"reviewState": review_state} if review_state else None)
321
+ def start_evaluation(self, request: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
322
+ return self._request("POST", "/v1/evaluations", body=dict(request), idempotency_key=idempotency_key)
323
+ def get_evaluation(self, evaluation_id: str) -> ViraResponse | None:
324
+ if not evaluation_id.strip(): raise ValueError("SDK_EVALUATION_ID_REQUIRED")
325
+ from urllib.parse import quote
326
+ return self._request("GET", f"/v1/evaluations/{quote(evaluation_id, safe='')}")
327
+ def create_release(self, request: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
328
+ return self._request("POST", "/v1/releases", body=dict(request), idempotency_key=idempotency_key)
329
+ def submit_teach_evidence(self, evidence: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
330
+ return self._request("POST", "/v1/teach/evidence", body=dict(evidence), idempotency_key=idempotency_key)
331
+
332
+ def attest_host_capabilities(self, attestation: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
333
+ if attestation.get("schemaVersion") != 2:
334
+ raise ValueError("HOST_CAPABILITY_SCHEMA_VERSION_INVALID")
335
+ return self._request("POST", "/v1/runtime/capabilities/attest", body=dict(attestation), idempotency_key=idempotency_key)
336
+
337
+ def resolve_with_capabilities(self, attestation_id: str, request: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
338
+ context = request.get("executionContext")
339
+ if not attestation_id.strip() or not isinstance(context, Mapping) or not str(context.get("executionId", "")).strip():
340
+ raise ValueError("HOST_CAPABILITY_EXECUTION_ID_REQUIRED")
341
+ return self._request("POST", "/v1/runtime/capabilities/resolve", body={"attestationId": attestation_id, "request": dict(request)}, idempotency_key=idempotency_key, enforcing=True)
342
+
343
+ def trace_with_capabilities(self, decision_digest: str, trace: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
344
+ context = trace.get("executionContext")
345
+ if not decision_digest.strip() or not str(trace.get("id", "")).strip() or not isinstance(context, Mapping) or not str(context.get("executionId", "")).strip():
346
+ raise ValueError("HOST_CAPABILITY_TRACE_IDENTITY_REQUIRED")
347
+ return self._request("POST", "/v1/runtime/capabilities/traces", body={"decisionDigest": decision_digest, "trace": dict(trace)}, idempotency_key=idempotency_key)
348
+
349
+ def model_selection(self, request: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
350
+ return self._request("POST", "/v1/model-serving/resolve", body=dict(request), idempotency_key=idempotency_key, enforcing=True)
351
+
352
+ def served_trace(self, trace: Mapping[str, Json], selection: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
353
+ if not str(trace.get("id", "")).strip() or not isinstance(trace.get("executionContext"), Mapping):
354
+ raise ValueError("MODEL_SERVING_TRACE_IDENTITY_REQUIRED")
355
+ return self._request("POST", "/v1/model-serving/traces", body={"trace": dict(trace), "selection": dict(selection)}, idempotency_key=idempotency_key)
356
+
357
+ def teach(self, evidence: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
358
+ return self._request("POST", "/v1/teach/evidence", body=dict(evidence), idempotency_key=idempotency_key)
359
+
360
+ def compare_rollout(self, deployment_id: str, comparison: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
361
+ if not deployment_id.strip():
362
+ raise ValueError("DEPLOYMENT_ID_REQUIRED")
363
+ from urllib.parse import quote
364
+ return self._request("POST", f"/v1/deployments/{quote(deployment_id, safe='')}/comparisons", body=dict(comparison), idempotency_key=idempotency_key)
365
+
366
+ def rollout_health(self, deployment_id: str, health: Mapping[str, Json], *, idempotency_key: str | None = None) -> ViraResponse | None:
367
+ if not deployment_id.strip():
368
+ raise ValueError("DEPLOYMENT_ID_REQUIRED")
369
+ from urllib.parse import quote
370
+ return self._request("POST", f"/v1/deployments/{quote(deployment_id, safe='')}/authoritative-health", body=dict(health), idempotency_key=idempotency_key)
371
+
372
+ def connection_health(self, connection_id: str) -> ViraResponse | None: return self._request("GET", f"/v1/connections/{connection_id}/health")
373
+ def inventory(self, system_id: str) -> ViraResponse | None: return self._request("GET", f"/v1/ai-systems/{system_id}/inventory")
374
+ def test_connection(self, connection_id: str, *, idempotency_key: str | None = None) -> ViraResponse | None:
375
+ if not connection_id.strip(): raise ValueError("SDK_CONNECTION_ID_REQUIRED")
376
+ from urllib.parse import quote
377
+ return self._request("POST", f"/v1/connections/{quote(connection_id, safe='')}/test", body={}, idempotency_key=idempotency_key)
378
+
379
+ def close(self) -> None:
380
+ return None
381
+
382
+ def __enter__(self) -> "ViraClient":
383
+ return self
384
+
385
+ def __exit__(self, exc_type: object, exc_value: object, traceback: object) -> None:
386
+ self.close()