okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
ff92766
main
1"""agents: spawn children, look up any session and read its messages, cancel 2or close your own children. 3Talking is mail.submit. Spawn returns once the child exists; its answer 4arrives later as mail and starts your next turn.""" 5 6from __future__import annotations 7 8import os 9import time 10from dataclasses import dataclass ,field 11from typing import cast 12 13from albedo_api import Host ,PythonApi ,Record 14 15host :Host 16_models_seen = False 17_last_progress = 0.0 18 19 20class MessagePage (Record ): 21"""page.content and page["content"] both work; next_offset is None at the end.""" 22 23seq :int 24offset :int 25content :str 26next_offset :int | None 27 28 29class MessageMatches (Record ): 30"""matches.rows is a list of {seq, preview}; read one with messages(seq=...).""" 31 32pattern :str 33offset :int 34count :int 35next_offset :int | None 36rows :list [dict ] 37 38 39@dataclass( frozen = True ) 40class Agent : 41"""A handle to one agent. `running` and `closed` are set on snapshots 42from get(), children(), siblings(), and sessions(); None on handles that 43were not looked up. `cwd`, `model`, `last_active` (unix seconds of its 44last answer), and `matches` are set only on snapshots from sessions(). 45`running=False` does not rule out a task waiting for kernel preparation.""" 46 47id :str 48name :str 49depth :int 50parent :"Agent | None" = None 51running :bool | None = None 52closed :bool | None = None 53cwd :str | None = None 54model :str | None = None 55last_active :int | None = None 56matches :list [dict ]| None = field (default = None ,compare = False ) 57 58async def spawn ( 59self , 60task :str , 61* , 62name :str , 63model :str , 64deliverable :str | None = None , 65evidence_bar :str | None = None , 66falsifier :str | None = None , 67 )-> "Agent" : 68"""Start a child with `task`. Returns at once; its answer comes as mail.""" 69if self .id != agents .self .id : 70raise PermissionError ( 71"only agents.self.spawn(...) starts children: you spawn your own" 72 ) 73if not _models_seen : 74raise RuntimeError ( 75"call await agents.models() first and pick a model from it" 76 ) 77for label ,value in (("task" ,task ), ("name" ,name ), ("model" ,model )): 78if not isinstance (value ,str )or not value .strip (): 79raise TypeError (f" { label } must be a non-empty str" ) 80brief = _brief ( 81task , 82name = name , 83deliverable = deliverable , 84evidence_bar = evidence_bar , 85falsifier = falsifier , 86 ) 87return _agent ( 88cast ( 89dict , 90await host ( 91"agents.spawn" , {"task" :brief ,"name" :name ,"model" :model } 92 ), 93 ) 94 ) 95 96async def children (self )-> list ["Agent" ]: 97if self .id != agents .self .id : 98raise PermissionError ( 99"children() lists your own: use agents.self.children()" 100 ) 101return [_agent (item )for item in cast (list ,await host ("agents.children" , {}))] 102 103async def siblings (self )-> list ["Agent" ]: 104if self .id != agents .self .id : 105raise PermissionError ( 106"siblings() lists your own: use agents.self.siblings()" 107 ) 108return [_agent (item )for item in cast (list ,await host ("agents.siblings" , {}))] 109 110async def cancel (self ,* ,tree :bool = False )-> bool : 111"""Stop this child's running turn; it keeps its session and work. With 112`tree=True` its own children, and theirs, are stopped too. True when a 113turn was running.""" 114stopped = await host ("agents.cancel" , {"id" :self .id ,"tree" :tree }) 115return bool (cast (list ,stopped )) 116 117async def close (self )-> bool : 118"""Done with this child: stop it, keep its messages and files, free its kernel.""" 119return bool (await host ("agents.close" , {"id" :self .id })) 120 121async def messages ( 122self ,seq :int = 0 ,* ,offset :int = 0 ,limit :int = 4000 123 )-> MessagePage : 124"""A page of this session's messages from row `seq` on (at most 8000 chars). 125Any session may read any other.""" 126return MessagePage ( 127cast ( 128dict , 129await host ( 130"agents.messages" , 131 {"id" :self .id ,"seq" :seq ,"offset" :offset ,"limit" :limit }, 132 ), 133 ) 134 ) 135 136async def search_messages ( 137self ,pattern :str ,* ,limit :int = 10 ,offset :int = 0 138 )-> MessageMatches : 139"""This session's message rows containing `pattern`, case-insensitively.""" 140return MessageMatches ( 141cast ( 142dict , 143await host ( 144"agents.search_messages" , 145 { 146"id" :self .id , 147"pattern" :pattern , 148"limit" :limit , 149"offset" :offset , 150 }, 151 ), 152 ) 153 ) 154 155async def delete (self )-> None : 156await host ("agents.delete" , {"id" :self .id }) 157 158 159def _agent (raw :dict )-> Agent : 160parent = raw .get ("parent" ) 161return Agent ( 162id = raw ["id" ], 163name = raw ["name" ], 164depth = raw ["depth" ], 165parent = _agent (parent )if isinstance (parent ,dict )else None , 166running = raw .get ("running" ), 167closed = raw .get ("closed" ), 168cwd = raw .get ("cwd" ), 169model = raw .get ("model" ), 170last_active = raw .get ("last_active" ), 171matches = raw .get ("matches" ), 172 ) 173 174 175def _directory (cwd :str | None )-> str : 176"""A cwd filter as sessions store it: absolute, or "" for every directory.""" 177return os .path .abspath (os .path .expanduser (cwd ))if cwd else "" 178 179 180def _brief ( 181task :str , 182* , 183name :str , 184deliverable :str | None , 185evidence_bar :str | None , 186falsifier :str | None , 187)-> str : 188parts = [f"TASK:\n { task . strip () } " ] 189if deliverable : 190parts .append ( 191f"DELIVERABLE: { deliverable . strip () } \nWrite it to { deliverable . strip () } .partial and rename it " 192"into place when finished, so a reader never sees half of it." 193 ) 194if evidence_bar : 195parts .append (f"EVIDENCE BAR:\n { evidence_bar . strip () } " ) 196if falsifier : 197parts .append (f"FALSIFIER:\n { falsifier . strip () } " ) 198parts .append ( 199'WHEN DONE: await mail.submit("parent", <short summary, with paths to anything large>).' 200 ) 201return "\n\n" .join (parts ) 202 203 204class Agents : 205self :Agent 206 207async def models (self )-> list [str ]: 208"""Models a child may run on. Required once before spawning.""" 209global _models_seen 210_models_seen = True 211return list (cast (list ,await host ("agents.models" , {}))) 212 213async def cancel_all (self ,* ,tree :bool = True )-> list [str ]: 214"""Stop the running turn of every one of your children, and with 215`tree=True` (the default) everything beneath them. Returns the ids of the 216sessions that were running. They keep their sessions and work.""" 217stopped = await host ("agents.cancel_all" , {"tree" :tree }) 218return list (cast (list ,stopped )) 219 220async def progress (self ,text :str )-> bool : 221"""A short status (≤512 chars) the agents view shows without starting a 222turn. At most one per 10 seconds; returns False when throttled.""" 223global _last_progress 224if not isinstance (text ,str )or not text .strip (): 225raise TypeError ("progress(text) takes a non-empty str" ) 226now = time .monotonic () 227if now - _last_progress < 10 : 228return False 229_last_progress = now 230return bool (await host ("agents.progress" , {"text" :text .strip ()})) 231 232async def get (self ,to :object )-> Agent : 233"""A live snapshot of "parent", a child or sibling by name, an agent 234handle, or any session id, resolved the way mail.submit resolves it.""" 235target = getattr (to ,"id" ,to ) 236if not isinstance (target ,str )or not target .strip (): 237raise TypeError ( 238'agents.get(to): to is "parent", a name, a session id, or an agent handle' 239 ) 240return _agent (cast (dict ,await host ("agents.get" , {"to" :target .strip ()}))) 241 242async def sessions ( 243self , 244query :str = "" , 245* , 246cwd :str | None = None , 247limit :int = 20 , 248offset :int = 0 , 249 )-> list [Agent ]: 250"""Every session, most recently active first (at most 100 per call). 251A `query` keeps those whose title or name contains it, or whose 252messages do, case-insensitively; each snapshot's `matches` then holds 253up to 3 of its newest matching rows as {seq, preview}, to read with 254messages(seq=...). Your own messages are not searched. With `cwd`, 255only sessions opened in that directory.""" 256found = await host ( 257"agents.sessions" , 258 { 259"query" :query , 260"cwd" :_directory (cwd ), 261"limit" :limit , 262"offset" :offset , 263 }, 264 ) 265return [_agent (item )for item in cast (list ,found )] 266 267def spawn (self ,* _ :object ,** __ :object )-> None : 268raise AttributeError ( 269"spawn lives on your handle: await agents.self.spawn(task, name=..., model=...)" 270 ) 271 272 273agents = Agents () 274 275 276async def setup (api :PythonApi )-> dict [str ,object ]: 277global host 278host = api .host 279agents .self = _agent (cast (dict ,await host ("agents.self" , {}))) 280return {"agents" :agents ,"Agent" :Agent ,"AgentsError" :api .HostError }