1919import asyncio
2020import logging
2121import os
22- import sys
2322from datetime import datetime , timezone
2423
2524import numpy as np
@@ -234,16 +233,19 @@ async def _embed_text(memory, text: str) -> np.ndarray:
234233 return np .array (vec , dtype = np .float32 )
235234
236235
237- async def cognitive_loop (config , layers , memory , shutdown_event , dmn_queue = None ):
236+ async def cognitive_loop (config , layers , memory , shutdown_event , input_queue : asyncio . Queue ):
238237 """The main attention-agnostic cognitive loop.
239238
240239 All input sources feed the same pipeline via AttentionAllocator.
241240 The cognitive state report enables metacognition through a single
242241 context window — Python pre-processing is subconscious, the LLM
243242 call is conscious, injection is the bridge.
244243
244+ Peripherals (stdin, Telegram, DMN, etc.) push AttentionCandidate
245+ objects into the shared input_queue. The loop drains it each cycle.
246+
245247 Args:
246- dmn_queue: Optional asyncio.Queue of DMN AttentionCandidate objects
248+ input_queue: Unified asyncio.Queue — all peripherals push here
247249 produced by the idle loop. Consumed when no user input.
248250 """
249251 logger .info ("Cognitive loop started. Awaiting input..." )
@@ -317,71 +319,77 @@ async def cognitive_loop(config, layers, memory, shutdown_event, dmn_queue=None)
317319 while not shutdown_event .is_set ():
318320 try :
319321 # ── Collect input candidates ──────────────────────────────
322+ # Drain unified input queue — all peripherals push here.
320323
321- # Read user input (non-blocking to allow shutdown)
322- print ("you> " , end = "" , flush = True )
323- loop = asyncio .get_event_loop ()
324324 try :
325- user_input = await asyncio .wait_for (
326- loop .run_in_executor (None , sys .stdin .readline ),
327- timeout = 1.0 ,
325+ candidate = await asyncio .wait_for (
326+ input_queue .get (), timeout = 1.0 ,
328327 )
329328 except asyncio .TimeoutError :
330- # No user input — check DMN queue for internal candidates
331- has_dmn = False
332- if dmn_queue :
333- while not dmn_queue .empty ():
334- try :
335- dmn_candidate = dmn_queue .get_nowait ()
336- attention .add_candidate (dmn_candidate )
337- has_dmn = True
338- except asyncio .QueueEmpty :
339- break
340- if not has_dmn :
329+ # Nothing arrived — check if there are leftover losers
330+ if attention .queue_size == 0 :
341331 continue
342- # DMN candidates available — fall through to attention allocation
343- user_input = None
344- else :
345- user_input = user_input .strip () if user_input else None
346-
347- # ── Handle user input (if present) ─────────────────────────
348- if user_input :
349- if user_input .lower () in ("exit" , "quit" , "/quit" ):
350- logger .info ("Final scratch flush before shutdown..." )
351- await _flush_scratch_through_exit_gate (
352- exit_gate , memory , layers , conversation ,
353- outcome_tracker = outcome_tracker , gut = gut ,
354- )
355- logger .info ("User requested shutdown." )
356- shutdown_event .set ()
332+ # Losers from previous cycle still in queue — process them
333+ candidate = None
334+
335+ # Process first candidate + drain remaining
336+ got_input = False
337+ candidates_this_cycle = []
338+ if candidate is not None :
339+ candidates_this_cycle .append (candidate )
340+ while not input_queue .empty ():
341+ try :
342+ extra = input_queue .get_nowait ()
343+ candidates_this_cycle .append (extra )
344+ except asyncio .QueueEmpty :
357345 break
358346
359- # ── Introspection commands ────────────────────────────
360- if user_input .startswith ("/" ):
361- handled = await _handle_command (
362- user_input , config , layers , memory , model_name ,
363- conversation , exchange_count , entry_gate , exit_gate ,
364- attention , gut = gut , bootstrap = bootstrap ,
365- )
366- if handled :
367- continue
347+ # Handle each candidate
348+ should_break = False
349+ for cand in candidates_this_cycle :
350+ if "external" in cand .source_tag :
351+ text = cand .content .strip ()
352+ reply_fn = cand .metadata .get ("reply_fn" )
353+
354+ # ── Quit command ──────────────────────────────
355+ if text .lower () in ("exit" , "quit" , "/quit" ):
356+ logger .info ("Final scratch flush before shutdown..." )
357+ await _flush_scratch_through_exit_gate (
358+ exit_gate , memory , layers , conversation ,
359+ outcome_tracker = outcome_tracker , gut = gut ,
360+ )
361+ logger .info ("User requested shutdown." )
362+ shutdown_event .set ()
363+ should_break = True
364+ break
365+
366+ # ── Introspection commands ────────────────────
367+ if text .startswith ("/" ):
368+ handled = await _handle_command (
369+ text , config , layers , memory , model_name ,
370+ conversation , exchange_count , entry_gate , exit_gate ,
371+ attention , gut = gut , bootstrap = bootstrap ,
372+ reply_fn = reply_fn ,
373+ )
374+ if handled :
375+ continue
368376
369- # ── Add user message as attention candidate ───────── ──
370- try :
371- user_embedding = await _embed_text ( memory , user_input )
372- except Exception as e :
373- logger . warning ( f"Failed to embed user input: { e } " )
374- user_embedding = None
377+ # ── Embed if needed (stdin peripheral skips embedding) ──
378+ if cand . embedding is None :
379+ try :
380+ cand . embedding = await _embed_text ( memory , text )
381+ except Exception as e :
382+ logger . warning ( f"Failed to embed input: { e } " )
375383
376- user_candidate = AttentionCandidate (
377- content = user_input ,
378- source_tag = "external_user" ,
379- embedding = user_embedding ,
380- )
381- attention . add_candidate ( user_candidate )
384+ # Add to attention allocator (external or internal)
385+ attention . add_candidate ( cand )
386+ got_input = True
387+
388+ if should_break :
389+ break
382390
383- elif not user_input and attention .queue_size == 0 :
384- # No user input and no DMN candidates — nothing to process
391+ if not got_input and attention .queue_size == 0 :
392+ # No input and no pending candidates — nothing to process
385393 continue
386394
387395 # ── Attention allocation ──────────────────────────────────
@@ -623,10 +631,14 @@ async def _call_retry():
623631
624632 # ── Output ────────────────────────────────────────────────
625633
626- # For external_user sources, print the reply
634+ # Route reply to the peripheral that sent the input
627635 if "external" in winner .source_tag :
628636 prefix = "agent" if not escalated else "agent[S2]"
629- print (f"\n { prefix } > { reply } \n " )
637+ reply_fn = winner .metadata .get ("reply_fn" )
638+ if reply_fn is not None :
639+ await reply_fn (f"{ prefix } > { reply } " )
640+ else :
641+ print (f"\n { prefix } > { reply } \n " )
630642 else :
631643 # Internal thoughts — log only, don't print
632644 logger .info (f"Internal thought response: { reply [:200 ]} " )
@@ -766,97 +778,119 @@ async def _handle_command(
766778 config , layers , memory , model_name ,
767779 conversation , exchange_count , entry_gate , exit_gate ,
768780 attention , gut = None , bootstrap = None ,
781+ reply_fn = None ,
769782) -> bool :
770783 """Handle introspection commands. Returns True if handled."""
771784
785+ async def _send (text : str ):
786+ """Route output to the peripheral that sent the command."""
787+ if reply_fn is not None :
788+ await reply_fn (text )
789+ else :
790+ print (text )
791+
772792 if command == "/identity" :
773- print ("\n " + layers .render_identity_full () + "\n " )
793+ await _send ("\n " + layers .render_identity_full () + "\n " )
774794 return True
775795
776796 if command == "/identity-hash" :
777- print ("\n " + layers .render_identity_hash () + "\n " )
797+ await _send ("\n " + layers .render_identity_hash () + "\n " )
778798 return True
779799
780800 if command == "/containment" :
781- print (f"\n Trust level: { config .containment .trust_level } " )
782- print (f"Self-spawn: { config .containment .self_spawn } " )
783- print (f"Self-migration: { config .containment .self_migration } " )
784- print (f"Network: { config .containment .network_mode } " )
785- print (f"Allowed endpoints: { config .containment .allowed_endpoints } " )
786- print (f"Can modify containment: { config .containment .can_modify_containment } \n " )
801+ lines = [
802+ f"\n Trust level: { config .containment .trust_level } " ,
803+ f"Self-spawn: { config .containment .self_spawn } " ,
804+ f"Self-migration: { config .containment .self_migration } " ,
805+ f"Network: { config .containment .network_mode } " ,
806+ f"Allowed endpoints: { config .containment .allowed_endpoints } " ,
807+ f"Can modify containment: { config .containment .can_modify_containment } \n " ,
808+ ]
809+ await _send ("\n " .join (lines ))
787810 return True
788811
789812 if command == "/status" :
790- print (f"\n Agent: { layers .manifest .get ('agent_id' )} " )
791- print (f"Phase: { layers .manifest .get ('phase' )} " )
792- print (f"System 1: { model_name } " )
793- print (f"Layer 0: v{ layers .layer0 .get ('version' )} , { len (layers .layer0 .get ('values' , []))} values" )
794- print (f"Layer 1: v{ layers .layer1 .get ('version' )} , { len (layers .layer1 .get ('active_goals' , []))} goals" )
795813 mc = await memory .memory_count ()
796- print (f"Memories: { mc } " )
797- print (f"Conversation: { len (conversation )} messages" )
798- print (f"Exchanges since flush: { exchange_count } /{ EXIT_GATE_FLUSH_INTERVAL } " )
799- print (f"Attention queue: { attention .queue_size } pending" )
800- print (f"Gut: { gut .gut_summary ()} \n " )
814+ lines = [
815+ f"\n Agent: { layers .manifest .get ('agent_id' )} " ,
816+ f"Phase: { layers .manifest .get ('phase' )} " ,
817+ f"System 1: { model_name } " ,
818+ f"Layer 0: v{ layers .layer0 .get ('version' )} , { len (layers .layer0 .get ('values' , []))} values" ,
819+ f"Layer 1: v{ layers .layer1 .get ('version' )} , { len (layers .layer1 .get ('active_goals' , []))} goals" ,
820+ f"Memories: { mc } " ,
821+ f"Conversation: { len (conversation )} messages" ,
822+ f"Exchanges since flush: { exchange_count } /{ EXIT_GATE_FLUSH_INTERVAL } " ,
823+ f"Attention queue: { attention .queue_size } pending" ,
824+ f"Gut: { gut .gut_summary ()} \n " ,
825+ ]
826+ await _send ("\n " .join (lines ))
801827 return True
802828
803829 if command == "/gate" :
804- print (f"\n Entry gate stats: { entry_gate .stats } " )
805- print (f"Exit gate stats: { exit_gate .stats } " )
806- print (f"Exchanges since flush: { exchange_count } /{ EXIT_GATE_FLUSH_INTERVAL } \n " )
830+ lines = [
831+ f"\n Entry gate stats: { entry_gate .stats } " ,
832+ f"Exit gate stats: { exit_gate .stats } " ,
833+ f"Exchanges since flush: { exchange_count } /{ EXIT_GATE_FLUSH_INTERVAL } \n " ,
834+ ]
835+ await _send ("\n " .join (lines ))
807836 return True
808837
809838 if command == "/memories" :
810839 mc = await memory .memory_count ()
811- print ( f"\n Total memories: { mc } " )
840+ lines = [ f"\n Total memories: { mc } " ]
812841 if mc > 0 :
813842 rows = await memory .pool .fetch (
814843 "SELECT id, content, importance, confidence, created_at "
815844 "FROM memories ORDER BY created_at DESC LIMIT 5"
816845 )
817846 for r in rows :
818- print (f" [{ r ['id' ]} ] imp={ r ['importance' ]:.2f} "
819- f"conf={ r ['confidence' ]:.2f} | { r ['content' ][:70 ]} " )
820- print ()
847+ lines .append (
848+ f" [{ r ['id' ]} ] imp={ r ['importance' ]:.2f} "
849+ f"conf={ r ['confidence' ]:.2f} | { r ['content' ][:70 ]} "
850+ )
851+ lines .append ("" )
852+ await _send ("\n " .join (lines ))
821853 return True
822854
823855 if command == "/flush" :
824- print ("Forcing scratch flush through exit gate..." )
856+ await _send ("Forcing scratch flush through exit gate..." )
825857 await _flush_scratch_through_exit_gate (
826858 exit_gate , memory , layers , conversation ,
827859 outcome_tracker = getattr (memory , 'outcome_tracker' , None ),
828860 gut = gut ,
829861 )
830- print (f"Done. Exit gate stats: { exit_gate .stats } \n " )
862+ await _send (f"Done. Exit gate stats: { exit_gate .stats } \n " )
831863 return True
832864
833865 if command == "/attention" :
834- print (f"\n Attention queue: { attention .queue_size } pending candidates" )
835866 centroid = attention .attention_centroid
836- print (f"Attention centroid: { 'computed' if centroid is not None else 'none (no history)' } " )
837867 prev = attention .previous_attention_embedding
838- print (f"Previous embedding: { 'set' if prev is not None else 'none' } \n " )
868+ lines = [
869+ f"\n Attention queue: { attention .queue_size } pending candidates" ,
870+ f"Attention centroid: { 'computed' if centroid is not None else 'none (no history)' } " ,
871+ f"Previous embedding: { 'set' if prev is not None else 'none' } \n " ,
872+ ]
873+ await _send ("\n " .join (lines ))
839874 return True
840875
841876 if command == "/cost" :
842877 from .llm import energy_tracker
843- print (f"\n { energy_tracker .detailed_report ()} \n " )
878+ await _send (f"\n { energy_tracker .detailed_report ()} \n " )
844879 return True
845880
846881 if command == "/readiness" :
847882 if bootstrap is not None :
848883 await bootstrap .check_all (memory , layers )
849- print (f"\n { bootstrap .render_status ()} \n " )
884+ await _send (f"\n { bootstrap .render_status ()} \n " )
850885 else :
851- print ("\n [Bootstrap not initialized]\n " )
886+ await _send ("\n [Bootstrap not initialized]\n " )
852887 return True
853888
854889 if command .startswith ("/docs" ):
855890 # §4.10: Agent reads own docs (read-only access to repo)
856891 parts = command .split (maxsplit = 1 )
857892 if len (parts ) < 2 :
858- print ("\n Available docs: notes.md, DOCUMENTATION.md, src/*.py" )
859- print ("Usage: /docs <filename>\n " )
893+ await _send ("\n Available docs: notes.md, DOCUMENTATION.md, src/*.py\n Usage: /docs <filename>\n " )
860894 else :
861895 import pathlib
862896 repo_root = pathlib .Path (__file__ ).resolve ().parent .parent
@@ -865,17 +899,17 @@ async def _handle_command(
865899 try :
866900 target_path = (repo_root / target ).resolve ()
867901 if not str (target_path ).startswith (str (repo_root )):
868- print ("\n [Access denied: path outside repo]\n " )
902+ await _send ("\n [Access denied: path outside repo]\n " )
869903 elif not target_path .exists ():
870- print (f"\n [File not found: { target } ]\n " )
904+ await _send (f"\n [File not found: { target } ]\n " )
871905 elif target_path .is_dir ():
872906 files = sorted (p .name for p in target_path .iterdir () if p .is_file ())
873- print (f"\n Files in { target } /: { ', ' .join (files [:20 ])} \n " )
907+ await _send (f"\n Files in { target } /: { ', ' .join (files [:20 ])} \n " )
874908 else :
875909 content = target_path .read_text ()[:4000 ]
876- print (f"\n --- { target } ---\n { content } \n --- end ---\n " )
910+ await _send (f"\n --- { target } ---\n { content } \n --- end ---\n " )
877911 except Exception as e :
878- print (f"\n [Error reading { target } : { e } ]\n " )
912+ await _send (f"\n [Error reading { target } : { e } ]\n " )
879913 return True
880914
881915 return False
0 commit comments