Skip to content

Commit 69e300d

Browse files
Copilotphrocker
andauthored
Add generational lineage to Sentrius agents with policy-controlled evolution and memory inheritance (#39)
* Initial plan * Add generational lineage to Sentrius agents with GenerationManager and LearningService Co-authored-by: phrocker <[email protected]> * Add integration tests for generational lineage workflow Co-authored-by: phrocker <[email protected]> * Address code review feedback: extract magic numbers and add null safety Co-authored-by: phrocker <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: phrocker <[email protected]>
1 parent a9503b2 commit 69e300d

File tree

9 files changed

+1667
-0
lines changed

9 files changed

+1667
-0
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
-- V31__add_generational_lineage_to_agent_contexts.sql
2+
-- Add generational lineage fields to agent_contexts table
3+
4+
-- Add generation tracking fields
5+
ALTER TABLE agent_contexts ADD COLUMN IF NOT EXISTS generation INTEGER DEFAULT 1;
6+
ALTER TABLE agent_contexts ADD COLUMN IF NOT EXISTS parent_id UUID;
7+
ALTER TABLE agent_contexts ADD COLUMN IF NOT EXISTS memory_namespace VARCHAR(255);
8+
ALTER TABLE agent_contexts ADD COLUMN IF NOT EXISTS trust_score DOUBLE PRECISION DEFAULT 0.5;
9+
ALTER TABLE agent_contexts ADD COLUMN IF NOT EXISTS policy_id VARCHAR(255);
10+
11+
-- Add indexes for efficient lineage queries
12+
CREATE INDEX IF NOT EXISTS idx_agent_contexts_parent_id ON agent_contexts(parent_id);
13+
CREATE INDEX IF NOT EXISTS idx_agent_contexts_generation ON agent_contexts(generation);
14+
CREATE INDEX IF NOT EXISTS idx_agent_contexts_policy_id ON agent_contexts(policy_id);
15+
16+
-- Add foreign key constraint for parent-child relationship
17+
ALTER TABLE agent_contexts ADD CONSTRAINT fk_agent_contexts_parent
18+
FOREIGN KEY (parent_id) REFERENCES agent_contexts(id) ON DELETE SET NULL;
19+
20+
-- Comment on columns
21+
COMMENT ON COLUMN agent_contexts.generation IS 'Generation number of the agent (1 for original, 2+ for descendants)';
22+
COMMENT ON COLUMN agent_contexts.parent_id IS 'Reference to parent agent context for lineage tracking';
23+
COMMENT ON COLUMN agent_contexts.memory_namespace IS 'Namespace for agent memory (e.g., agents/name_v2)';
24+
COMMENT ON COLUMN agent_contexts.trust_score IS 'Trust score for agent (0.0 to 1.0, decays with generations)';
25+
COMMENT ON COLUMN agent_contexts.policy_id IS 'Associated ATPL policy ID for agent authorization';

dataplane/src/main/java/io/sentrius/sso/core/model/agents/AgentContext.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,52 @@ public class AgentContext {
3434
private Instant createdAt;
3535
private Instant updatedAt;
3636

37+
// Generational Lineage fields
38+
@Column(name = "generation")
39+
private Integer generation = 1;
40+
41+
@Column(name = "parent_id")
42+
private UUID parentId;
43+
44+
@Column(name = "memory_namespace")
45+
private String memoryNamespace;
46+
47+
@Column(name = "trust_score")
48+
private Double trustScore = 0.5;
49+
50+
@Column(name = "policy_id")
51+
private String policyId;
52+
3753
@PrePersist
3854
protected void onCreate() {
3955
createdAt = updatedAt = Instant.now();
56+
if (generation == null) {
57+
generation = 1;
58+
}
59+
if (trustScore == null) {
60+
trustScore = 0.5;
61+
}
62+
if (memoryNamespace == null && name != null) {
63+
memoryNamespace = "agents/" + name + "_v" + generation;
64+
}
4065
}
4166

4267
@PreUpdate
4368
protected void onUpdate() {
4469
updatedAt = Instant.now();
4570
}
4671

72+
// Helper methods for generational lineage
73+
public boolean isFirstGeneration() {
74+
return generation == 1 && parentId == null;
75+
}
76+
77+
public String getMemoryNamespace() {
78+
if (memoryNamespace == null && name != null) {
79+
memoryNamespace = "agents/" + name + "_v" + generation;
80+
}
81+
return memoryNamespace;
82+
}
83+
4784
// Getters and setters omitted for brevity
4885
}

dataplane/src/main/java/io/sentrius/sso/core/repository/AgentMemoryRepository.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ public interface AgentMemoryRepository extends JpaRepository<AgentMemory, Long>
4545
@Query("SELECT m FROM AgentMemory m WHERE m.markings LIKE %:marking%")
4646
List<AgentMemory> findByMarkingsContaining(@Param("marking") String marking);
4747

48+
@Query("SELECT m FROM AgentMemory m WHERE m.agentId = :agentId AND m.markings LIKE %:marking% ORDER BY m.createdAt DESC")
49+
List<AgentMemory> findByAgentIdAndMarkingsContaining(@Param("agentId") String agentId, @Param("marking") String marking);
50+
4851
// === JPQL filterable query ===
4952

5053
@Query("""
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
package io.sentrius.sso.core.services.agents;
2+
3+
import io.sentrius.sso.core.model.agents.AgentContext;
4+
import io.sentrius.sso.core.repository.AgentContextRepository;
5+
import io.sentrius.sso.core.services.abac.EvaluationContext;
6+
import io.sentrius.sso.core.services.abac.PolicyDecision;
7+
import io.sentrius.sso.core.services.abac.PolicyEvaluator;
8+
import io.sentrius.sso.provenance.ProvenanceEvent;
9+
import io.sentrius.sso.provenance.ProvenanceLogger;
10+
import lombok.extern.slf4j.Slf4j;
11+
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
12+
import org.springframework.stereotype.Service;
13+
import org.springframework.transaction.annotation.Transactional;
14+
15+
import java.time.Instant;
16+
import java.util.UUID;
17+
18+
/**
19+
* Sentrius GenerationManager: spawn next agent generation from parent under ATPL policy.
20+
* Clone memory, decay trust, and record lineage.
21+
*/
22+
@Slf4j
23+
@Service
24+
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
25+
public class GenerationManager {
26+
27+
private final AgentContextRepository agentContextRepository;
28+
private final LearningService learningService;
29+
private final PolicyEvaluator policyEvaluator;
30+
private final ProvenanceLogger provenanceLogger;
31+
private final VectorAgentMemoryStore vectorMemoryStore;
32+
33+
// Trust and memory decay constants
34+
private static final double TRUST_DECAY_FACTOR = 0.95;
35+
private static final double MEMORY_RELEVANCE_DECAY = 0.9;
36+
private static final double MIN_TRUST_SCORE_FOR_GENERATION = 0.8;
37+
private static final double DEFAULT_TRUST_SCORE = 0.5;
38+
39+
public GenerationManager(
40+
AgentContextRepository agentContextRepository,
41+
LearningService learningService,
42+
PolicyEvaluator policyEvaluator,
43+
ProvenanceLogger provenanceLogger,
44+
VectorAgentMemoryStore vectorMemoryStore) {
45+
this.agentContextRepository = agentContextRepository;
46+
this.learningService = learningService;
47+
this.policyEvaluator = policyEvaluator;
48+
this.provenanceLogger = provenanceLogger;
49+
this.vectorMemoryStore = vectorMemoryStore;
50+
}
51+
52+
/**
53+
* Creates a new agent generation from a parent agent.
54+
* Validates policy authorization, decays trust and memory, and initializes the new generation.
55+
*
56+
* @param parentId The ID of the parent agent
57+
* @param requestingUserId The ID of the user requesting the generation
58+
* @return The newly created agent generation
59+
* @throws IllegalStateException if generation creation is not authorized
60+
*/
61+
@Transactional
62+
public AgentContext createNextGeneration(UUID parentId, String requestingUserId) {
63+
log.info("Creating next generation from parent: {}, requestedBy: {}", parentId, requestingUserId);
64+
65+
// Load parent agent
66+
AgentContext parent = agentContextRepository.findById(parentId)
67+
.orElseThrow(() -> new IllegalArgumentException("Parent agent not found: " + parentId));
68+
69+
// Validate policy authorization for GENERATION_CREATE
70+
validateGenerationCreationPolicy(parent, requestingUserId);
71+
72+
// Create child agent with incremented generation
73+
AgentContext child = createChildAgent(parent);
74+
75+
// Decay trust score
76+
double childTrustScore = calculateDecayedTrustScore(parent.getTrustScore());
77+
child.setTrustScore(childTrustScore);
78+
79+
// Save child agent
80+
child = agentContextRepository.save(child);
81+
log.info("Created new agent generation: id={}, generation={}, trustScore={}",
82+
child.getId(), child.getGeneration(), child.getTrustScore());
83+
84+
// Bootstrap memory from parent
85+
learningService.bootstrapFromParent(parent, child, MEMORY_RELEVANCE_DECAY);
86+
87+
// Log provenance event
88+
logGenerationCreation(parent, child, requestingUserId);
89+
90+
return child;
91+
}
92+
93+
/**
94+
* Validates that the parent agent meets policy requirements for creating a new generation.
95+
*/
96+
private void validateGenerationCreationPolicy(AgentContext parent, String requestingUserId) {
97+
log.debug("Validating GENERATION_CREATE policy for parent: {}", parent.getId());
98+
99+
// Check minimum trust score
100+
if (parent.getTrustScore() < MIN_TRUST_SCORE_FOR_GENERATION) {
101+
String message = String.format(
102+
"Parent trust score %.2f is below minimum %.2f required for generation",
103+
parent.getTrustScore(), MIN_TRUST_SCORE_FOR_GENERATION);
104+
log.warn(message);
105+
throw new IllegalStateException(message);
106+
}
107+
108+
// Evaluate ABAC policy for GENERATION_CREATE action
109+
EvaluationContext context = policyEvaluator.buildContext(requestingUserId, parent.getId().toString());
110+
context.addResourceAttribute("parent_trust_score", String.valueOf(parent.getTrustScore()));
111+
context.addResourceAttribute("parent_generation", String.valueOf(parent.getGeneration()));
112+
context.addResourceAttribute("parent_policy_id", parent.getPolicyId());
113+
context.addResourceAttribute("resource_type", "agent_generation");
114+
115+
PolicyDecision decision = policyEvaluator.evaluate(context, parent.getId().toString(), "GENERATION_CREATE");
116+
117+
if (decision.getEffect() != PolicyDecision.Effect.ALLOW) {
118+
String message = "Policy denied generation creation: " + decision.getReason();
119+
log.warn(message);
120+
throw new IllegalStateException(message);
121+
}
122+
123+
log.info("GENERATION_CREATE policy validated successfully for parent: {}", parent.getId());
124+
}
125+
126+
/**
127+
* Creates a child agent from the parent with incremented generation.
128+
*/
129+
private AgentContext createChildAgent(AgentContext parent) {
130+
AgentContext child = new AgentContext();
131+
child.setId(UUID.randomUUID());
132+
child.setName(parent.getName());
133+
child.setDescription("Generation " + (parent.getGeneration() + 1) + " of " + parent.getName());
134+
child.setContext(parent.getContext()); // Copy context configuration
135+
child.setGeneration(parent.getGeneration() + 1);
136+
child.setParentId(parent.getId());
137+
child.setPolicyId(parent.getPolicyId()); // Inherit policy
138+
139+
// Create new memory namespace for this generation
140+
child.setMemoryNamespace("agents/" + parent.getName() + "_v" + child.getGeneration());
141+
142+
return child;
143+
}
144+
145+
/**
146+
* Calculates the decayed trust score for the child generation.
147+
*/
148+
private double calculateDecayedTrustScore(Double parentTrustScore) {
149+
if (parentTrustScore == null) {
150+
return DEFAULT_TRUST_SCORE;
151+
}
152+
double decayed = parentTrustScore * TRUST_DECAY_FACTOR;
153+
// Ensure trust score stays within bounds [0.0, 1.0]
154+
return Math.max(0.0, Math.min(1.0, decayed));
155+
}
156+
157+
/**
158+
* Logs provenance event for generation creation.
159+
*/
160+
private void logGenerationCreation(AgentContext parent, AgentContext child, String requestingUserId) {
161+
ProvenanceEvent event = ProvenanceEvent.builder()
162+
.eventId(UUID.randomUUID().toString())
163+
.sessionId(child.getId().toString())
164+
.actor(child.getName())
165+
.triggeringUser(requestingUserId)
166+
.eventType(ProvenanceEvent.EventType.AGENT_RESPOND) // Reusing existing type
167+
.input("Parent: " + parent.getId() + " (gen " + parent.getGeneration() + ")")
168+
.outputSummary("Child: " + child.getId() + " (gen " + child.getGeneration() +
169+
"), TrustScore: " + child.getTrustScore())
170+
.timestamp(Instant.now())
171+
.build();
172+
173+
provenanceLogger.log(event);
174+
log.info("Logged provenance for generation creation: parent={}, child={}", parent.getId(), child.getId());
175+
}
176+
177+
/**
178+
* Gets the memory decay factor used for inheritance.
179+
*/
180+
public double getMemoryDecayFactor() {
181+
return MEMORY_RELEVANCE_DECAY;
182+
}
183+
184+
/**
185+
* Gets the minimum trust score required for generation creation.
186+
*/
187+
public double getMinTrustScoreForGeneration() {
188+
return MIN_TRUST_SCORE_FOR_GENERATION;
189+
}
190+
}

0 commit comments

Comments
 (0)