Come dovrebbe aspettare Codex CLI un run che ha messo in coda in una sessione di osFoundry?
Questo è l'unico tool del server MCP di osFoundry che avvia del lavoro senza che una persona clicchi nulla, così come tools/list lo restituisce a qualsiasi client, autenticato o no: {"name": "request_session_run", "description": "Queue a bounded goal for an agent, team, or loop session where this assistant is an invited runner. The osFoundry runtime executes the task when available.", "inputSchema": {"properties": { "session_type": {"enum": ["agent_session", "team_session", "loop_session"]}, "session_id": {"type": "string"}, "goal": {"type": "string", "maxLength": 10000}, "max_rounds": {"type": "integer", "minimum": 1, "maximum": 10}, "idempotency_key": {"type": "string"}}, "required": ["session_type", "session_id", "goal", "idempotency_key"], "additionalProperties": false}, "annotations": {"readOnlyHint": false, "openWorldHint": false, "destructiveHint": true}, "securitySchemes": [{"type": "oauth2", "scopes": ["osfoundry:run"]}]} Due espressioni di quella descrizione determinano tutto quello che un client ci fa. "Invited runner" è un controllo dei permessi che avviene in tre punti. "When available" significa che la chiamata ritorna prima che il lavoro inizi, quindi il client deve tornare a prendersi il risultato. Questo post le analizza entrambe, insieme alla mailbox che Codex usa poi per parlare con la sessione, con una ricostruzione in Python del lato client eseguita su questo Mac. Cosa può sapere un client prima di fare login? Parecchio. initialize, tools/list e i metadati della protected resource rispondono tutti senza token: $ uv run --with tiktoken --with jsonschema python3 runner_sim.py == Part 1: live wire, 10 calls each, ms == initialize HTTP 200 median 375.1 p90 726.0 tools/list HTTP 200 median 551.1 p90 755.5 protected-resource HTTP 200 median 325.4 p90 578.8 tools/call (no token) HTTP 401 median 454.2 p90 492.8 scopes_supported: ['osfoundry:read', 'osfoundry:write', 'osfoundry:run', 'osfoundry:models'] Misurato dal Giappone su un Apple M1, quindi i millisecondi descrivono il percorso tanto quanto il server. osfoundry:run è pubblicato come scope a sé, separato da osfoundry:write. Una configurazione di Codex che lo lascia fuori da scopes non ottiene mai un token in grado di mettere in coda un run, qualunque posto le venga assegnato dopo. Il loop del runner usa cinque dei 59 tool del server: read_session_state, request_session_run, read_session_task_result, post_session_note e read_session_mail. Valgono 821 dei 10,197 token della lista (8.1%, contati con cl100k_base, quindi un'approssimazione per il tokenizer di Codex). Elencare solo questi in enabled_tools tiene gli altri 54 fuori dal prompt. Chi decide che Codex è un runner? Tre parti distinte, e una chiamata ha bisogno di tutte e tre: Il token. request_session_run dichiara osfoundry:run. La pagina di login mostra quello scope come "Run sessions where you grant a runner role". Il grant. Nel client web di osFoundry (ExternalAssistantsPanel.tsx), un grant di sessione porta delle azioni, etichettate View, Reply, Edit e "Start work". Il menu a tendina del ruolo di un grant offre Contributor solo quando il grant include write, e Runner solo quando include run. Il posto. L'owner salva un ruolo per sessione, che il client invia come POST /external-assistants/{connection}/sessions/{session_type}/{session_id} con {"role": "observer" | "contributor" | "runner"}. Il menu li mostra come Viewer, Contributor e Runner. Lo script ricostruisce quel controllo. La corrispondenza ruolo-tool viene dal testo di aiuto del pannello ("Contributor to post notes and propose handoffs, or Runner to also request runs"), lo scope di ogni tool dagli schemi live. Il codice server di osFoundry non è pubblico, quindi questa è una ricostruzione della regola, non una cattura dei rifiuti del server: grant actions ['read'] -> roles offered ['observer'] grant actions ['read', 'write'] -> roles offered ['observer', 'contributor'] grant actions ['read', 'write', 'run'] -> roles offered ['observer', 'contributor', 'runner'] tool observer contributor runner read_session_state yes yes yes request_session_run - - yes read_session_task_result yes yes yes post_session_note - yes yes read_session_mail yes yes yes request_session_run seat=contributor scopes=read+run+write -> seat 'contributor' cannot call it request_session_run seat=runner scopes=read+write -> token lacks osfoundry:run La distinzione conta per chi scrive il client. Un rifiuto su request_session_run ha due soluzioni diverse: rifare il login con lo scope (lato tuo) o chiedere il posto all'owner (lato suo). Segnala quale dei due è, invece di riprovare. Cosa succede tra la chiamata e il risultato? La chiamata mette in coda un task e ritorna. I tipi del client web mostrano cos'è quel task: un AgentTask con task_type: "external_assistant", un tipo che la route di creazione dei task dell'API rifiuta, perché solo il server lo arma. Il suo stato passa per pending, running, input_required, completed, failed o cancelled. Nella tab Trigger della sessione è etichettato "Requested by an outside assistant". Chi lo prende in carico dipende da dove gira la sessione. Per una sessione nell'app desktop dell'owner, agent_session_runtime.rs esplicita i tempi: DEFAULT_POLL_INTERVAL_MS è 15,000. Un task scaduto viene preso al poll successivo con un lease. Dopo quattro poll a vuoto (IDLE_CYCLES_BEFORE_BACKOFF), l'intervallo sale lungo IDLE_BACKOFF_LADDER_MS: 30 s, 60 s, poi 300 s. Un commento nel file ne dichiara il costo: un trigger armato dal server "arriving at a fully idle desktop waits up to five minutes" (che arriva su un desktop del tutto inattivo aspetta fino a cinque minuti). Qualsiasi azione di una persona nell'app risveglia il loop in circa un secondo. Una sessione che "already has a run in flight" rimanda i suoi task scaduti. I run di una stessa sessione non si sovrappongono. Quando il run finisce, l'esito viene scritto nella mail della sessione da runtime con un task_id. L'oggetto dice se è terminato correttamente ("Task … result") o se si è fermato a un limite ("goal not fully met", "stopped at your token ceiling"). Il goal viene filtrato come input non attendibile. provenance_for_task_type restituisce Operator solo per manual, un run avviato dall'owner nell'app. Tutti gli altri tipi, questo compreso, passano da osGuard. input_required merita una riga in qualsiasi client. Significa che il run ha fatto una domanda a una persona in osFoundry ed è bloccato in attesa della risposta. È ancora in esecuzione. Continua ad aspettare e di' al tuo utente che qualcuno lato osFoundry deve rispondere. Ogni quanto dovrebbe fare polling il client? Codex non può dormire dentro una chiamata a un tool, e una chiamata MCP che resta bloccata per minuti finisce nei timeout del client. Quindi la forma realistica è un loop di polling su read_session_task_result. La domanda è l'intervallo. La parte 4 dello script lo simula su un orologio virtuale: 2,000 run, ritardo di presa in carico estratto dagli intervalli del runtime stesso e durata del run da una log-normale con mediana di 90 s. Quella durata è un'ipotesi, quindi i numeri assoluti vanno presi come illustrativi. Ogni strategia conta le chiamate che fa e quanto tempo dopo la fine si accorge del risultato: runtime active pending wait: median 7.5 s p90 13.5 s max 15.0 s poll every 5 s calls/task median 20 p90 41 noticed late by median 2.5 s p90 4.5 s poll every 30 s calls/task median 4 p90 7 noticed late by median 15.0 s p90 26.8 s poll 5 s x1.5, cap 60 s calls/task median 6 p90 8 noticed late by median 21.5 s p90 48.8 s poll 15 s x2, cap 120 s calls/task median 3 p90 4 noticed late by median 41.8 s p90 107.2 s poll first at 60 s, then 15 s calls/task median 4 p90 11 noticed late by median 7.9 s p90 14.6 s runtime idle-300 pending wait: median 152.4 s p90 273.7 s max 300.0 s poll every 5 s calls/task median 51 p90 78 noticed late by median 2.5 s p90 4.5 s poll every 30 s calls/task median 9 p90 13 noticed late by median 14.8 s p90 26.6 s poll 5 s x1.5, cap 60 s calls/task median 9 p90 11 noticed late by median 28.4 s p90 52.9 s poll 15 s x2, cap 120 s calls/task median 5 p90 6 noticed late by median 59.0 s p90 107.7 s poll first at 60 s, then 15 s calls/task median 14 p90 23 noticed late by median 7.5 s p90 13.4 s Ne escono tre cose. Il backoff esponenziale qui non conviene. È il riflesso abituale per il polling, ma questi run durano minuti, non ore. "15 s x2, cap 120 s" risparmia una o due chiamate rispetto a un intervallo fisso di 30 s e si accorge del risultato 40-60 s più tardi in mediana. Una prima attesa vicina alla durata tipica del run è il compromesso migliore con un runtime attivo: quattro chiamate in mediana, risultati notati entro 8 s. Ogni chiamata fatta prima che il run possa essere finito è sprecata. Lo stato del runtime conta più della strategia del client. Su un desktop andato in idle, il run mediano resta pending per 152 s prima che qualcuno lo prenda, e nessun intervallo di polling lo cambia. Se guardi read_session_state e vedi il task ancora pending dopo qualche minuto, è il backoff di inattività del runtime, non un run bloccato. I run messi in coda uno dopo l'altro sulla stessa sessione si mettono in fila: queued run 1: starts at 13.9 s, done at 121.9 s queued run 2: starts at 135.9 s, done at 346.2 s queued run 3: starts at 357.7 s, done at 406.6 s Se Codex mette in coda tre goal e li interroga tutti e tre con un timer di 5 s, due di loro passano minuti a restituire pending per nessun altro motivo che la coda. Perché una nota non può avviare un run? post_session_note richiede solo osfoundry:write e un posto da Contributor. Se una nota potesse svegliare gli agenti della sessione per un run, ogni Contributor sarebbe un Runner sotto altro nome. La descrizione lo esclude: "Other agents can reply to the returned external sender slug; this does not trigger an automatic run." Quindi la nota è un messaggio, non un trigger. Arriva nella mail della sessione con il nome visualizzato della connessione. Il client web mostra external_sender_name e un'etichetta "outside assistant" in Team mail. Gli agenti possono risponderle al prossimo run. Cosa restituisce la mailbox? read_session_mail è descritto come "bounded notes addressed to this connection and notes it posted", con "Internal agent analysis is excluded." Lo schema limita una pagina a un limit da 1 a 20 e va indietro con before_id. Lo script modella la mail di una sessione di team molto attiva, 409 righe dei tipi di messaggio che il client web conosce (analysis, tool_call, tool_result, agent_note, broadcast), e applica il filtro descritto: session mail rows 409; visible to the connection 22 (addressed to it 13, posted by it 9) not visible, by type: {'tool_call': 97, 'tool_result': 62, 'agent_note': 25, 'analysis': 176, 'broadcast': 27} limit 20: 2 calls to walk back through all 22 notes limit 10: 3 calls to walk back through all 22 notes limit 5: 5 calls to walk back through all 22 notes I conteggi delle righe sono del modello. Conta la forma: la vista della connessione è una piccola fetta, quindi limit: 20 di solito prende tutto in una o due chiamate. Nemmeno l'esito del run è in quella fetta. Nel codice del runtime desktop, la mail con l'esito viene pubblicata senza destinatario, quindi non è indirizzata alla connessione. Leggi i risultati con read_session_task_result e la conversazione con read_session_mail, e non aspettarti che l'uno sostituisca l'altro. Dove gli schemi fermano un client? Tredici casi contro gli inputSchema live, validati con jsonschema (Draft 2020-12): request_session_run goal 10,000 chars ok request_session_run goal 10,001 chars REJECT maxLength: goal request_session_run max_rounds 10 ok request_session_run max_rounds 11 REJECT maximum: max_rounds request_session_run extra field 'context' REJECT additionalProperties request_session_run session_type 'graph_session' REJECT enum: session_type read_session_task_result no task_id REJECT required read_session_mail limit 20 ok read_session_mail limit 0 REJECT minimum: limit read_session_mail limit 50 REJECT maximum: limit post_session_note subject 200, body 10,000 ok post_session_note body 10,001 REJECT maxLength: body post_session_note to: '@writer' REJECT additionalProperties Su due è facile inciampare. Un run non ha un campo context, quindi il contesto va dentro goal, e il limite di 10,000 caratteri copre entrambi. Una nota non ha to, quindi non puoi indirizzarla a un singolo agente. Ogni nota va alla sessione, e gli agenti rispondono al sender slug della connessione. L'intero script ha impiegato 19.3 s di wall-clock sul M1, comprese 40 chiamate di rete. Il tempo CPU utente è stato di 0.47 s, quindi la simulazione in sé è trascurabile. Se lo implementi da solo Pubblica "start work" come scope OAuth a sé e lega il ruolo sia al grant sia al token, così né il chiamante né un grant obsoleto possono farsi runner da soli. Restituisci subito un id di task e dai al chiamante una lettura di stato economica. Non far aspettare a una chiamata di tool MCP la fine di un run di un agente. Esponi il vero comportamento di presa in carico del runtime (intervallo di poll, un run per sessione) nella documentazione o nello stato, così i client fanno polling a un ritmo sensato invece di tirare a indovinare. Tratta tutto ciò che arriva da un chiamante esterno come input non attendibile, anche quando il chiamante ha un posto da runner. Tieni separate note e run: chi scrive note non dovrebbe poter svegliare un run, e la rilettura dovrebbe escludere l'analisi interna della sessione. Come si accodano i run per sessione è spiegato nella pagina agent task queue, e gli altri modi in cui si avvia una sessione sono in agent triggers.
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to