aigp-agent-core 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,40 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .venv/
8
+ .eggs/
9
+
10
+ # Node
11
+ node_modules/
12
+ dist/
13
+
14
+ # Go
15
+ bin/
16
+
17
+ # Rust
18
+ target/
19
+
20
+ # .NET
21
+ bin/
22
+ obj/
23
+
24
+ # Java
25
+ *.class
26
+ out/
27
+
28
+ # IDE
29
+ .idea/
30
+ .vscode/
31
+ *.swp
32
+
33
+ # OS
34
+ .DS_Store
35
+ Thumbs.db
36
+
37
+ # Secrets (never commit)
38
+ .env
39
+ *.pem
40
+ *.key
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: aigp-agent-core
3
+ Version: 1.0.0
4
+ Summary: AIGP Agent Governance Core — shared lifecycle for all framework adapters
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: aigp-client>=3.0.0
7
+ Provides-Extra: evidence
8
+ Requires-Dist: amigo-bedrock>=0.2.0; extra == 'evidence'
@@ -0,0 +1,18 @@
1
+ """AIGP Agent Core — shared governance lifecycle for all framework adapters.
2
+
3
+ Usage by adapter authors:
4
+ from aigp_agent_core import AgentGovernance
5
+
6
+ class MyFrameworkAdapter(AgentGovernance):
7
+ def on_my_framework_start(self, ...):
8
+ self.pre_invoke(agent_name, model_id, user_id)
9
+ def on_my_framework_end(self, ...):
10
+ self.post_invoke()
11
+ """
12
+
13
+ __version__ = "1.0.0"
14
+
15
+ from .governance import AgentGovernance
16
+ from .stage_mapper import StageMapper
17
+
18
+ __all__ = ["AgentGovernance", "StageMapper"]
@@ -0,0 +1,183 @@
1
+ """Agent Governance — shared lifecycle that all framework adapters inherit."""
2
+
3
+ import logging
4
+ import time
5
+
6
+ from aigp_client import (
7
+ AigpClient, TraceBuilder, SessionContext, TokenAccumulator,
8
+ ToolGovernance, DelegationToken, retry_on_rate_limit,
9
+ )
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class GovernanceBlockedError(Exception):
15
+ """Raised when AIGP CHECK returns DENY."""
16
+ pass
17
+
18
+
19
+ class AgentGovernance:
20
+ """Base class for governed agent adapters.
21
+
22
+ Handles the full AIGP lifecycle:
23
+ - pre_invoke: CHECK (is this agent allowed?)
24
+ - on_model_call: accumulate tokens
25
+ - on_tool_call: optional per-tool CHECK
26
+ - post_invoke: RECORD + TRACE + D-DNA evidence
27
+ - on_error: RECORD with ERROR status
28
+
29
+ Subclasses map framework-specific hooks to these methods.
30
+ """
31
+
32
+ def __init__(self, gov_url: str, app_id: str, hmac_secret: str, *,
33
+ vault_url: str = "", evidence_bucket: str = "",
34
+ consent_tier: str = "STANDARD", tool_policies: dict | None = None):
35
+ self._client = AigpClient(gov_url, app_id, hmac_secret)
36
+ self._app_id = app_id
37
+ self._session: SessionContext | None = None
38
+ self._tokens = TokenAccumulator()
39
+ self._trace: TraceBuilder | None = None
40
+ self._tools = ToolGovernance(self._client, tool_policies) if tool_policies else None
41
+ self._request_id = ""
42
+ self._model_id = ""
43
+ self._use_case = ""
44
+ self._t0 = 0.0
45
+ self._consent_tier = consent_tier
46
+
47
+ # Evidence writer (optional)
48
+ self._evidence = None
49
+ if vault_url and evidence_bucket:
50
+ try:
51
+ from amigo_bedrock import EvidenceWriter
52
+ self._evidence = EvidenceWriter(app_id, vault_url, evidence_bucket)
53
+ except ImportError:
54
+ logger.debug("amigo_bedrock not available, evidence writing disabled")
55
+
56
+ async def start(self) -> None:
57
+ """Start governance — register app and begin heartbeat."""
58
+ try:
59
+ await self._client.start_heartbeat(3600)
60
+ logger.info("AIGP governance started: %s", self._app_id)
61
+ except Exception as e:
62
+ logger.warning("AIGP start failed (non-fatal): %s", e)
63
+
64
+ def pre_invoke(self, agent_name: str, model_id: str, user_id: str = "",
65
+ session: SessionContext | None = None) -> str:
66
+ """Call before agent execution. Sends CHECK. Returns request_id.
67
+
68
+ Raises GovernanceBlockedError if DENY.
69
+ """
70
+ import asyncio
71
+
72
+ self._t0 = time.time()
73
+ self._model_id = model_id
74
+ self._use_case = agent_name
75
+ self._tokens.reset()
76
+ self._session = session or SessionContext(user_id=user_id, consent_tier=self._consent_tier)
77
+ self._trace = TraceBuilder(request_id="")
78
+
79
+ if self._session.parent_session_id:
80
+ self._trace.link_parent(self._session.parent_session_id)
81
+
82
+ # S1: identity_session_initiation
83
+ self._trace.start(1)
84
+ self._trace.end(1, attributes={"user_id": user_id, "agent_name": agent_name})
85
+
86
+ # S3: request_registration (CHECK)
87
+ self._trace.start(3)
88
+ try:
89
+ decision = asyncio.get_event_loop().run_until_complete(
90
+ retry_on_rate_limit(lambda: self._client.check(use_case=agent_name, model_id=model_id))
91
+ )
92
+ except RuntimeError:
93
+ # No event loop — try sync
94
+ import concurrent.futures
95
+ with concurrent.futures.ThreadPoolExecutor() as pool:
96
+ decision = pool.submit(lambda: __import__('asyncio').run(
97
+ retry_on_rate_limit(lambda: self._client.check(use_case=agent_name, model_id=model_id))
98
+ )).result()
99
+
100
+ self._trace.end(3)
101
+
102
+ d = decision.get("decision", "ALLOW") if isinstance(decision, dict) else "ALLOW"
103
+ if d == "DENY":
104
+ raise GovernanceBlockedError(decision.get("reason", "Blocked by governance"))
105
+
106
+ self._request_id = decision.get("request_id", "") if isinstance(decision, dict) else ""
107
+ return self._request_id
108
+
109
+ def on_model_call(self, input_tokens: int = 0, output_tokens: int = 0, usage: dict | None = None) -> None:
110
+ """Call after each model invocation with token counts."""
111
+ if usage:
112
+ self._tokens.add_from_usage(usage)
113
+ else:
114
+ self._tokens.add(input_tokens, output_tokens)
115
+
116
+ async def on_tool_call(self, tool_name: str, params: dict = None, result: str = "",
117
+ duration_ms: int = 0) -> dict:
118
+ """Call after a tool execution. Optionally CHECKs tool permission."""
119
+ decision = {"decision": "ALLOW"}
120
+ if self._tools:
121
+ decision = await self._tools.check_tool(tool_name, params)
122
+ if decision.get("decision") == "DENY":
123
+ raise GovernanceBlockedError(f"Tool '{tool_name}' denied: {decision.get('reason')}")
124
+
125
+ if self._trace:
126
+ self._trace.add_tool_span(tool_name, duration_ms, attributes={"classification": decision.get("classification", "")})
127
+
128
+ return decision
129
+
130
+ async def post_invoke(self, status: str = "SUCCESS") -> None:
131
+ """Call after agent session completes. Sends RECORD + TRACE + evidence."""
132
+ duration_ms = int((time.time() - self._t0) * 1000)
133
+
134
+ # RECORD
135
+ try:
136
+ await self._client.record(
137
+ use_case=self._use_case, model_id=self._model_id,
138
+ status=status, request_id=self._request_id,
139
+ input_tokens=self._tokens.total_input,
140
+ output_tokens=self._tokens.total_output,
141
+ duration_ms=duration_ms,
142
+ user_id=self._session.user_id if self._session else "",
143
+ session_id=self._session.session_id if self._session else "",
144
+ )
145
+ except Exception as e:
146
+ logger.warning("RECORD failed: %s", e)
147
+
148
+ # TRACE
149
+ if self._trace:
150
+ self._trace.start(14)
151
+ self._trace.end(14)
152
+ spans, summary = self._trace.build()
153
+ try:
154
+ await self._client.trace(
155
+ trace_id=self._trace.trace_id,
156
+ request_id=self._request_id,
157
+ use_case=self._use_case,
158
+ model_id=self._model_id,
159
+ user_id=self._session.user_id if self._session else "",
160
+ spans=spans, summary=summary,
161
+ session_id=self._session.session_id if self._session else "",
162
+ )
163
+ except Exception as e:
164
+ logger.warning("TRACE failed: %s", e)
165
+
166
+ # Evidence
167
+ if self._evidence:
168
+ try:
169
+ from datetime import datetime, timezone
170
+ await self._evidence.write({
171
+ "app_id": self._app_id, "request_id": self._request_id,
172
+ "use_case": self._use_case, "model_id": self._model_id,
173
+ "user_id": self._session.user_id if self._session else "",
174
+ "timestamp": datetime.now(timezone.utc).isoformat(),
175
+ "duration_ms": duration_ms, "status": status,
176
+ "tokens": self._tokens.to_dict(),
177
+ })
178
+ except Exception as e:
179
+ logger.warning("Evidence write failed: %s", e)
180
+
181
+ async def on_error(self, error: Exception) -> None:
182
+ """Call on agent error. Sends RECORD with ERROR status."""
183
+ await self.post_invoke(status="ERROR")
@@ -0,0 +1,76 @@
1
+ """Stage Mapper — maps framework-specific events to AIGP 14+9 stages."""
2
+
3
+ from aigp_client import STAGE_NAMES
4
+
5
+ # Common mappings across frameworks
6
+ COMMON_MAPPINGS = {
7
+ "agent_start": 1, # identity_session_initiation
8
+ "consent_check": 2, # consent_policy_determination
9
+ "pre_check": 3, # request_registration
10
+ "prompt_build": 5, # prompt_assembly
11
+ "context_retrieve": 7, # context_retrieval
12
+ "model_select": 8, # model_selection
13
+ "model_invoke": 9, # runtime_invocation
14
+ "tool_auth": 10, # tool_agent_authorization
15
+ "reasoning": 11, # intermediate_reasoning
16
+ "output_gen": 12, # output_generation
17
+ "output_release": 13, # output_release
18
+ "record": 14, # post_invocation_record
19
+ # Agentic
20
+ "agent_bind": 18, # agent_identity_binding
21
+ "delegation": 19, # delegation_scope_assignment
22
+ "input_attest": 20, # input_attestation
23
+ "tool_runtime": 21, # tool_authorization_runtime
24
+ "plan_govern": 22, # execution_plan_governance
25
+ "autonomy_eval": 23, # autonomy_boundary_evaluation
26
+ "inter_agent": 24, # inter_agent_communication
27
+ "memory_gov": 25, # memory_governance
28
+ "circuit_break": 26, # circuit_breaker_evaluation
29
+ }
30
+
31
+ # Framework-specific mappings
32
+ STRANDS_MAPPINGS = {
33
+ "Strands Agent": 1,
34
+ "Cycle": 11,
35
+ "Model invoke": 9,
36
+ "Tool": 21,
37
+ }
38
+
39
+ LANGCHAIN_MAPPINGS = {
40
+ "on_chain_start": 1,
41
+ "on_llm_start": 9,
42
+ "on_llm_end": 12,
43
+ "on_tool_start": 21,
44
+ "on_tool_end": 21,
45
+ "on_retriever_start": 7,
46
+ "on_chain_end": 13,
47
+ }
48
+
49
+ CREWAI_MAPPINGS = {
50
+ "task_start": 22, # execution_plan_governance
51
+ "agent_start": 18, # agent_identity_binding
52
+ "tool_use": 21, # tool_authorization_runtime
53
+ "delegation": 19, # delegation_scope_assignment
54
+ "task_end": 13, # output_release
55
+ }
56
+
57
+
58
+ class StageMapper:
59
+ """Maps framework events to AIGP stage numbers."""
60
+
61
+ def __init__(self, framework: str = "common"):
62
+ if framework == "strands":
63
+ self._map = STRANDS_MAPPINGS
64
+ elif framework == "langchain":
65
+ self._map = LANGCHAIN_MAPPINGS
66
+ elif framework == "crewai":
67
+ self._map = CREWAI_MAPPINGS
68
+ else:
69
+ self._map = COMMON_MAPPINGS
70
+
71
+ def stage_for(self, event_name: str) -> int | None:
72
+ """Get AIGP stage number for a framework event. Returns None if unmapped."""
73
+ return self._map.get(event_name) or COMMON_MAPPINGS.get(event_name)
74
+
75
+ def stage_name(self, stage: int) -> str:
76
+ return STAGE_NAMES.get(stage, f"unknown_{stage}")
@@ -0,0 +1,13 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "aigp-agent-core"
7
+ version = "1.0.0"
8
+ description = "AIGP Agent Governance Core — shared lifecycle for all framework adapters"
9
+ requires-python = ">=3.11"
10
+ dependencies = ["aigp-client>=3.0.0"]
11
+
12
+ [project.optional-dependencies]
13
+ evidence = ["amigo-bedrock>=0.2.0"]