6Prompt-layer classification inventory.
10Phase C5 records the authoritative classification of prompt and
11request layers that affect prefix-cache stability. The runtime already
12separates the immutable system prefix from variable context; this module
13turns that separation into a typed inventory that tests and docs can
16Plan 157 adds the session-anchors block (sandbox root + cwd + git branch)
17and an in-process ``.git/HEAD`` parser that handles linked-worktree
18``.git``-FILE indirection — no subprocess, no git invocation.
22- :class:`PromptLayer` describes one prompt/request layer, where it is
23 assembled, and whether it is immutable or variable.
24- :data:`PROMPT_LAYERS` is the canonical C5 inventory.
25- :func:`prompt_layer_inventory` returns the immutable inventory tuple.
26- :func:`layers_by_classification` filters the inventory for tests and
28- :func:`session_anchors_block` renders the session self-location block
29 (plan 157) — sandbox root, process cwd, and git branch.
30- :func:`_git_branch` is the in-process ``.git/HEAD`` parser that
31 handles normal repos, linked worktrees, and detached HEAD.
43- Layer names are stable public identifiers for documentation and tests.
44- Classifications are deliberately binary: ``immutable`` means the layer
45 belongs in the cache-stable prefix; ``variable`` means it must remain
46 outside that prefix or be treated as per-turn/per-session data.
47- The inventory is descriptive. It must not mutate session messages.
48- ``_git_branch`` never invokes subprocess or git; it reads ``.git/HEAD``
49 (and the linked-worktree indirection file) via in-process file I/O.
53- oc_diagnostics (_dbg)
54- oc_chat.tools.filesystem (lazy; _sandbox_root_info for the sandbox-root and source-label pair)
56from __future__
import annotations
58from dataclasses
import dataclass
59from pathlib
import Path
60from typing
import Literal
62from oc_diagnostics
import _dbg
65PromptLayerClassification = Literal[
"immutable",
"variable"]
68@dataclass(frozen=True)
70 """One classified prompt or request layer."""
73 classification: PromptLayerClassification
81PROMPT_LAYERS: tuple[PromptLayer, ...] = (
83 name=
"base_system_prompt",
84 classification=
"immutable",
85 role_or_scope=
"messages[0].role == system",
86 owner=
"oc_chat.core.policy.CANONICAL_POLICY",
87 source=
"oc_chat.core.session.ChatSession.messages",
88 cache_policy=
"Must remain byte-stable unless the user changes /system.",
89 notes=
"The first message anchors the cacheable prompt prefix.",
92 name=
"compact_tool_map",
93 classification=
"immutable",
94 role_or_scope=
"messages[0].content suffix",
95 owner=
"oc_chat.core.tool_prompt.compact_tool_map",
96 source=
"one-line-per-tool map rendered from the mode-filtered registry",
97 cache_policy=
"Behind the compact-tool-map sentinel; changes only when the registered tools change.",
98 notes=
"Full JSON Schema is still sent through the API tools parameter (plan 135 §3.3).",
101 name=
"variable_environment_context",
102 classification=
"variable",
103 role_or_scope=
"messages[1].content (## Environment sublayer)",
104 owner=
"oc_chat.core.startup._inject_variable_context",
105 source=
"date/timezone/cwd/branch/git/model/mode per the active mode env_fields",
106 cache_policy=
"Outside messages[0]; refreshed on session load and model switch.",
107 notes=
"Volatile facts like date + branch live here, never in messages[0] (plan 135 §3.5).",
110 name=
"variable_context_user_message",
111 classification=
"variable",
112 role_or_scope=
"messages[1].role == user",
113 owner=
"oc_chat.core.startup._inject_variable_context",
114 source=
"project tree plus AGENT_CONTEXT.md",
115 cache_policy=
"Always outside messages[0]; refreshed on session load.",
116 notes=
"Marked by VARIABLE_CONTEXT_SENTINEL.",
119 name=
"session_anchors",
120 classification=
"variable",
121 role_or_scope=
"messages[1].content suffix (after project tree)",
122 owner=
"oc_chat.core.prompt_layers.session_anchors_block",
123 source=
"sandbox root (_sandbox_root_info) + cwd + git branch (.git/HEAD parser)",
124 cache_policy=
"Always outside messages[0]; refreshed on session load.",
125 notes=
"Session self-location block for agent orientation (plan 157).",
128 name=
"chat_turn_user_input",
129 classification=
"variable",
130 role_or_scope=
"user history after the context layer",
131 owner=
"oc_chat.core.session.ChatSession.add_user",
132 source=
"interactive input, batch input, or API caller text",
133 cache_policy=
"Per-turn content; never part of the immutable prefix.",
134 notes=
"The latest user task drives the ReAct turn.",
137 name=
"assistant_and_tool_history",
138 classification=
"variable",
139 role_or_scope=
"assistant/tool history",
140 owner=
"oc_chat.core.react_loop.run_turn",
141 source=
"assistant replies, tool calls, and tool results",
142 cache_policy=
"History grows during a turn and across turns.",
143 notes=
"API shaping may truncate copies, but the session log is full.",
146 name=
"finalize_iteration_nudge",
147 classification=
"variable",
148 role_or_scope=
"API-only synthetic user message",
149 owner=
"oc_chat.core.api_context.messages_for_api",
150 source=
"FORCE_FINALIZE_USER_MESSAGE",
151 cache_policy=
"Transient final-iteration nudge; never persisted.",
152 notes=
"Injected only when inject_finalize_nudge is true.",
155 name=
"sub_loop_result_message",
156 classification=
"variable",
157 role_or_scope=
"synthetic user message",
158 owner=
"oc_chat.core.react_loop.run_turn",
159 source=
"[Sub-loop result] payload",
160 cache_policy=
"Per-task result; appended after prompt prefix.",
161 notes=
"Lets the main loop reason over delegated work output.",
164 name=
"provider_request_options",
165 classification=
"variable",
166 role_or_scope=
"request metadata outside messages",
167 owner=
"oc_chat.core.react_loop.run_turn",
168 source=
"model, effort, extra_body, tools, max_tokens",
169 cache_policy=
"Provider/request dependent; not a system prompt layer.",
170 notes=
"Tracked because it can still affect provider cache keys.",
176 """Return the canonical C5 prompt-layer inventory."""
177 func =
"prompt_layer_inventory"
178 _dbg(
"OC_CHAT.MAIN", func, f
"layers={len(PROMPT_LAYERS)}", 4)
183 classification: PromptLayerClassification,
184) -> tuple[PromptLayer, ...]:
185 """Return prompt layers with the requested C5 classification."""
186 func =
"layers_by_classification"
188 layer
for layer
in PROMPT_LAYERS
189 if layer.classification == classification
191 _dbg(
"OC_CHAT.MAIN", func, f
"{classification}={len(layers)}", 4)
199 """Return the current git branch name, or ``None`` when no git repo exists.
201 In-process file reads only — no subprocess, no git invocation.
204 * **Normal ``.git/`` directory** — reads ``.git/HEAD`` directly.
205 * **Linked-worktree ``.git`` FILE** — parses the ``gitdir: <path>``
206 indirection line and reads ``HEAD`` from the real git dir.
207 * **Detached HEAD** — returns the short (7-char) hash.
209 Returns ``None`` when ``.git`` is missing or unreadable.
211 dot_git = root /
".git"
212 if not dot_git.exists():
216 if dot_git.is_file():
218 content = dot_git.read_text(encoding=
"utf-8").strip()
221 if content.startswith(
"gitdir: "):
222 gitdir = Path(content[len(
"gitdir: "):].strip())
228 head = gitdir /
"HEAD"
230 head_content = head.read_text(encoding=
"utf-8").strip()
237 if head_content.startswith(
"ref: refs/heads/"):
238 return head_content[len(
"ref: refs/heads/"):]
240 if head_content.startswith(
"ref: "):
241 return head_content[len(
"ref: "):]
243 return head_content[:7]
247 """Return the ``## Session anchors`` block for the system prompt.
249 Composed of three lines:
251 * **sandbox root** — effective root path and its source label,
252 obtained from :func:`oc_chat.tools.filesystem._sandbox_root_info`.
253 * **process cwd** — :func:`pathlib.Path.cwd`.
254 * **git branch** — of the SANDBOX ROOT (the anchors' subject — in a
255 worktree launch the cwd has no ``.git`` while the root does), via
256 :func:`_git_branch`, rendered as the branch
257 name, ``(detached: <hash>)``, or ``(no git)``.
259 Designed to be injected into ``messages[1]`` after the project tree
260 so every agent session can answer "where am I?" on the first turn.
264 root_path, label = _sandbox_root_info()
268 if branch_raw
is None:
269 branch_str =
"(no git)"
270 elif len(branch_raw) == 7
and all(c
in "0123456789abcdef" for c
in branch_raw.lower()):
271 branch_str = f
"(detached: {branch_raw})"
273 branch_str = branch_raw
276 "## Session anchors (verify before acting)\n"
277 f
"- sandbox root: {root_path} (source: {label})\n"
278 f
"- process cwd: {cwd}\n"
279 f
"- git branch: {branch_str}"
286 "PromptLayerClassification",
288 "layers_by_classification",
289 "prompt_layer_inventory",
290 "session_anchors_block",
tuple[PromptLayer,...] prompt_layer_inventory()
str|None _git_branch(Path root)
tuple[PromptLayer,...] layers_by_classification(PromptLayerClassification classification)
str session_anchors_block()