Runtime Session Lifecycle
This page documents the current end-to-end runtime path between the Python client and the server, and the exact shape of the session record stored on the server.
Complete Flow
1. Initialization
At initialization time, the current Python implementation behaves as follows:
- The caller provides
session_idwhen constructingAgentGuard. - The client generates
session_keyautomatically if the caller does not provide one. - The client builds
RuntimeContextwithsession_id,agent_id,user_id, and metadata such as:client_session_keyclient_plugin_configremote_plugin_config
- If remote mode is enabled (
server_urlconfigured), the client constructor attempts to start a local config API immediately and writes these URLs intocontext.metadata:client_config_urlclient_plugin_list_urlclient_health_url
- The client then registers the session to the server.
- The server upserts a session record into the session pool.
Current code references:
src/client/python/agentguard/guard.py:60src/client/python/agentguard/guard.py:61src/client/python/agentguard/guard.py:155src/server/backend/api/client_router.py:66src/server/backend/runtime/storage/__init__.py:113
2. Runtime Decision
At decision time, the current path is:
- The client runs client-side plugins first.
- If the client-side result is final, the client applies it locally and stores the decision in
ClientSyncBuffer. - If the client-side result is not final, the client calls
/v1/server/guard/decide. - The server refreshes or upserts the session context for this request.
- The server looks up the session by the composite identity
session_id::agent_id::user_id, then applies any agent-scoped plugin override on top of the stored session config. - The server plugin manager parses the effective plugin config by phase and only executes the
serverplugin list for each phase. - The server returns the decision to the client.
Current code references:
src/client/python/agentguard/u_guard/enforcer.py:68src/client/python/agentguard/u_guard/enforcer.py:75src/client/python/agentguard/u_guard/enforcer.py:96src/client/python/agentguard/u_guard/remote_client.py:102src/server/backend/runtime/manager.py:221src/server/backend/runtime/manager.py:256src/server/backend/runtime/plugins/manager.py:32src/server/backend/runtime/manager.py:267
3. Client-Side Result Sync
Client-side-only decisions are not discarded. The client syncs them back to the server through two paths:
- At the end of a full round, the client asynchronously uploads trace entries.
- If another remote decision happens before the async upload completes, the buffered local entries are piggybacked in
client_cached_entries. - If the client hits an exception, it calls
sync_local_cache_now(reason="client_error")to try an immediate upload.
Current code references:
src/client/python/agentguard/harness/runtime.py:130src/client/python/agentguard/harness/runtime.py:133src/client/python/agentguard/harness/runtime.py:164src/client/python/agentguard/harness/runtime.py:183src/client/python/agentguard/u_guard/enforcer.py:133src/client/python/agentguard/u_guard/remote_client.py:110src/server/backend/runtime/manager.py:245src/server/backend/runtime/manager.py:338
4. Health Check
The server also maintains a background health check loop:
- The server periodically calls the client's
/v1/client/healthendpoint. - If the client is reachable, the server refreshes
last_seenand stores health metadata on the session. - If the client is unreachable, the returned health-check result is marked as
unreachable, but the session record itself is left unchanged. - The current code does not automatically delete the session when the client is dead or unreachable.
Current code references:
src/client/python/agentguard/config_api.py:108src/server/backend/runtime/manager.py:164src/server/backend/runtime/manager.py:192src/server/backend/runtime/manager.py:210
Plugin Config Shape
The session-scoped remote_plugin_config is not stored as a flattened server-only structure. It keeps the same phased shape as the client-side plugin config. During initial registration, clients populate it with the same payload as client_plugin_config; later client-side update_plugin_config() calls only update client_plugin_config, so the stored remote_plugin_config reflects the last server-synchronized server-side view unless the client re-registers or the server applies overrides.
A typical shape is:
{
"phases": {
"tool_before": {
"client": [],
"server": [
{
"name": "rule_based_plugin",
"env": {}
}
]
},
"llm_before": {
"client": [],
"server": []
},
"llm_after": {
"client": [],
"server": []
},
"tool_after": {
"client": [],
"server": []
},
"global": {
"client": [],
"server": []
}
}
}
Important behavior:
- When a plugin manager loads config for execution, the parser requires a
phasesobject. - When a phase is present, the execution parser expects both
clientandserverkeys. - The server only reads the
serverlist for execution. - The client-side plugin manager reads the same phased structure, but uses the
clientside. - If the server already has a default
plugin_configand the client mirrors that same structure intoremote_plugin_config, the server clears the mirrored session-scoped server override so the server default remains authoritative. Explicit session-scoped server overrides are still preserved.
Code references:
src/client/python/agentguard/guard.py:68src/server/backend/runtime/plugins/manager.py:42src/server/backend/runtime/plugins/manager.py:48src/server/backend/runtime/plugins/manager.py:54
Default Server Decision
If the server plugin pipeline does not produce a final decision, the server returns a default allow decision.
That default comes from _decision_from_plugin_result():
- If
check.is_finalanddecision_candidateexist, return that final plugin decision. - Otherwise return
GuardDecision.allow("No server plugin returned a final decision; default allow.").
Code reference:
src/server/backend/runtime/manager.py:418
Server Session Record Format
The server stores one session record per composite identity:
session_key = session_id::agent_id::user_id
This session_key is an internal storage key. It is different from client_key, which is the client session secret used in headers.
A typical healthy session record may look like this:
{
"session_key": "session_id::agent_id::user_id",
"session_id": "sess_123",
"agent_id": "agent-alpha",
"user_id": "user-1",
"client_ip": "127.0.0.1",
"client_key": "sk_xxx",
"client_config_url": "http://127.0.0.1:38181/v1/client/plugins/config",
"client_plugin_list_url": "http://127.0.0.1:38181/v1/client/plugins/list",
"client_health_url": "http://127.0.0.1:38181/v1/client/health",
"client_plugin_config": {
"phases": {
"tool_before": {
"client": [
{
"name": "tool_invoke",
"env": {}
}
],
"server": []
}
}
},
"remote_plugin_config": {
"phases": {
"tool_before": {
"client": [],
"server": [
{
"name": "rule_based_plugin",
"env": {}
}
]
}
}
},
"principal": null,
"metadata": {
"client_session_key": "sk_xxx",
"client_config_url": "http://127.0.0.1:38181/v1/client/plugins/config",
"client_plugin_list_url": "http://127.0.0.1:38181/v1/client/plugins/list",
"client_health_url": "http://127.0.0.1:38181/v1/client/health",
"client_plugin_config": {
"phases": {
"tool_before": {
"client": [
{
"name": "tool_invoke",
"env": {}
}
],
"server": []
}
}
},
"remote_plugin_config": {
"phases": {
"tool_before": {
"client": [],
"server": [
{
"name": "rule_based_plugin",
"env": {}
}
]
}
}
},
"event_metadata": {
"example": true
},
"last_health_check_status": "ok",
"last_health_check_url": "http://127.0.0.1:38181/v1/client/health",
"last_health_check_response": {
"status": "ok",
"service": "agentguard-client-config",
"session_id": "sess_123",
"agent_id": "agent-alpha",
"user_id": "user-1"
},
"last_trace_upload_reason": "round_complete"
},
"last_seen": 1781423456.123
}
Code references:
src/server/backend/runtime/storage/__init__.py:113src/server/backend/runtime/storage/__init__.py:149src/server/backend/runtime/manager.py:196src/server/backend/runtime/manager.py:339
Notes:
principalis optional and only appears when incoming event metadata provides it.metadata.last_health_check_*fields appear only after a successful health check.- The effective server-side execution config can still be replaced by agent-scoped overrides at decision time.