Return TurnResult from Python turn handles

This commit is contained in:
Ahmed Ibrahim
2026-05-17 06:01:12 -07:00
parent 4c89772314
commit fab7cba2c9
42 changed files with 401 additions and 684 deletions

View File

@@ -25,23 +25,10 @@
" f'Notebook requires Python 3.10+; current interpreter is {sys.version.split()[0]}.'\n",
" )\n",
"\n",
"try:\n",
" _ = os.getcwd()\n",
"except FileNotFoundError:\n",
" os.chdir(str(Path.home()))\n",
"\n",
"\n",
"def _is_sdk_python_dir(path: Path) -> bool:\n",
" return (path / 'pyproject.toml').exists() and (path / 'src' / 'openai_codex').exists()\n",
"\n",
"\n",
"def _iter_home_fallback_candidates(home: Path):\n",
" # bounded depth scan under home to support launching notebooks from unrelated cwd values\n",
" patterns = ('sdk/python', '*/sdk/python', '*/*/sdk/python', '*/*/*/sdk/python')\n",
" for pattern in patterns:\n",
" yield from home.glob(pattern)\n",
"\n",
"\n",
"def _find_sdk_python_dir(start: Path) -> Path | None:\n",
" checked = set()\n",
"\n",
@@ -70,21 +57,6 @@
" if found is not None:\n",
" return found\n",
"\n",
" for entry in sys.path:\n",
" if not entry:\n",
" continue\n",
" entry_path = Path(entry).expanduser()\n",
" for candidate in (entry_path, entry_path / 'sdk' / 'python'):\n",
" found = _consider(candidate)\n",
" if found is not None:\n",
" return found\n",
"\n",
" home = Path.home()\n",
" for candidate in _iter_home_fallback_candidates(home):\n",
" found = _consider(candidate)\n",
" if found is not None:\n",
" return found\n",
"\n",
" return None\n",
"\n",
"\n",
@@ -129,7 +101,7 @@
"outputs": [],
"source": [
"# Cell 2: imports (public only)\n",
"from _bootstrap import assistant_text_from_turn, find_turn_by_id, server_label\n",
"from _bootstrap import server_label\n",
"from openai_codex import (\n",
" AsyncCodex,\n",
" Codex,\n",
@@ -172,12 +144,10 @@
" thread = codex.thread_start(model='gpt-5.4', config={'model_reasoning_effort': 'high'})\n",
" turn = thread.turn(TextInput('Explain gradient descent in 3 bullets.'))\n",
" result = turn.run()\n",
" persisted = thread.read(include_turns=True)\n",
" persisted_turn = find_turn_by_id(persisted.thread.turns, result.id)\n",
"\n",
" print('server:', server_label(codex.metadata))\n",
" print('status:', result.status)\n",
" print(assistant_text_from_turn(persisted_turn))\n"
" print(result.final_response)\n",
" print('items:', len(result.items))\n"
]
},
{
@@ -192,12 +162,9 @@
"\n",
" first = thread.turn(TextInput('Give a short summary of transformers.')).run()\n",
" second = thread.turn(TextInput('Now explain that to a high-school student.')).run()\n",
" persisted = thread.read(include_turns=True)\n",
" second_turn = find_turn_by_id(persisted.thread.turns, second.id)\n",
"\n",
" print('first status:', first.status)\n",
" print('second status:', second.status)\n",
" print('second text:', assistant_text_from_turn(second_turn))\n"
" print('second text:', second.final_response)\n"
]
},
{
@@ -221,41 +188,27 @@
" listing_archived = codex.thread_list(limit=20, archived=True)\n",
" unarchived = codex.thread_unarchive(reopened.id)\n",
"\n",
" resumed_info = 'n/a'\n",
" try:\n",
" resumed = codex.thread_resume(\n",
" unarchived.id,\n",
" model='gpt-5.4',\n",
" config={'model_reasoning_effort': 'high'},\n",
" )\n",
" resumed_result = resumed.turn(TextInput('Continue in one short sentence.')).run()\n",
" resumed_info = f'{resumed_result.id} {resumed_result.status}'\n",
" except Exception as e:\n",
" resumed_info = f'skipped({type(e).__name__})'\n",
" resumed = codex.thread_resume(\n",
" unarchived.id,\n",
" model='gpt-5.4',\n",
" config={'model_reasoning_effort': 'high'},\n",
" )\n",
" resumed_result = resumed.turn(TextInput('Continue in one short sentence.')).run()\n",
"\n",
" forked_info = 'n/a'\n",
" try:\n",
" forked = codex.thread_fork(unarchived.id, model='gpt-5.4')\n",
" forked_result = forked.turn(TextInput('Take a different angle in one short sentence.')).run()\n",
" forked_info = f'{forked_result.id} {forked_result.status}'\n",
" except Exception as e:\n",
" forked_info = f'skipped({type(e).__name__})'\n",
" forked = codex.thread_fork(unarchived.id, model='gpt-5.4')\n",
" forked_result = forked.turn(TextInput('Take a different angle in one short sentence.')).run()\n",
"\n",
" compact_info = 'sent'\n",
" try:\n",
" _ = unarchived.compact()\n",
" except Exception as e:\n",
" compact_info = f'skipped({type(e).__name__})'\n",
" compact_result = unarchived.compact()\n",
"\n",
" print('Lifecycle OK:', thread.id)\n",
" print('first:', first.id, first.status)\n",
" print('second:', second.id, second.status)\n",
" print('read.turns:', len(reading.thread.turns or []))\n",
" print('read.turns:', len(reading.thread.turns))\n",
" print('list.active:', len(listing_active.data))\n",
" print('list.archived:', len(listing_archived.data))\n",
" print('resumed:', resumed_info)\n",
" print('forked:', forked_info)\n",
" print('compact:', compact_info)\n"
" print('resumed:', resumed_result.id, resumed_result.status)\n",
" print('forked:', forked_result.id, forked_result.status)\n",
" print('compact:', compact_result.model_dump(mode='json', by_alias=True))\n"
]
},
{
@@ -299,11 +252,8 @@
" summary=summary,\n",
" )\n",
" result = turn.run()\n",
" persisted = thread.read(include_turns=True)\n",
" persisted_turn = find_turn_by_id(persisted.thread.turns, result.id)\n",
"\n",
" print('status:', result.status)\n",
" print(assistant_text_from_turn(persisted_turn))\n"
" print(result.final_response)\n"
]
},
{
@@ -332,17 +282,20 @@
"\n",
"\n",
"def pick_highest_model(models):\n",
" visible = [m for m in models if not m.hidden] or models\n",
" visible = [m for m in models if not m.hidden]\n",
" if not visible:\n",
" raise RuntimeError('models response did not include visible models')\n",
" known_names = {m.id for m in visible} | {m.model for m in visible}\n",
" top_candidates = [m for m in visible if not (m.upgrade and m.upgrade in known_names)]\n",
" pool = top_candidates or visible\n",
" return max(pool, key=lambda m: (m.model, m.id))\n",
" if not top_candidates:\n",
" raise RuntimeError('models response did not include top-level visible models')\n",
" return max(top_candidates, key=lambda m: (m.model, m.id))\n",
"\n",
"\n",
"def pick_highest_turn_effort(model) -> ReasoningEffort:\n",
" if not model.supported_reasoning_efforts:\n",
" return ReasoningEffort.medium\n",
" best = max(model.supported_reasoning_efforts, key=lambda opt: reasoning_rank.get(opt.reasoning_effort.value, -1))\n",
" raise RuntimeError(f'{model.model} did not advertise supported reasoning efforts')\n",
" best = max(model.supported_reasoning_efforts, key=lambda opt: reasoning_rank[opt.reasoning_effort.value])\n",
" return ReasoningEffort(best.reasoning_effort.value)\n",
"\n",
"\n",
@@ -372,10 +325,8 @@
" model=selected_model.model,\n",
" effort=selected_effort,\n",
" ).run()\n",
" persisted = thread.read(include_turns=True)\n",
" first_turn = find_turn_by_id(persisted.thread.turns, first.id)\n",
" print('agent.message:', assistant_text_from_turn(first_turn))\n",
" print('items:', 0 if first_turn is None else len(first_turn.items or []))\n",
" print('agent.message:', first.final_response)\n",
" print('items:', len(first.items))\n",
"\n",
" second = thread.turn(\n",
" TextInput('Return JSON for a safe feature-flag rollout plan.'),\n",
@@ -387,10 +338,8 @@
" sandbox_policy=sandbox_policy,\n",
" summary=ReasoningSummary.model_validate('concise'),\n",
" ).run()\n",
" persisted = thread.read(include_turns=True)\n",
" second_turn = find_turn_by_id(persisted.thread.turns, second.id)\n",
" print('agent.message.params:', assistant_text_from_turn(second_turn))\n",
" print('items.params:', 0 if second_turn is None else len(second_turn.items or []))\n"
" print('agent.message.params:', second.final_response)\n",
" print('items.params:', len(second.items))\n"
]
},
{
@@ -408,11 +357,8 @@
" TextInput('What do you see in this image? 3 bullets.'),\n",
" ImageInput(remote_image_url),\n",
" ]).run()\n",
" persisted = thread.read(include_turns=True)\n",
" persisted_turn = find_turn_by_id(persisted.thread.turns, result.id)\n",
"\n",
" print('status:', result.status)\n",
" print(assistant_text_from_turn(persisted_turn))\n"
" print(result.final_response)\n"
]
},
{
@@ -429,11 +375,8 @@
" TextInput('Describe the colors and layout in this generated local image in 2 bullets.'),\n",
" LocalImageInput(str(local_image_path.resolve())),\n",
" ]).run()\n",
" persisted = thread.read(include_turns=True)\n",
" persisted_turn = find_turn_by_id(persisted.thread.turns, result.id)\n",
"\n",
" print('status:', result.status)\n",
" print(assistant_text_from_turn(persisted_turn))\n"
" print(result.final_response)\n"
]
},
{
@@ -452,11 +395,8 @@
" initial_delay_s=0.25,\n",
" max_delay_s=2.0,\n",
" )\n",
" persisted = thread.read(include_turns=True)\n",
" persisted_turn = find_turn_by_id(persisted.thread.turns, result.id)\n",
"\n",
" print('status:', result.status)\n",
" print(assistant_text_from_turn(persisted_turn))\n"
" print(result.final_response)\n"
]
},
{
@@ -484,41 +424,27 @@
" listing_archived = await codex.thread_list(limit=20, archived=True)\n",
" unarchived = await codex.thread_unarchive(reopened.id)\n",
"\n",
" resumed_info = 'n/a'\n",
" try:\n",
" resumed = await codex.thread_resume(\n",
" unarchived.id,\n",
" model='gpt-5.4',\n",
" config={'model_reasoning_effort': 'high'},\n",
" )\n",
" resumed_result = await (await resumed.turn(TextInput('Continue in one short sentence.'))).run()\n",
" resumed_info = f'{resumed_result.id} {resumed_result.status}'\n",
" except Exception as e:\n",
" resumed_info = f'skipped({type(e).__name__})'\n",
" resumed = await codex.thread_resume(\n",
" unarchived.id,\n",
" model='gpt-5.4',\n",
" config={'model_reasoning_effort': 'high'},\n",
" )\n",
" resumed_result = await (await resumed.turn(TextInput('Continue in one short sentence.'))).run()\n",
"\n",
" forked_info = 'n/a'\n",
" try:\n",
" forked = await codex.thread_fork(unarchived.id, model='gpt-5.4')\n",
" forked_result = await (await forked.turn(TextInput('Take a different angle in one short sentence.'))).run()\n",
" forked_info = f'{forked_result.id} {forked_result.status}'\n",
" except Exception as e:\n",
" forked_info = f'skipped({type(e).__name__})'\n",
" forked = await codex.thread_fork(unarchived.id, model='gpt-5.4')\n",
" forked_result = await (await forked.turn(TextInput('Take a different angle in one short sentence.'))).run()\n",
"\n",
" compact_info = 'sent'\n",
" try:\n",
" _ = await unarchived.compact()\n",
" except Exception as e:\n",
" compact_info = f'skipped({type(e).__name__})'\n",
" compact_result = await unarchived.compact()\n",
"\n",
" print('Lifecycle OK:', thread.id)\n",
" print('first:', first.id, first.status)\n",
" print('second:', second.id, second.status)\n",
" print('read.turns:', len(reading.thread.turns or []))\n",
" print('read.turns:', len(reading.thread.turns))\n",
" print('list.active:', len(listing_active.data))\n",
" print('list.archived:', len(listing_archived.data))\n",
" print('resumed:', resumed_info)\n",
" print('forked:', forked_info)\n",
" print('compact:', compact_info)\n",
" print('resumed:', resumed_result.id, resumed_result.status)\n",
" print('forked:', forked_result.id, forked_result.status)\n",
" print('compact:', compact_result.model_dump(mode='json', by_alias=True))\n",
"\n",
"\n",
"await async_lifecycle_demo()\n"
@@ -530,7 +456,7 @@
"metadata": {},
"outputs": [],
"source": [
"# Cell 10: async turn controls (best effort steer + interrupt)\n",
"# Cell 10: async turn controls (steer + interrupt)\n",
"import asyncio\n",
"\n",
"\n",
@@ -539,46 +465,46 @@
" thread = await codex.thread_start(model='gpt-5.4', config={'model_reasoning_effort': 'high'})\n",
" steer_turn = await thread.turn(TextInput('Count from 1 to 40 with commas, then one summary sentence.'))\n",
"\n",
" steer_result = 'sent'\n",
" try:\n",
" _ = await steer_turn.steer(TextInput('Keep it brief and stop after 10 numbers.'))\n",
" except Exception as e:\n",
" steer_result = f'skipped {type(e).__name__}'\n",
" steer_result = await steer_turn.steer(TextInput('Keep it brief and stop after 10 numbers.'))\n",
"\n",
" steer_event_count = 0\n",
" steer_completed_status = 'unknown'\n",
" steer_completed_turn = None\n",
" steer_completed_status = None\n",
" steer_deltas = []\n",
" async for event in steer_turn.stream():\n",
" steer_event_count += 1\n",
" if event.method == 'item/agentMessage/delta':\n",
" steer_deltas.append(event.payload.delta)\n",
" continue\n",
" if event.method == 'turn/completed':\n",
" steer_completed_turn = event.payload.turn\n",
" steer_completed_status = getattr(event.payload.turn.status, 'value', str(event.payload.turn.status))\n",
" steer_completed_status = event.payload.turn.status.value\n",
"\n",
" steer_preview = assistant_text_from_turn(steer_completed_turn).strip() or '[no assistant text]'\n",
" if steer_completed_status is None:\n",
" raise RuntimeError('stream ended without turn/completed')\n",
" steer_preview = ''.join(steer_deltas).strip()\n",
"\n",
" interrupt_turn = await thread.turn(TextInput('Count from 1 to 200 with commas, then one summary sentence.'))\n",
" interrupt_result = 'sent'\n",
" try:\n",
" _ = await interrupt_turn.interrupt()\n",
" except Exception as e:\n",
" interrupt_result = f'skipped {type(e).__name__}'\n",
" interrupt_result = await interrupt_turn.interrupt()\n",
"\n",
" interrupt_event_count = 0\n",
" interrupt_completed_status = 'unknown'\n",
" interrupt_completed_turn = None\n",
" interrupt_completed_status = None\n",
" interrupt_deltas = []\n",
" async for event in interrupt_turn.stream():\n",
" interrupt_event_count += 1\n",
" if event.method == 'item/agentMessage/delta':\n",
" interrupt_deltas.append(event.payload.delta)\n",
" continue\n",
" if event.method == 'turn/completed':\n",
" interrupt_completed_turn = event.payload.turn\n",
" interrupt_completed_status = getattr(event.payload.turn.status, 'value', str(event.payload.turn.status))\n",
" interrupt_completed_status = event.payload.turn.status.value\n",
"\n",
" interrupt_preview = assistant_text_from_turn(interrupt_completed_turn).strip() or '[no assistant text]'\n",
" if interrupt_completed_status is None:\n",
" raise RuntimeError('stream ended without turn/completed')\n",
" interrupt_preview = ''.join(interrupt_deltas).strip()\n",
"\n",
" print('steer.result:', steer_result)\n",
" print('steer.result:', steer_result.model_dump(mode='json', by_alias=True))\n",
" print('steer.final.status:', steer_completed_status)\n",
" print('steer.events.count:', steer_event_count)\n",
" print('steer.assistant.preview:', steer_preview)\n",
" print('interrupt.result:', interrupt_result)\n",
" print('interrupt.result:', interrupt_result.model_dump(mode='json', by_alias=True))\n",
" print('interrupt.final.status:', interrupt_completed_status)\n",
" print('interrupt.events.count:', interrupt_event_count)\n",
" print('interrupt.assistant.preview:', interrupt_preview)\n",