Skip to main content
NORMATIVE FROZENFROZENMPGC

PSG – Project Semantic Graph

1. Scope

This specification defines the normative requirements for PSG – Project Semantic Graph.

2. Non-Goals

This specification does not mandate specific implementation details beyond the defined interfaces and invariants.

3. Purpose

The Project Semantic Graph (PSG) is the "Brain" of the MPLP runtime – the semantic substrate that models all entities, relationships, and state transitions within an agent project. PSG provides a clean graph abstraction over raw storage (VSL).

Key Responsibilities:

  • Model protocol entities (Context, Plan, Step, Trace, etc.)
  • Enforce semantic relationships (Plan → Context, Step → Plan)
  • Track state transitions with history
  • Provide graph queries for complex lookups
  • Enable drift detection by comparing reality vs. expected state

Design Principle: "PSG is the semantic truth; VSL is the physical storage"

4. Core Concepts

4.1 Nodes

PSG organizes protocol objects as nodes in a directed property graph:

Node TypeDescriptionKey Fields
ContextNodeRoot of a lifecyclecontext_id, source, constraints
PlanNodeA sequence of proposed actionsplan_id, context_id, steps[]
StepNodeAtomic unit of workstep_id, plan_id, status
TraceNodeExecution recordtrace_id, context_id, segments[]
ConfirmNodeGovernance gateconfirm_id, target_id, status
RoleNodeAgent identity and capabilitiesrole_id, capabilities[]
CollabNodeMulti-agent session statecollab_id, participants[]
DialogNodeMessage exchangedialog_id, turns[]
ExtensionNodeTool/integration registryextension_id, tools[]
NetworkNodeAgent topologynetwork_id, connections[]

4.2 Edges

Nodes are connected by typed edges that enforce semantic integrity:

Edge TypeDescriptionExample
HAS_CONTEXTLinks Plan → Contextplan.context_id → context
CONTAINSLinks Plan → Step[]plan.steps → step[]
DEPENDS_ONLinks Step B → Step Astep.dependencies → step_id[]
ASSIGNED_TOLinks Step → Rolestep.agent_role → role_id
TRACESLinks Trace → Contexttrace.context_id → context
CONFIRMSLinks Confirm → Targetconfirm.target_id → plan/step
PRODUCESLinks Step → Artifactstep.outputs → artifact[]

5. Normative Interface

5.1 Core Interface

export interface ProjectSemanticGraph {
// Node operations
getNode<T>(type: string, id: string): Promise<T | null>;
putNode<T>(type: string, id: string, data: T): Promise<void>;
deleteNode(type: string, id: string): Promise<void>;

// Edge operations
getEdges(fromType: string, fromId: string, edgeType: string): Promise<Edge[]>;
addEdge(from: NodeRef, to: NodeRef, edgeType: string): Promise<void>;
removeEdge(from: NodeRef, to: NodeRef, edgeType: string): Promise<void>;

// Query operations
query(query: GraphQuery): Promise<Node[]>;
traverse(startId: string, edgeTypes: string[], depth: number): Promise<Node[]>;
}

interface NodeRef {
type: string;
id: string;
}

interface Edge {
from: NodeRef;
to: NodeRef;
edgeType: string;
metadata?: Record<string, any>;
}

5.2 Reference Implementation

InMemoryPSG:

export class InMemoryPSG implements ProjectSemanticGraph {
private nodes = new Map<string, Map<string, any>>(); // type -> id -> data
private edges = new Map<string, Edge[]>(); // fromKey -> edges

async getNode<T>(type: string, id: string): Promise<T | null> {
const typeMap = this.nodes.get(type);
return typeMap?.get(id) || null;
}

async putNode<T>(type: string, id: string, data: T): Promise<void> {
if (!this.nodes.has(type)) {
this.nodes.set(type, new Map());
}
this.nodes.get(type)!.set(id, data);
}

async query(query: GraphQuery): Promise<Node[]> {
const typeMap = this.nodes.get(query.type);
if (!typeMap) return [];

return Array.from(typeMap.values())
.filter(node => this.matchesFilters(node, query.filters || []))
.slice(query.offset || 0, (query.offset || 0) + (query.limit || 100));
}
}

5.3 Production Interface (Extended)

export interface PSGExtended extends ProjectSemanticGraph {
// Lifecycle operations
initialize(): Promise<void>;
close(): Promise<void>;

// Bulk operations
bulkPut(nodes: { type: string; id: string; data: any }[]): Promise<void>;
bulkQuery(queries: GraphQuery[]): Promise<Node[][]>;

// Snapshot for drift detection
getStateHash(): Promise<string>;
compareState(expectedHash: string): Promise<DriftResult>;

// Event binding
onNodeChange(callback: (event: NodeChangeEvent) => void): void;
onEdgeChange(callback: (event: EdgeChangeEvent) => void): void;

// VSL binding
getVSL(): ValueStateLayer;
}

interface DriftResult {
hasDrift: boolean;
driftedNodes: { type: string; id: string; reason: string }[];
}

interface NodeChangeEvent {
type: 'created' | 'updated' | 'deleted';
nodeType: string;
nodeId: string;
oldValue?: any;
newValue?: any;
}

6. Normative Requirements (MUST/SHALL)

6.1 Semantic Integrity

Requirement: The PSG MUST enforce parent-child relationships

Example (invalid operation):

// MUST fail: Step without Plan
await psg.putNode('Step', 'step-123', {
step_id: 'step-123',
plan_id: 'plan-nonexistent', // Plan doesn't exist
description: 'Orphan step'
});
// Error: Cannot create Step without existing Plan

Enforcement:

async putNode<T>(type: string, id: string, data: T): Promise<void> {
// Validate parent references
if (type === 'Step' && data.plan_id) {
const plan = await this.getNode('Plan', data.plan_id);
if (!plan) {
throw new Error(`Plan ${data.plan_id} does not exist`);
}
}

if (type === 'Plan' && data.context_id) {
const context = await this.getNode('Context', data.context_id);
if (!context) {
throw new Error(`Context ${data.context_id} does not exist`);
}
}

await this.storage.put(type, id, data);
}

6.2 Unique IDs

Requirement: The PSG MUST ensure all node IDs are unique within their type

Enforcement:

async putNode<T>(type: string, id: string, data: T): Promise<void> {
// UUID v4 format validation
if (!isValidUUIDv4(id)) {
throw new Error(`Invalid ID format: ${id}`);
}

await this.storage.put(type, id, data);
}

6.3 Status Transition Validation

Requirement: The PSG MUST validate status transitions for lifecycle objects

Valid Transitions:

ObjectValid Transitions
Plandraft → proposed → approved → in_progress → completed/failed
Steppending → running → completed/failed/skipped
Confirmpending → approved/rejected/cancelled
Tracerecording → completed

Enforcement:

async updateStatus(type: string, id: string, newStatus: string): Promise<void> {
const node = await this.getNode(type, id);
const currentStatus = node.status;

if (!this.isValidTransition(type, currentStatus, newStatus)) {
throw new Error(
`Invalid status transition: ${type} cannot go from ${currentStatus} to ${newStatus}`
);
}

node.status = newStatus;
await this.putNode(type, id, node);
}

6.4 Read-After-Write Consistency

Requirement: The PSG MUST provide read-after-write consistency

Test:

// Write
await psg.putNode('Plan', 'plan-123', { plan_id: 'plan-123', status: 'draft' });

// Read (MUST return the plan just written)
const plan = await psg.getNode('Plan', 'plan-123');
assert(plan.status === 'draft'); // MUST pass

6.5 Event Emission

Requirement: The PSG SHALL emit events for all state changes

Events:

  • GraphUpdateEvent — Emitted on node/edge changes
  • Maps to graph_update event family in observability

Example:

async putNode<T>(type: string, id: string, data: T): Promise<void> {
const existing = await this.getNode(type, id);

await this.storage.put(type, id, data);

// Emit event
await this.eventBus.emit({
event_family: 'graph_update',
event_type: existing ? 'node_updated' : 'node_created',
payload: {
node_type: type,
node_id: id,
old_value: existing,
new_value: data
}
});
}

7. Cross-Module Bindings

ModulePSG PathOperations
Contextpsg.contexts/<context_id>CRUD for Context objects
Planpsg.plans/<plan_id>CRUD + status transitions
Tracepsg.traces/<trace_id>Append-only segments
Confirmpsg.confirms/<confirm_id>Status + decisions
Rolepsg.roles/<role_id>Capabilities lookup
Collabpsg.collabs/<collab_id>Session state
Dialogpsg.dialogs/<dialog_id>Turn append
Extensionpsg.extensions/<ext_id>Tool registry
Networkpsg.networks/<net_id>Topology

8. VSL Relationship

8.1 PSG over VSL

PSG is a logical layer over VSL (physical storage):

┌─────────────────────────────────────┐
│ PSG (Semantic) │
│ Nodes, Edges, Queries, Validation │
├─────────────────────────────────────┤
│ VSL (Physical) │
│ K-V Store, Event Log, Snapshots │
└─────────────────────────────────────┘

8.2 Translation

// PSG operation (semantic)
const plan = await psg.getNode('Plan', 'plan-123');

// Translates to VSL operation (physical)
const plan = await vsl.get('plans/plan-123');

9. Invariants

9.1 Parent Existence

Invariant: A child node MUST NOT exist without its parent

ChildParent Requirement
PlanContext MUST exist
StepPlan MUST exist
TraceContext MUST exist
ConfirmTarget (Plan/Step) MUST exist

9.2 Terminal State Immutability

Invariant: Nodes in terminal states MUST NOT be modified

ObjectTerminal States
Plancompleted, failed, cancelled
Stepcompleted, failed, skipped
Tracecompleted

9.3 ID Immutability

Invariant: Node IDs MUST NOT change after creation

10. Governance Considerations

10.1 Auditability

PSG provides complete audit trail via:

  • GraphUpdateEvent for all changes
  • Trace segments for execution history
  • Immutable terminal states

10.2 Portability

PSG state can be exported/imported via VSL:

// Export PSG
const state = {
contexts: await psg.query({ type: 'Context' }),
plans: await psg.query({ type: 'Plan' }),
traces: await psg.query({ type: 'Trace' })
};

// Import PSG
for (const context of state.contexts) {
await psg.putNode('Context', context.context_id, context);
}

10.3 Drift Detection

PSG enables drift detection by comparing expected vs. actual state:

async function detectDrift(psg: PSG, expectedHash: string): Promise<DriftResult> {
const currentHash = await psg.getStateHash();

if (currentHash === expectedHash) {
return { hasDrift: false, driftedNodes: [] };
}

// Identify specific drift
const driftedNodes = await psg.compareState(expectedHash);
return { hasDrift: true, driftedNodes };
}

Architecture:

Runtime Components:

Observability:


Core Interface: getNode(), putNode(), query()
Reference Implementation: InMemoryPSG
Invariants: Parent existence, Terminal immutability, ID immutability
Relationship: PSG (semantic) over VSL (physical)