Add reusable multi-session skill and five-page project workbench
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
"""Small stdio client for the supported Codex App Server protocol."""
|
||||
import json
|
||||
import queue
|
||||
import shutil
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
|
||||
|
||||
class AppServer:
|
||||
def __init__(self, executable=None):
|
||||
executable = executable or shutil.which('codex')
|
||||
if not executable:
|
||||
raise RuntimeError('Codex unavailable on this executor')
|
||||
self.process = subprocess.Popen([executable,'app-server','--stdio'], stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,text=True,encoding='utf-8',errors='replace')
|
||||
self.incoming = queue.Queue()
|
||||
self.next_id = 0
|
||||
self.notifications = []
|
||||
def read():
|
||||
for line in self.process.stdout:
|
||||
try:
|
||||
self.incoming.put(json.loads(line))
|
||||
except ValueError:
|
||||
continue
|
||||
self.incoming.put(None)
|
||||
threading.Thread(target=read,daemon=True).start()
|
||||
self.call('initialize', {'clientInfo':{'name':'project_agent_control','version':'1.0'},
|
||||
'capabilities':{'experimentalApi':True}})
|
||||
self.send({'method':'initialized','params':{}})
|
||||
|
||||
def send(self, value):
|
||||
self.process.stdin.write(json.dumps(value,ensure_ascii=True)+'\n')
|
||||
self.process.stdin.flush()
|
||||
|
||||
def call(self, method, params, timeout=30):
|
||||
self.next_id += 1
|
||||
request_id = self.next_id
|
||||
self.send({'id':request_id,'method':method,'params':params})
|
||||
deadline=time.monotonic()+timeout
|
||||
while True:
|
||||
try:
|
||||
event=self.incoming.get(timeout=max(0.01,deadline-time.monotonic()))
|
||||
except queue.Empty:
|
||||
raise RuntimeError('App Server request timed out; outcome must be inspected') from None
|
||||
if event is None:
|
||||
raise RuntimeError('App Server stopped')
|
||||
if event.get('id') == request_id and 'method' not in event:
|
||||
if 'error' in event:
|
||||
# Surface protocol code only, never arbitrary environment output.
|
||||
raise RuntimeError('App Server rejected request: '+str(event['error'].get('code')))
|
||||
return event['result']
|
||||
if 'id' in event and 'method' in event:
|
||||
self.send({'id':event['id'],'error':{'code':-32601,'message':'Interactive approval must be handled by the human operator'}})
|
||||
else:
|
||||
self.notifications.append(event)
|
||||
|
||||
def history(self, thread_id):
|
||||
meta=self.call('thread/read',{'threadId':thread_id,'includeTurns':False})['thread']
|
||||
cursor=None
|
||||
turns=[]
|
||||
while True:
|
||||
params={'threadId':thread_id,'limit':100,'sortDirection':'asc','itemsView':'full'}
|
||||
if cursor:
|
||||
params['cursor']=cursor
|
||||
result=self.call('thread/turns/list',params)
|
||||
turns.extend(result.get('data',[]))
|
||||
cursor=result.get('nextCursor')
|
||||
if not cursor:
|
||||
break
|
||||
self.notifications.clear()
|
||||
return meta,turns
|
||||
|
||||
def close(self):
|
||||
if self.process.poll() is None:
|
||||
self.process.terminate()
|
||||
try:
|
||||
self.process.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.process.kill()
|
||||
self.process.wait(timeout=10)
|
||||
|
||||
def __enter__(self): return self
|
||||
def __exit__(self,*args): self.close()
|
||||
Reference in New Issue
Block a user