oc_chat
Loading...
Searching...
No Matches
oc_chat.main Namespace Reference

Functions

None register_active_agent ("object | None" agent)
None _announce_interrupt ("object | None" agent, str stage)
 _o3_sigint_handler (signum, frame)
str _active_sessions_dir ()
str _read_startup_input (prompt_driver, str prompt_text, str default, *, presenter)
None _show_serving_host (presenter, client)
ChatSession _load_session_by_name_for_startup (str name, presenter, list|None tool_schemas, str sessions_dir)
ChatSession|None _maybe_autoreload_crashed_session (Agent agent, presenter, prompt_driver, str sessions_dir)
bool _recover_incomplete_session_tail (ChatSession session, presenter, prompt_driver)
bool _show_config_validation (presenter, result)
"object" _select_presenter_once (args)
None _safe_presenter_close (presenter)
None _assert_stdin_is_tty_or_input_mode (args)
str _anchor_banner_line (object root, str source, "str | None" branch)
None _emit_startup_anchor_banner (presenter, args)
None main (list[str]|None argv=None)

Variables

 _VERSION = resolve_version()
dict _active_agent_ref = {"agent": None}

Detailed Description

Main entry point for the OC Chat REPL — wiring shell only.

Purpose
-------
Parse argv, initialise diagnostics, select a presenter, run the
storage migration, drive the startup session selection menu, build
an :class:`~oc_chat.core.agent.Agent` and a slash-command
:class:`~oc_chat.core.commands.Context`, and run the read-eval loop
that ferries user input into ``agent.run_turn(...)`` and slash
commands into :func:`dispatch`. All the per-turn ReAct logic, tool
dispatch, registry construction, startup wizard, CHANGELOG updates,
and presentation formatting now live in their own modules; this
file is the orchestration shell that wires them together.

Responsibilities
----------------
- :func:`main` — the wiring sequence above.
- Slash-command loop body: dispatch slash commands, otherwise
  drive ``agent.run_turn(user_input)``; on ``/goal`` completion,
  trigger :func:`append_session_log` against the agent's session.

The decomposition lives at:
- :mod:`oc_chat.core.react_loop` (G.6.A) — ``run_turn`` + tool dispatch.
- :mod:`oc_chat.core.tool_registry` (G.6.A) — ``build_tool_registry``.
- :mod:`oc_chat.core.startup` (G.6.B) — ``startup_session_prompt``.
- :mod:`oc_chat.core.session_log` (G.6.B) — ``append_session_log``.
- :mod:`oc_ui.terminal.formatting` (G.6.B) — presentation formatters.
- :mod:`oc_chat.cli` (G.6.C) — argparse, presenter selection,
  terminal clear.
- :mod:`oc_chat.core._version` (G.6.C) — version-string resolver.
- :mod:`oc_chat.core.agent` (G.6.C) — :class:`Agent` and
  :class:`AgentConfig`, the runtime building block this shell wires.

Diagnostics
-----------
Domain: OC_CHAT.MAIN
Levels:
    L2 — REPL lifecycle events.
    L3 — semantic details.
    L4 — deep tracing (currently unused).

Contracts
---------
- ``oc_diagnostics.init(intercept_streams=False,
  intercept_exceptions=False)`` runs once at the top of ``main()``.
  Stream intercept is disabled so the REPL keeps clean stdout/stderr.

Dependencies
------------
- oc_diagnostics (structured debug logging via _dbg)
- oc_chat.cli (argparse, presenter selection, terminal clear)
- oc_chat.core.agent (Agent + AgentConfig — runtime building block)
- oc_chat.core.session_log (post-goal SESSION_LOG.md writer)
- oc_chat.core.commands (slash-command dispatch)
- oc_chat.core.persistence (migrate_legacy_layout)
- oc_chat.core.provider_plugin (ProviderPlugin ABC and registry singleton)
- oc_chat.core.startup (startup_session_prompt — interactive bootstrap)
- oc_chat.core._version (resolve_version — banner/CHANGELOG)
- providers.deepseek (DeepSeek provider plugin — default backend)
- oc_ui.terminal.input (PromptDriver, TurnStats, prompt_prefix builder)
- oc_chat.core.config_validation (validate_runtime_config and format_config_issue - the startup configuration check and its messages)
- oc_chat.core.session (ChatSession, TokenStats and the metadata and system-prompt helpers - rebuilding a session for the crash auto-reload)
- oc_chat.core.session_transition (IterationStatsSink and the turn-phase marks - the process-owned stats sink and the deferred Ctrl+C)
- oc_chat.tools.ask_user (bind_interaction - binds the ask_user tool to the live prompt driver)
- providers.ollama (Ollama provider plugin - registered on import)
- providers.openai_compat (the seven OCON lane providers - registered on import)
- oc_chat.core.batch_runner (lazy; run_batch_turn - the headless --input / --output turn)
- oc_chat.core.cancellation (lazy; signal_tool_cancel - stops in-flight tools on the second Ctrl+C)
- oc_chat.core.deployment_shape (lazy; check_deployment_shape and DeploymentShapeError - refuses a sandbox that overlaps an importable root)
- oc_chat.core.endpoint_registry (lazy; EndpointRegistry - restores the session's last-used endpoint)
- oc_chat.core.event_bootstrap (lazy; attach_default_writer - the NDJSON event writer on the default bus)
- oc_chat.core.external_content_gate (lazy; blast_radius_banner - the permissive profile's startup banner)
- oc_chat.core.generation_options (lazy; GenerationOptions - restores the session's persisted generation options)
- oc_chat.core.lane_switch (lazy; switch_lane - applies a lane chosen in the opening menu to the Agent)
- oc_chat.core.lane_tree (lazy; serving_host_of - names the host that served a turn)
- oc_chat.core.presenter_events (lazy; attach_presenter_bridge - renders the approval and sub-agent events in the terminal)
- oc_chat.core.prompt_layers (lazy; _git_branch - the branch in the startup anchor banner)
- oc_chat.core.risk_profile (lazy; resolve_risk_profile - the active risk profile for the deployment-shape check)
- oc_chat.core.session_mode (lazy; the --auto launch flag's switch into auto mode)
- oc_chat.core.settings (lazy; init_runtime_settings - resolves the composition mode once from the settings file and the CLI)
- oc_chat.tools.filesystem (lazy; _sandbox_root and _sandbox_root_info - the sandbox root for the anchor banner and the deployment-shape check)
- oc_ui.presenter_base (lazy; INTERRUPT_NOTICES - the stderr fallback for a Ctrl+C notice)

Function Documentation

◆ _active_sessions_dir()

str oc_chat.main._active_sessions_dir ( )
protected

Definition at line 275 of file main.py.

Here is the caller graph for this function:

◆ _anchor_banner_line()

str oc_chat.main._anchor_banner_line ( object root,
str source,
"str | None" branch )
protected
Compose the one-line pre-menu anchor banner (plan 173 item 2 / F19).

``sandbox root: <abs> (<source label>) | branch: <branch or ->``. ASCII-only
(F4): the separator is `` | ``, never a non-cp1252 middot, so it never crashes
a stock Windows console.

Definition at line 577 of file main.py.

Here is the caller graph for this function:

◆ _announce_interrupt()

None oc_chat.main._announce_interrupt ( "object | None" agent,
str stage )
protected
Announce a Ctrl+C *stage* through the presenter, falling back to stderr.

Plan 188 T5. The handler used to write this notice inline, which meant the
component that OWNS the terminal — the presenter, possibly mid-``Live`` —
had no say in how or where it landed. It now goes through
:meth:`~oc_ui.presenter_base.Presenter.show_interrupt_notice`, whose
default is the same stderr write, so a presenter that does not override it
behaves exactly as before.

Two properties matter because this runs inside a signal handler:

* **it never raises** — a broken presenter must not turn Ctrl+C into a
  crash, so any failure degrades to the stderr notice and the
  cancellation below still proceeds;
* **it stays one short write** — no prompting, no blocking, no
  renderable pipeline (see the hook's signal-safety contract).

Definition at line 169 of file main.py.

Here is the caller graph for this function:

◆ _assert_stdin_is_tty_or_input_mode()

None oc_chat.main._assert_stdin_is_tty_or_input_mode ( args)
protected
Refuse to enter the interactive REPL with redirected stdin.

The REPL reads from ``sys.stdin``; if stdin is a regular file (not a
TTY) and the user didn't ask for batch mode via ``--input``/``--output``,
each newline in that file becomes a separate user prompt and the loop
only exits on EOF. Almost always a mistake (e.g. accidentally pointing
stdin at ``.oc_chat/launch/agent-*.md`` or another launch artifact).

Bypasses on any of: stdin is a TTY, ``--input PATH`` was passed, or
``--output PATH`` was passed. See plan 115 ADR-3 — no env-var override.

Definition at line 554 of file main.py.

Here is the caller graph for this function:

◆ _emit_startup_anchor_banner()

None oc_chat.main._emit_startup_anchor_banner ( presenter,
args )
protected
Print the pre-menu anchor banner; on a ``--workdir`` vs resolved-root
disagreement, warn and require an explicit y/N before the menu (plan 173 / F19).

F19: three consecutive mis-anchored launches (a resumed-session persisted-root
override + an omitted ``--workdir``) preceded the successful one because the
anchor block existed only in the model's context, discovered after pasting.
This surfaces ``root · source · branch`` to the operator BEFORE the menu.

Definition at line 587 of file main.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ _load_session_by_name_for_startup()

ChatSession oc_chat.main._load_session_by_name_for_startup ( str name,
presenter,
list | None tool_schemas,
str sessions_dir )
protected
Load a named session without entering the interactive picker.

Definition at line 342 of file main.py.

Here is the caller graph for this function:

◆ _maybe_autoreload_crashed_session()

ChatSession | None oc_chat.main._maybe_autoreload_crashed_session ( Agent agent,
presenter,
prompt_driver,
str sessions_dir )
protected

Definition at line 421 of file main.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ _o3_sigint_handler()

oc_chat.main._o3_sigint_handler ( signum,
frame )
protected
Two-stage cooperative Ctrl+C (Phase O3e + TUI-polish).

During an active turn the Agent owns a ``CancellationToken``:

* **1st Ctrl+C** → request a cooperative interrupt and NOTHING ELSE
  (operator decision 2026-08-12): the in-flight tool keeps running, and the
  turn is asked "Stop the current action? (y/N)" at its next safe
  checkpoint — which arrives after that tool returns. "n" continues with
  nothing lost; nothing was cancelled before the answer. A one-line notice
  says the current tool keeps running and a second press force-stops.
* **2nd Ctrl+C** (an interrupt is already pending, or a hard cancel is set)
  → force: flip the hard-cancel flag, signal the process-wide tool-cancel
  flag (the ``[cancelled by user]`` / returncode −2 machinery in
  ``tools/_proc.py`` — so the child dies cleanly, not orphaned), and raise
  ``KeyboardInterrupt`` so blocking I/O unwinds immediately.

**Plan 188 T5.** Both notices go through :func:`_announce_interrupt` →
the active presenter's ``show_interrupt_notice``, instead of the raw
``stderr`` write this handler used to do inline. The mechanism above is
unchanged; what changed is that the component owning the terminal (Rich,
possibly mid-``Live``) now chooses how and where the message lands, with
the previous stderr write as the base-class default and the fallback.

Between turns (no active token) Ctrl+C raises ``KeyboardInterrupt`` as before
— the prompt's own handler cancels the input line without exiting OC Chat.

**Transition deferral (plan 152 R09-01).** While a session transition is
committing, the interrupt is RECORDED and the handler returns without
raising. That is what makes the commit block genuinely uninterruptible:
``dispatch`` is covered by no ``KeyboardInterrupt`` guard, and
``KeyboardInterrupt`` is a ``BaseException`` an ``except Exception`` wrapper
misses, so a press landing between two "no-fail" installs would otherwise
strand a mixed tuple with no recovery. Repeated presses coalesce to one
pending flag; it is consumed exactly once at the next prompt boundary or
turn launch.

Definition at line 204 of file main.py.

Here is the call graph for this function:

◆ _read_startup_input()

str oc_chat.main._read_startup_input ( prompt_driver,
str prompt_text,
str default,
* ,
presenter )
protected
Read one answer at a pre-session prompt; a Ctrl+C asks before deciding.

Plan 220 P3 / TUI-02 finding 3, at ``main``'s own two pre-session
prompts: the crash auto-reload and the incomplete-tail recovery. Both
used to fold a cancelled read into *default*, so a Ctrl+C here
auto-reloaded a session, or dropped a saved tail, that nobody asked
for — and one more press carried the operator into the REPL.

EOF still yields *default*: a closed stdin is not an operator choice
and there is nobody left to confirm with. A cancel is the opposite —
somebody is at the keyboard — so it goes to
:func:`~oc_chat.core.startup.confirm_quit_or_continue`, which either
exits or returns to re-ask this same question.

Definition at line 279 of file main.py.

Here is the caller graph for this function:

◆ _recover_incomplete_session_tail()

bool oc_chat.main._recover_incomplete_session_tail ( ChatSession session,
presenter,
prompt_driver )
protected

Definition at line 457 of file main.py.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ _safe_presenter_close()

None oc_chat.main._safe_presenter_close ( presenter)
protected
Call ``presenter.close()`` if present; swallow + log on failure.

Phase TUI runtime-prep. The future TUI presenter needs reliable
teardown so it can restore the ANSI scroll region, leave the
alt-screen, and drop its key bindings — even when :func:`main` is
exiting via an unhandled exception. Extracted as a helper so:

1. :func:`main`'s ``finally`` block stays tidy.
2. The contract is unit-testable in isolation
   (``test_main_cleanup.py``).

``hasattr`` keeps the older :class:`ConsolePresenter` (which has
no ``close()``) compatible. ``try/except Exception`` keeps the
finalizer robust when cleanup itself fails mid-exception-handling.

Definition at line 523 of file main.py.

Here is the caller graph for this function:

◆ _select_presenter_once()

"object" oc_chat.main._select_presenter_once ( args)
protected
Pick the presenter exactly once for this :func:`main` invocation.

Delegates to :func:`oc_chat.cli.select_presenter`, which already
honours both ``--ui`` and the legacy ``--enhanced-ui`` flag (the
latter via ``getattr(args, "enhanced_ui", False)``). The named
indirection makes the single-call invariant explicit at the call
site — :func:`main` previously selected the presenter twice (once
for config validation, once after) which silently dropped the
``--enhanced-ui`` choice. Keep this helper as the only path that
constructs a presenter inside :func:`main`.

Definition at line 508 of file main.py.

Here is the caller graph for this function:

◆ _show_config_validation()

bool oc_chat.main._show_config_validation ( presenter,
result )
protected

Definition at line 500 of file main.py.

Here is the caller graph for this function:

◆ _show_serving_host()

None oc_chat.main._show_serving_host ( presenter,
client )
protected
Name the host that actually served the turn — where the lane says so.

Plan 220 P4 / TUI-02 finding 4. A routed lane picks its upstream per
request, so the serving host is knowable only from the response and
only after the fact (the plan-215 catalogue's provenance caveat). It is
therefore reported here, after the turn, and never claimed in advance
at selection — where the model VENDOR is shown instead.

A lane reports one by leaving a mapping on its provider's
``last_response_report``; a lane that reports nothing renders nothing.
Duck-typed exactly as the plan-215 lane panel is, so ``main`` stays
free of any import of an optional provider bundle: a bundle that
learns to report a serving host starts showing one here with no change
to this function.

Definition at line 314 of file main.py.

Here is the caller graph for this function:

◆ main()

None oc_chat.main.main ( list[str] | None argv = None)
Start the OC Chat REPL.

*argv* is exposed for testing; defaults to ``sys.argv[1:]`` when None.

Definition at line 631 of file main.py.

Here is the call graph for this function:

◆ register_active_agent()

None oc_chat.main.register_active_agent ( "object | None" agent)
Register *agent* as the SIGINT-flippable target (Phase O3e).

Public for tests; main() also calls this after building its Agent.

Definition at line 161 of file main.py.

Here is the caller graph for this function:

Variable Documentation

◆ _active_agent_ref

dict oc_chat.main._active_agent_ref = {"agent": None}
protected

Definition at line 158 of file main.py.

◆ _VERSION

oc_chat.main._VERSION = resolve_version()
protected

Definition at line 150 of file main.py.