DEV Community

Cover image for osFoundry, Codex को handoff प्रस्तावित करने के लिए एक tool और run शुरू करने के लिए अलग tool क्यों देता है?
BBT for BBT हिन्दी

Posted on

osFoundry, Codex को handoff प्रस्तावित करने के लिए एक tool और run शुरू करने के लिए अलग tool क्यों देता है?

Sign in करने से पहले जब Codex CLI पहली बार osFoundry पर कोई session tool call करता है, तो उसे यह जवाब मिलता है:

POST /api/v1/external-mcp  {"method":"tools/call","params":{"name":"propose_session_handoff","arguments":{}}}

HTTP/2 401
www-authenticate: Bearer resource_metadata="https://api.osfoundry.io/.well-known/oauth-protected-resource/api/v1/external-mcp",
                  error="invalid_token", error_description="Connect osFoundry to continue"

{"jsonrpc":"2.0","id":3,"result":{"isError":true,
 "content":[{"type":"text","text":"Authentication required"}], …}}
Enter fullscreen mode Exit fullscreen mode

initialize और tools/list बिना token के जवाब देते हैं, यानी किसी के sign in करने से पहले ही client सभी 59 tools देख सकता है। इनमें से दो ऐसे लगते हैं जैसे एक ही काम करते हों:

{"name": "propose_session_handoff",
 "description": "Send a scoped task and context summary to the owner of an invited session. The owner must accept before a run is queued.",
 "required": ["session_type","session_id","goal","context_summary","requested_output","idempotency_key"],
 "annotations": {"readOnlyHint": false, "openWorldHint": true, "destructiveHint": true},
 "securitySchemes": [{"type": "oauth2", "scopes": ["osfoundry:write"]}]}

{"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.",
 "required": ["session_type","session_id","goal","idempotency_key"],
 "annotations": {"readOnlyHint": false, "openWorldHint": false, "destructiveHint": true},
 "securitySchemes": [{"type": "oauth2", "scopes": ["osfoundry:run"]}]}
Enter fullscreen mode Exit fullscreen mode

दोनों का नतीजा यही है कि कोई session काम करता है। इस post के बाकी हिस्से में बात है कि इन्हें अलग क्यों रखा गया है, हर एक पर कौन-सी सीमाएँ लगी हैं, और client side का एक Python rebuild जो इन सीमाओं को live schemas के against जाँचता है।

दोनों calls में फ़र्क क्या है?

हाँ कौन कहता है, और कब।

Handoff किसी इंसान से अभी पूछता है। Owner को notification मिलती है ("Codex CLI requests a handoff")। Session का Handoffs tab इसे "Outside assistant requests" (बाहरी assistants की requests) के नीचे दिखाता है, साथ में goal, एक खुलने वाला "Context and expected output" हिस्सा और लाइन "Up to N rounds on your agent runtime"। जब तक कोई Accept and run पर click नहीं करता, कुछ भी queue में नहीं जाता। Click होने पर web client {decision: "accept"} को /agent-sessions/{id}/external-handoffs/{handoff_id}/decision पर भेजता है, और toast में लिखा आता है "Handoff accepted and run queued"। Handoff की row pending से accepted, declined या cancelled में जाती है।

Run को पहले ही मंज़ूरी मिल चुकी होती है। Owner ने Connected tab में connection को Runner seat दी, और वही हाँ है। वहाँ का help text: "Invite it to a session as Contributor to post notes and propose handoffs, or Runner to also request runs. A handoff still needs the session owner's approval." Sign-in page पर run scope लिखा है "Run sessions where you grant a runner role"।

अगर आप दोनों को needs_approval flag वाले एक tool में मिला दें, तो caller तय करेगा कि किसी इंसान से पूछा जाए या नहीं। दो tools होने से यह फ़ैसला owner की तरफ़ रहता है, ऐसी दो जगहों पर जिन्हें caller छू नहीं सकता: OAuth scope (osfoundry:write बनाम osfoundry:run) और seat (Contributor बनाम Runner)। जिस Codex config में scopes के अंदर osfoundry:run नहीं है, वह Runner seat पर होते हुए भी run शुरू नहीं कर सकता।

Annotations भी इसी बँटवारे पर चलते हैं। propose_session_handoff में openWorldHint: true है, क्योंकि इसका output call के बाहर किसी इंसान तक पहुँचता है। request_session_run में openWorldHint: false है, क्योंकि यह सिर्फ़ उस session की queue को छूता है जिसने पहले से सहमति दी है। दोनों destructiveHint: true हैं, इसलिए Codex के default_tools_approval_mode = "writes" पर चलने वाला client दोनों में से किसी से भी पहले पूछता है।

Handoff में context summary ज़रूरी क्यों है?

क्योंकि कुछ भी चलने से पहले कोई इंसान उसे पढ़ता है। context_summary और requested_output handoff में ज़रूरी हैं और run में होते ही नहीं। Owner यह तय कर रहा है कि अपने session का runtime इस पर खर्च करे या नहीं, और सिर्फ़ "fix the export" से यह तय नहीं हो पाता। Runner के पास यह भरोसा पहले से है, इसलिए run को सिर्फ़ एक goal चाहिए।

Limits भी अलग हैं। Handoff का goal 5,000 characters पर रुकता है, साथ में 5,000 context के लिए और 1,000 requested output के लिए। Run का goal 10,000 तक जा सकता है। असल में run का goal ही context भी ले जाता है, क्योंकि उसके लिए कोई और field नहीं है।

Codex वापस क्या पढ़ सकता है?

Session में जितना है, उससे कम:

  • read_session_task_result: "the status and attributed runtime result for one task started by this connection"। किसी दूसरे connection का task id यहाँ काम नहीं आता।
  • read_session_mail: "notes addressed to this connection and notes it posted", हर page पर 1 से 20, पुराने notes के लिए before_id। "Internal agent analysis is excluded."
  • read_session_state: उस session का मौजूदा status और हाल के tasks, जिसमें connection जुड़ा है।
  • post_session_note: एक attributed note, जिसका जवाब agents "the returned external sender slug" के ज़रिए दे सकते हैं। यह "does not trigger an automatic run", इसलिए note के सहारे owner के click को bypass नहीं किया जा सकता।

Team session में ये notes Team mail में connection के display name और "outside assistant" label के साथ दिखते हैं।

Rebuild

इस Mac पर कोई codex binary install नहीं है, इसलिए client का काम handoff_sim.py (करीब 220 lines) करता है। यह तीन काम करता है:

  1. बिना token के live server को call करता है: initialize, tools/list और एक tools/call, हर एक 15 बार, और tool list save करता है।
  2. jsonschema (Draft 2020-12) से बारह argument cases को live inputSchemas के against validate करता है, ताकि सीमाओं के बारे में कुछ भी अंदाज़े से न हो।
  3. Seats, scopes, owner के फ़ैसलों और idempotency का एक छोटा in-memory model चलाता है। osFoundry का server code public नहीं है, इसलिए यह हिस्सा tool descriptions और handoffs दिखाने वाले web client के आधार पर दोबारा बनाया गया है। इसके responses model के हैं, server के नहीं।
$ uv run --with tiktoken --with jsonschema python3 handoff_sim.py
Enter fullscreen mode Exit fullscreen mode

Wire

initialize             HTTP 200  median  566.3  p90  799.0
tools/list             HTTP 200  median  559.7  p90  937.7
tools/call (no token)  HTTP 401  median  482.6  p90  677.0
Enter fullscreen mode Exit fullscreen mode

Milliseconds में, जापान में रखे इस Mac से CloudFront के KIX edge के रास्ते नापे गए, इसलिए ये numbers server से ज़्यादा route के बारे में बताते हैं। असली बात shape है। Discovery मुफ़्त है और इतनी तेज़ है कि हर Codex start पर चलाई जा सके। पहली असली call की कीमत एक 401 round trip है, और उस 401 में resource_metadata आता है, जिससे Codex बिना बताए OAuth server ढूँढ लेता है।

सात session tools पूरी list के 10,197 tokens में से 1,152 tokens हैं (11.3%, cl100k_base, इसलिए Codex के अपने tokenizer के लिए इसे अनुमान ही मानें)। enabled_tools में सिर्फ़ इन्हें रखने से prompt छोटा रहता है।

सीमाएँ

Tool Scope ज़रूरी fields Limits
propose_session_handoff write 6 goal 5,000, context 5,000, output 1,000, max_rounds 1–10
cancel_session_handoff write 1 सिर्फ़ handoff_id
request_session_run run 4 goal 10,000, max_rounds 1–10
read_session_state read 2 कोई नहीं
read_session_task_result read 3 कोई नहीं
read_session_mail read 2 limit 1–20
post_session_note write 5 subject 200, body 10,000

Live schemas के against बारह cases:

propose_session_handoff   6,000-char goal        REJECT maxLength: goal
request_session_run       6,000-char goal        ok
propose_session_handoff   no context_summary     REJECT required
propose_session_handoff   max_rounds omitted     ok
request_session_run       max_rounds 0           REJECT minimum: max_rounds
request_session_run       max_rounds 11          REJECT maximum: max_rounds
request_session_run       no idempotency_key     REJECT required
request_session_run       session_type 'room'    REJECT enum: session_type
request_session_run       extra field 'model'    REJECT additionalProperties
read_session_mail         limit 21               REJECT maximum: limit
post_session_note         201-char subject       REJECT maxLength: subject
cancel_session_handoff    only handoff_id        ok
Enter fullscreen mode Exit fullscreen mode

इनमें से दो पर दोबारा नज़र डालनी चाहिए। वही 6,000 characters का goal run के तौर पर ठीक है और handoff के तौर पर बहुत लंबा। जो client एक लंबा prompt बनाकर जो भी tool उपलब्ध हो उसे भेज देता है, वह यहीं अटकेगा। और additionalProperties: false एक model field को लौटा देता है: caller session का model नहीं चुन सकता। Session अपनी settings पर ही चलता है।

max_rounds दोनों में optional है। Schemas कोई default नहीं बताते, इसलिए अगर यह number आपके लिए मायने रखता है, तो उसे भेजें।

Seats, cancel और accept (model)

tool                      observer contributor runner
propose_session_handoff   -        yes         yes
cancel_session_handoff    -        yes         yes
request_session_run       -        -           yes
read_session_state        yes      yes         yes
read_session_task_result  yes      yes         yes
read_session_mail         yes      yes         yes
post_session_note         -        yes         yes
runner seat, token without osfoundry:run -> {'error': 'token lacks osfoundry:run'}

cancel while pending -> {'status': 'cancelled'}
owner clicks Accept after cancel -> cancelled
owner accepts -> accepted | cancel after -> {'error': 'already accepted'}
another connection reads that task -> {'error': 'not a task started by this connection'}
Enter fullscreen mode Exit fullscreen mode

यह matrix Connected tab के help text को table में बदलकर बनाया गया है। दिलचस्प rule cancel window का है। cancel_session_handoff की description के मुताबिक यह "before its owner accepts it" काम करता है, यानी जो पहले हो, वही जीतता है। एक बार run queue में चला गया, तो उसे वापस लेना caller के हाथ में नहीं रहता। इसी वजह से cancel सिर्फ़ एक handoff_id लेता है: कोई state भेजनी नहीं है, बस एक pending row बंद करनी है।

खोए हुए responses

हर write में idempotency_key ज़रूरी है। इससे क्या मिलता है, यह देखने के लिए model 1,000 request_session_run calls भेजता है, 20% responses गिरा देता है (seed 7) और client को उसी key के साथ तीन बार तक retry करने देता है:

server honours idempotency_key=True  calls sent 1276  tasks queued 1000  duplicates 0
server honours idempotency_key=False calls sent 1276  tasks queued 1276  duplicates 276
Enter fullscreen mode Exit fullscreen mode

किसी और के runtime पर 276 extra runs, ठीक इसी case के लिए key ज़रूरी रखी गई है। Timeout के बाद retry करने वाला हर client इससे टकराएगा, और जो run पहले ही शुरू हो चुका है, उसे आप दो बार नहीं चाहेंगे। Key को optional की जगह required रखने का मतलब है कि कोई client उसे भूल नहीं सकता।

पूरी script को Apple M1 पर 26.4 s wall-clock time लगा। लगभग सारा समय 45 network calls में गया, और user CPU time 0.37 s था।

अगर आप इसे खुद wire करें

  • "किसी इंसान से पूछना" और "पहले से मिली अनुमति पर काम करना" को दो scopes वाले दो tools में बाँटें, ताकि owner की पसंद token और seat में रहे, caller के set किए argument में नहीं।
  • जिन fields की इंसान को फ़ैसला लेने के लिए ज़रूरत है (context_summary, requested_output), उन्हें सिर्फ़ proposal पर ज़रूरी बनाएँ, और उनकी limit run के goal से कड़ी रखें।
  • Caller को सिर्फ़ तब तक cancel करने दें जब तक request pending है। Accept होने के बाद काम रोकना owner का काम है।
  • हर read-back को उसी तक सीमित रखें जो इस connection ने शुरू किया या जो इसे भेजा गया, और session का internal analysis उससे बाहर रखें।
  • हर write पर idempotency_key ज़रूरी बनाएँ, और repeat होने पर पहला result लौटाएँ।
  • Schema को additionalProperties: false से बंद करें, ताकि caller ऐसा model या tool list न घुसा सके जो session ने नहीं चुनी।

osFoundry के अपने agents steps के बीच काम कैसे आगे बढ़ाते हैं, यह handoffs page पर है। नीचे के protocol के लिए MCP glossary entry देखें।

Top comments (0)