optio-codex 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
optio_codex/models.py ADDED
@@ -0,0 +1,68 @@
1
+ """Available-model list for the codex conversation widget's model picker.
2
+
3
+ ============================================================================
4
+ MODEL-SWITCH MECHANISM — pinned from the app-server contract (codex-cli
5
+ 0.142.5 schema dump + upstream README; see the conversation docstring).
6
+ ============================================================================
7
+
8
+ Decision: **INLINE** (opencode/grok-style), NOT restart (claudecode-style) —
9
+ and codex needs NO dedicated set-model request at all: a ``model`` override
10
+ on ``turn/start`` "become[s] the default for subsequent turns" (README,
11
+ turn/start). So ``CodexConversation.request_model_change()`` just pins the
12
+ model sent with the next ``turn/start``; the session body needs no
13
+ model_change_requested restart loop.
14
+
15
+ MODEL LIST source: the ``model/list`` request, answered in-session
16
+ (``{data:[Model], nextCursor}``; ``Model`` carries ``id``, ``displayName``,
17
+ ``hidden``, ``isDefault``). The conversation captures the raw result at
18
+ bootstrap; this module maps it to the widget shape. There is no CLI listing
19
+ tier (unlike grok) — live result → static fallback, nothing in between.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ # Shown when the live model/list is unavailable (fake agents, offline, error
24
+ # response). Version-sensitive vendor strings; update alongside the pinned
25
+ # codex-cli version.
26
+ FALLBACK_MODELS: dict = {
27
+ "models": [
28
+ {"id": "gpt-5.5", "label": "GPT-5.5", "disabled": False},
29
+ {"id": "gpt-5.4-mini", "label": "GPT-5.4 Mini", "disabled": False},
30
+ ],
31
+ "default": "gpt-5.5",
32
+ }
33
+
34
+
35
+ def parse_model_list(result: "dict | None") -> dict:
36
+ """Map a raw ``model/list`` result to the widget shape
37
+ ``{models:[{id,label,disabled}], default}``.
38
+
39
+ Hidden models are omitted; ``default`` is the ``isDefault`` entry's id
40
+ (None when absent). Missing / malformed input returns the static
41
+ fallback (never raises, never falsely empties the picker).
42
+ """
43
+ if not isinstance(result, dict):
44
+ return _copy_fallback()
45
+ data = result.get("data")
46
+ if not isinstance(data, list):
47
+ return _copy_fallback()
48
+ out: list[dict] = []
49
+ default: str | None = None
50
+ for m in data:
51
+ if not isinstance(m, dict):
52
+ continue
53
+ mid = m.get("id")
54
+ if not isinstance(mid, str) or not mid or m.get("hidden"):
55
+ continue
56
+ out.append({"id": mid, "label": m.get("displayName") or mid, "disabled": False})
57
+ if m.get("isDefault"):
58
+ default = mid
59
+ if not out:
60
+ return _copy_fallback()
61
+ return {"models": out, "default": default}
62
+
63
+
64
+ def _copy_fallback() -> dict:
65
+ return {
66
+ "models": [dict(m) for m in FALLBACK_MODELS["models"]],
67
+ "default": FALLBACK_MODELS["default"],
68
+ }
optio_codex/prompt.py ADDED
@@ -0,0 +1,184 @@
1
+ """AGENTS.md composition for optio-codex.
2
+
3
+ Codex reads an ``AGENTS.md`` file in its workdir. The shared framing and
4
+ the keyword-protocol documentation are owned by ``optio-agents`` (the
5
+ prompt SSOT); this module threads codex's protocol mode through and owns
6
+ the codex-specific resume-awareness section (Stage 2), rendered from the
7
+ EFFECTIVE snapshot exclude list so its preservation claims never drift
8
+ from what the snapshot actually keeps.
9
+ """
10
+
11
+ from optio_agents.prompt import compose_agents_md as _compose_agents_md_host
12
+ from optio_agents.prompt import downloadables_block
13
+ from optio_agents.protocol import ProtocolFeatures, build_log_channel_prompt
14
+
15
+ from optio_codex.snapshots import effective_workdir_exclude
16
+
17
+
18
+ # Self-contained System: explainer for sessions without the keyword-protocol
19
+ # docs (which normally explain the convention). Per-wrapper copy is the
20
+ # established pattern (claudecode/opencode/grok each carry their own).
21
+ _SYSTEM_PREFIX_EXPLAINER = """\
22
+ (Messages prefixed `System:` on your input channel originate from the
23
+ harness coordinating this session, not from the human user.)
24
+ """
25
+
26
+
27
+ RESUME_SECTION_TEMPLATE = """## Resumes
28
+
29
+ This harness may pause your session, save your context to a database,
30
+ terminate the underlying process, and later rehydrate it. From your
31
+ point of view the conversation is fully continuous — you keep your
32
+ prior context and will not "notice" the resume.
33
+
34
+ **A resume can happen at any point, not only at the start.** The host
35
+ environment may have changed across a resume — different host,
36
+ different running processes, files outside this workdir gone — even
37
+ though your context remembers everything as alive and well.
38
+
39
+ **The workdir (this directory) is preserved across resumes, with two
40
+ caveats:**
41
+
42
+ - {excludes_clause}
43
+ - **Anything outside the workdir is not preserved.**
44
+
45
+ - **Your `home/.codex/` directory — the codex session store (rollout
46
+ files under `home/.codex/sessions`, auth, config) — IS preserved
47
+ across resumes** (minus the excluded paths above), so your history
48
+ travels with you even when the underlying process and host change.
49
+
50
+ {outside_clause}
51
+
52
+ ### Detecting a resume: `resume.log`
53
+
54
+ Each session start (fresh or resumed) appends one line to
55
+ `./resume.log`. Line format:
56
+
57
+ ```
58
+ <ISO 8601 UTC timestamp>[ REFRESHED:<comma-separated filenames>]
59
+ ```
60
+
61
+ The very first line is the original launch timestamp; each subsequent
62
+ line is a resume. The optional `REFRESHED:` suffix signals that the
63
+ harness rewrote the listed files on that resume (e.g.
64
+ `2026-05-28T13:15:42Z REFRESHED:AGENTS.md`) — your in-memory copy of
65
+ those files is stale and must be re-read before continuing.
66
+
67
+ **At the start of every new incoming user message, read
68
+ `./resume.log` first.** Compare the latest line to the value you
69
+ remembered last time you checked. If a new line has appeared, treat
70
+ the situation as a resume:
71
+
72
+ - Verify any tools, processes, or files you previously gathered
73
+ outside the workdir are still where you left them.
74
+ - Re-establish anything that's gone (re-launch a server, re-fetch a
75
+ file, etc.) before continuing.
76
+ - **If the latest line carries a `REFRESHED:` suffix, re-read each
77
+ listed file** (e.g. `cat ./AGENTS.md`) — the harness updated it
78
+ since your last context snapshot and the version you remember is
79
+ out of date.
80
+ - Then resume the work you were doing.
81
+
82
+ If a resume slips past unnoticed, a failing tool call is the
83
+ next-best signal — re-check `./resume.log` then.
84
+
85
+ You may also be notified of a resume by a `System:` message on your input
86
+ channel; when you see one, follow the `resume.log` procedure above.
87
+ """
88
+
89
+
90
+ def _render_resume_section(workdir_exclude: list[str] | None) -> str:
91
+ """Render RESUME_SECTION_TEMPLATE with the EFFECTIVE exclude list.
92
+
93
+ ``effective_workdir_exclude`` is the same resolver the snapshot archive
94
+ uses (``None`` → the codex defaults), so what this section claims is
95
+ preserved is exactly what the snapshot preserves.
96
+ """
97
+ effective = effective_workdir_exclude(workdir_exclude)
98
+ if not effective:
99
+ excludes_clause = (
100
+ "**No paths are excluded** — every file in the workdir is preserved."
101
+ )
102
+ outside_clause = (
103
+ "If you need to stash large data, place it outside the workdir "
104
+ "(e.g. `/tmp/`) — but remember it may be missing when you next look."
105
+ )
106
+ else:
107
+ excludes_str = ", ".join(f"`{p}`" for p in effective)
108
+ excludes_clause = (
109
+ f"**Paths matching the snapshot exclude list are NOT preserved**, "
110
+ f"even inside the workdir. The current exclude list is: {excludes_str}."
111
+ )
112
+ outside_clause = (
113
+ "If you need to stash large data, place it outside the workdir "
114
+ "(e.g. `/tmp/`) or inside an excluded subdirectory — but remember "
115
+ "any such location may be missing when you next look."
116
+ )
117
+ return RESUME_SECTION_TEMPLATE.format(
118
+ excludes_clause=excludes_clause,
119
+ outside_clause=outside_clause,
120
+ )
121
+
122
+
123
+ def compose_agents_md(
124
+ consumer_instructions: str,
125
+ *,
126
+ documentation: str | None = None,
127
+ host_protocol: bool = True,
128
+ workdir_exclude: list[str] | None = None,
129
+ supports_resume: bool = True,
130
+ file_download: bool = False,
131
+ ) -> str:
132
+ """Build the full AGENTS.md body.
133
+
134
+ ``documentation`` is the keyword-protocol block; the session passes
135
+ ``get_protocol(browser="redirect").documentation``. Defaults (for unit
136
+ tests / standalone callers) to codex's ``redirect`` docs. It must
137
+ always come from the session's ``Protocol`` where one exists — never
138
+ rebuild features at a second site.
139
+
140
+ ``host_protocol=False`` omits the keyword-protocol documentation and
141
+ instead includes a self-contained ``System:`` message explainer
142
+ (guide Part 2D); iframe mode always runs with ``host_protocol=True``
143
+ (validated in ``CodexTaskConfig``), the False branch serves
144
+ conversation mode in a later stage.
145
+
146
+ ``supports_resume=True`` (default) appends the resume-awareness section
147
+ so the agent watches ``resume.log`` and knows ``home/.codex`` (minus
148
+ ``workdir_exclude``) survives across resumes. ``workdir_exclude`` is
149
+ this task's snapshot exclude list (None → the codex defaults), used to
150
+ keep the section's claims in sync with what is actually preserved.
151
+
152
+ ``file_download=True`` appends the downloadables instruction block so
153
+ codex offers files to the human via the ``optio-file:`` sentinel link
154
+ (conversation_ui file-download feature); the wording is comparative when
155
+ the keyword protocol is active (``host_protocol``).
156
+ """
157
+ if file_download:
158
+ consumer_instructions = (
159
+ consumer_instructions.rstrip()
160
+ + downloadables_block(comparative=host_protocol)
161
+ )
162
+ if host_protocol:
163
+ if documentation is None:
164
+ documentation = build_log_channel_prompt(
165
+ ProtocolFeatures(browser="redirect")
166
+ )
167
+ else:
168
+ documentation = None
169
+ resume_section: str | None = (
170
+ _render_resume_section(workdir_exclude) if supports_resume else None
171
+ )
172
+ if not host_protocol:
173
+ # The protocol docs normally explain the `System:` convention;
174
+ # without them the composed prompt carries its own explainer.
175
+ resume_section = (
176
+ resume_section + _SYSTEM_PREFIX_EXPLAINER
177
+ if resume_section
178
+ else _SYSTEM_PREFIX_EXPLAINER
179
+ )
180
+ return _compose_agents_md_host(
181
+ consumer_instructions,
182
+ documentation=documentation,
183
+ resume_section=resume_section,
184
+ )
@@ -0,0 +1,91 @@
1
+ """codex adopter of the generic optio-agents seed engine.
2
+
3
+ Defines the codex seed manifest (HOME layout + capture-time include triage),
4
+ the Mongo collection suffix, and ergonomic ``delete_seed`` / ``list_seeds`` /
5
+ ``purge_seed`` wrappers that bind the suffix for consuming apps.
6
+
7
+ A codex *seed* carries the logged-in identity that lives under ``CODEX_HOME``
8
+ (``<workdir>/home/.codex``): ``auth.json`` (ChatGPT mode: ``auth_mode`` +
9
+ ``tokens{id_token, access_token, refresh_token}`` + ``last_refresh``; API-key
10
+ mode: ``OPENAI_API_KEY``) plus ``config.toml``. Replanting it into a fresh
11
+ workdir is the answer to headless login.
12
+
13
+ The include list is an allowlist, which is also the exclusion mechanism:
14
+ ``packages/`` (the ~286MB binary cache), ``*.sqlite*`` (absolute
15
+ rollout-path poison; rebuilt from rollouts), ``sessions/``, ``cache/``,
16
+ ``tmp/``, logs etc. are simply never members.
17
+
18
+ Like grok/opencode (and unlike claudecode), codex needs no consume-time
19
+ rekey: auth/config are cwd-independent, so ``consume_transform`` is None.
20
+ The one cwd-dependent consume step — pre-trusting the new workdir via a
21
+ ``[projects."<workdir>"]`` entry in config.toml — is deliberately a
22
+ post-merge edit in the session's ``_prepare`` (see
23
+ ``host_actions.ensure_workdir_trusted``), NOT a manifest transform: codex
24
+ rewrites config.toml itself at runtime, so optio's edit must stay a
25
+ minimal, idempotent append against the *planted* file, applied exactly at
26
+ the point the workdir is known.
27
+
28
+ Path note: the engine roots capture/extract at ``host.workdir + "/" +
29
+ home_subdir`` (see ``SeedManifest.home_subdir``). CODEX_HOME is
30
+ ``<workdir>/home/.codex``, so the manifest uses ``home_subdir="home"`` with
31
+ ``.codex/`` prefixes on the include paths (mirroring grok's ``.grok/…``).
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ from optio_agents import seeds
37
+
38
+ CODEX_SEED_SUFFIX = "_codex_seeds"
39
+ CODEX_SEED_MANIFEST_VERSION = 1
40
+
41
+
42
+ # Credential-only manifest for in-session save-back (the write-back analog
43
+ # of the full CODEX_SEED_MANIFEST; mirrors grok's GROK_CRED_MANIFEST and
44
+ # opencode's OPENCODE_CRED_MANIFEST). Only auth.json is re-captured — the
45
+ # seed's config.toml is never touched by save-back.
46
+ CODEX_CRED_MANIFEST = seeds.SeedManifest(
47
+ home_subdir="home",
48
+ include=[".codex/auth.json"],
49
+ version=CODEX_SEED_MANIFEST_VERSION,
50
+ consume_transform=None,
51
+ )
52
+
53
+
54
+ CODEX_SEED_MANIFEST = seeds.SeedManifest(
55
+ home_subdir="home",
56
+ include=CODEX_CRED_MANIFEST.include + [
57
+ ".codex/config.toml",
58
+ ],
59
+ version=CODEX_SEED_MANIFEST_VERSION,
60
+ consume_transform=None, # no cwd-rekey for codex (pre-trust is in _prepare)
61
+ )
62
+
63
+
64
+ async def delete_seed(store, seed_id: str):
65
+ """Delete a codex seed doc; returns its GridFS blobId (or None).
66
+
67
+ Takes an optio store binding (``optio.mongo_store`` — exposes ``db`` and
68
+ ``prefix``) as-is, so consuming apps hand over the whole namespace handle
69
+ instead of threading db+prefix (or knowing the collection suffix). The
70
+ caller still removes the returned blob from GridFS.
71
+ """
72
+ return await seeds.delete_seed(
73
+ store.db, prefix=store.prefix, suffix=CODEX_SEED_SUFFIX, seed_id=seed_id,
74
+ )
75
+
76
+
77
+ async def list_seeds(store) -> list[dict]:
78
+ """List codex seeds as [{seedId, createdAt}, ...]. Takes an optio store
79
+ binding (``optio.mongo_store``) as-is."""
80
+ return await seeds.list_seeds(store.db, prefix=store.prefix, suffix=CODEX_SEED_SUFFIX)
81
+
82
+
83
+ async def purge_seed(store, seed_id: str) -> None:
84
+ """Fully expunge a codex seed (doc + its GridFS blob); raises KeyError if
85
+ absent. Takes an optio store binding (``optio.mongo_store``) as-is.
86
+
87
+ Mirrors ``optio_grok.purge_seed`` / ``optio_claudecode.purge_seed``; a
88
+ thin re-export of the ``optio_agents.seeds.purge_seed`` engine."""
89
+ return await seeds.purge_seed(
90
+ store.db, prefix=store.prefix, suffix=CODEX_SEED_SUFFIX, seed_id=seed_id,
91
+ )