2121 Edge ,
2222 EdgeKind ,
2323 EvidenceChain ,
24+ MaintenanceResult ,
2425 Node ,
2526 NodeKind ,
2627 SearchResult ,
@@ -229,7 +230,7 @@ async def add(
229230 title : str ,
230231 content : str ,
231232 * ,
232- kind : NodeKind | None = None ,
233+ kind : str | NodeKind | None = None ,
233234 tags : list [str ] | None = None ,
234235 source : str = "" ,
235236 embedding : list [float ] | None = None ,
@@ -363,6 +364,16 @@ async def agent_search(
363364 depth = depth ,
364365 )
365366
367+ async def list (
368+ self ,
369+ * ,
370+ kind : str | NodeKind | None = None ,
371+ level : ConsolidationLevel | None = None ,
372+ limit : int = 100 ,
373+ ) -> list [Node ]:
374+ """List all nodes with optional kind/level filtering."""
375+ return await self ._backend .list_nodes (kind = kind , level = level , limit = limit )
376+
366377 async def get (self , node_id : str ) -> Node | None :
367378 cached = self ._cache .get (node_id )
368379 if cached is not None :
@@ -376,6 +387,39 @@ async def get(self, node_id: str) -> Node | None:
376387 self ._cache .put (node )
377388 return node
378389
390+ async def update (
391+ self ,
392+ node_id : str ,
393+ * ,
394+ title : str | None = None ,
395+ content : str | None = None ,
396+ kind : str | NodeKind | None = None ,
397+ tags : list [str ] | None = None ,
398+ properties : dict [str , str ] | None = None ,
399+ embedding : list [float ] | None = None ,
400+ ) -> Node | None :
401+ """Update a node's fields by ID. Returns updated node, or None if not found."""
402+ node = await self ._backend .get_node (node_id )
403+ if node is None :
404+ return None
405+ if title is not None :
406+ node .title = title
407+ if content is not None :
408+ node .content = content
409+ if kind is not None :
410+ node .kind = kind
411+ if tags is not None :
412+ node .tags = tags
413+ if properties is not None :
414+ node .properties = properties
415+ if embedding is not None :
416+ node .embedding = embedding
417+ node .updated_at = time ()
418+ await self ._backend .update_node (node )
419+ self ._cache .invalidate (node_id )
420+ self ._cache .put (node )
421+ return node
422+
379423 async def remove (self , node_id : str ) -> bool :
380424 node = await self ._backend .get_node (node_id )
381425 if node is None :
@@ -408,6 +452,20 @@ async def decay(self) -> int:
408452 self ._cache .clear () # Vitality changed globally
409453 return await self ._backend .decay_vitality (factor = 0.95 )
410454
455+ async def maintain (
456+ self ,
457+ digester : Digester | None = None ,
458+ * ,
459+ context : dict [str , object ] | None = None ,
460+ ) -> MaintenanceResult :
461+ """Run consolidate + decay + prune in one call with a unified result."""
462+ consolidated = await self ._consolidation .consolidate (
463+ self ._backend , digester , context = context ,
464+ )
465+ decayed = await self .decay ()
466+ pruned = await self .prune ()
467+ return MaintenanceResult (consolidated = consolidated , decayed = decayed , pruned = pruned )
468+
411469 async def export_markdown (self , * , node_ids : list [str ] | None = None ) -> str :
412470 return await self ._md_exporter .export (self ._backend , node_ids = node_ids )
413471
@@ -537,6 +595,79 @@ async def build_evidence(
537595 self ._backend , query , search_result , max_steps = max_steps ,
538596 )
539597
598+ # --- Conversation helpers ---
599+
600+ async def add_turn (
601+ self ,
602+ user_msg : str ,
603+ assistant_msg : str ,
604+ * ,
605+ session_id : str | None = None ,
606+ tags : list [str ] | None = None ,
607+ ) -> tuple [Node , Node , Node ]:
608+ """Add a conversation turn (user + assistant) linked to a session.
609+
610+ Creates a SESSION node on first call for a given session_id.
611+ Returns (session_node, user_node, assistant_node).
612+ """
613+ from synaptic .models import _new_id # noqa: PLC0415
614+
615+ if session_id is None :
616+ session_id = f"session_{ _new_id ()} "
617+
618+ # Get or create session node
619+ session_node = await self ._backend .get_node (session_id )
620+ if session_node is None :
621+ session_node = await self ._store .add_node (
622+ f"Session { session_id [:8 ]} " ,
623+ "" ,
624+ kind = NodeKind .SESSION ,
625+ tags = ["_session" ],
626+ source = session_id ,
627+ )
628+ # Override the auto-generated ID with session_id
629+ await self ._backend .delete_node (session_node .id )
630+ session_node .id = session_id
631+ await self ._backend .save_node (session_node )
632+
633+ turn_tags = [* tags ] if tags else []
634+
635+ # Create user message node
636+ user_node = await self ._store .add_node (
637+ "user" , user_msg , kind = NodeKind .OBSERVATION , tags = [* turn_tags , "_turn_user" ],
638+ )
639+
640+ # Create assistant message node
641+ assistant_node = await self ._store .add_node (
642+ "assistant" , assistant_msg , kind = NodeKind .OBSERVATION , tags = [* turn_tags , "_turn_assistant" ],
643+ )
644+
645+ # Link: user → assistant (FOLLOWED_BY)
646+ await self ._store .add_edge (
647+ user_node .id , assistant_node .id , kind = EdgeKind .FOLLOWED_BY ,
648+ )
649+
650+ # Link: session → user (CONTAINS)
651+ await self ._store .add_edge (
652+ session_id , user_node .id , kind = EdgeKind .CONTAINS ,
653+ )
654+
655+ # Link last turn to this one (FOLLOWED_BY)
656+ session_edges = await self ._backend .get_edges (session_id , direction = "outgoing" )
657+ contained = [e for e in session_edges if e .kind == EdgeKind .CONTAINS and e .target_id != user_node .id ]
658+ if contained :
659+ # Find the most recent contained user node
660+ last_user_id = contained [- 1 ].target_id
661+ # Get the assistant node linked from last user
662+ last_edges = await self ._backend .get_edges (last_user_id , direction = "outgoing" )
663+ last_assistant = [e for e in last_edges if e .kind == EdgeKind .FOLLOWED_BY ]
664+ if last_assistant :
665+ await self ._store .add_edge (
666+ last_assistant [- 1 ].target_id , user_node .id , kind = EdgeKind .FOLLOWED_BY ,
667+ )
668+
669+ return session_node , user_node , assistant_node
670+
540671 # --- Ontology persistence ---
541672
542673 async def save_ontology (self ) -> None :
0 commit comments