6Multi-step task planner for agentic goal execution.
10Process-singleton plan state that the model uses to break a complex
11goal into discrete steps and track progress across ReAct iterations.
12The model calls :func:`create_plan` to set up a plan, :func:`mark_step`
13as each step completes, :func:`get_status` for progress, and
14:func:`stop_plan` to halt execution.
18- :class:`Step` / :class:`Plan` — dataclasses describing a single
19 step (description, status, result) and the active plan (goal,
20 steps, completion check).
21- :func:`create_plan` — start a new plan; replaces any existing one.
22- :func:`mark_step` — transition a step to ``done`` / ``failed`` with
23 an optional result string.
24- :func:`get_status` — return the current plan as a dict.
25- :func:`stop_plan` — clear active plan state and set the stop flag.
37- Module-level ``_active_plan`` and ``_stopped`` are process-singleton
38 state — only one plan is active at a time per REPL.
39- ``create_plan`` overwrites any existing plan without warning.
40- ``mark_step`` validates the step index; out-of-range indices return
41 an error dict rather than raising.
42- ``stop_plan`` is idempotent — calling it twice has no extra effect.
46- oc_diagnostics (structured debug logging via _dbg)
50oc_chat/docs/conventions/CONTRACTS.md §8 Plan / Step — owner of the planner
51dataclasses; §3 Tool registry contract; §4 Tool return-value conventions.
53from dataclasses
import dataclass, field
54from typing
import Literal
56from oc_diagnostics
import _dbg
64 """A single step within a :class:`Plan`."""
66 status: Literal[
"pending",
"running",
"done",
"failed"] =
"pending"
72 """A multi-step plan with a goal and ordered steps."""
74 steps: list = field(default_factory=list)
78 """Return ``True`` when every step has completed (done or failed)."""
79 return bool(self.
steps)
and all(s.status
in (
"done",
"failed")
for s
in self.
steps)
83 """Return the index of the first pending step, or ``None``."""
84 for i, s
in enumerate(self.
steps):
85 if s.status ==
"pending":
94_STEP_TEXT_KEYS = (
"description",
"step",
"title",
"text",
"name",
"task")
97def _coerce_steps(steps: object) ->
"tuple[list[str] | None, str | None]":
98 """Coerce a model-supplied *steps* value into a ``list[str]``, or return an error.
100 F28: ``create_plan`` used to ``len(steps)`` / iterate ``steps`` blindly, so a
101 ``None`` raised ``TypeError`` (invisible crash) and a bare STRING was silently
102 char-split into one step per character. This tolerates the shapes the model
103 actually sends — a list of dicts (``{'description'|'step'|'title'|...: ...}``),
104 non-string scalars (stringified) — and hard-rejects only the crash/corruption
105 shapes (``None`` and a bare string). An empty list is allowed (an empty plan is
106 a valid, supported state).
108 Returns ``(steps_list, None)`` on success or ``(None, error_message)``.
111 return None,
"steps is required: pass a non-empty list of step description strings."
112 if isinstance(steps, str):
114 "steps must be a LIST of step strings, not a single string "
115 "(a string would be split into one step per character). "
116 'Pass e.g. ["first step", "second step"].'
121 return None, f
"steps must be a list of step strings; got {type(steps).__name__}."
125 if isinstance(item, str):
127 elif isinstance(item, dict):
129 for key
in _STEP_TEXT_KEYS:
131 if isinstance(val, str)
and val.strip():
135 text = str(item).strip()
137 text = str(item).strip()
148 """Create a new plan with a list of step descriptions.
150 Validates and coerces the model-supplied arguments (F28) so a malformed call
151 returns a structured ``{"error": ...}`` the model can act on — it never raises
152 (an invisible crash) or silently char-splits a bare string.
155 global _active_plan, _stopped
157 goal_text = goal.strip()
if isinstance(goal, str)
else str(goal
or "").strip()
159 _dbg(
"OC_CHAT.TOOLS", func,
"VALIDATION_ERROR: empty goal", 3)
160 return {
"error":
"goal is required: pass a short non-empty goal string."}
164 _dbg(
"OC_CHAT.TOOLS", func, f
"VALIDATION_ERROR: {err}", 3)
165 return {
"error": err}
167 _dbg(
"OC_CHAT.TOOLS", func, f
"goal={goal_text!r} steps={len(coerced)}", 2)
168 _active_plan =
Plan(goal=goal_text, steps=[
Step(description=s)
for s
in coerced])
173 "step_count": len(coerced),
174 "steps": [s.description
for s
in _active_plan.steps],
179 """Return the current plan status including all step statuses."""
181 return {
"status":
"no active plan"}
183 "goal": _active_plan.goal,
184 "done": _active_plan.is_done,
187 {
"index": i,
"description": s.description,
"status": s.status,
"result": s.result}
188 for i, s
in enumerate(_active_plan.steps)
193def mark_step(step_index: int, status: str, result: str =
"") -> dict:
194 """Update the status of a plan step.
196 *status* must be one of ``pending``, ``running``, ``done``, ``failed``.
199 return {
"error":
"No active plan"}
200 if step_index < 0
or step_index >= len(_active_plan.steps):
201 return {
"error": f
"Invalid step index: {step_index}"}
202 if status
not in (
"pending",
"running",
"done",
"failed"):
203 return {
"error": f
"Invalid status: {status}"}
205 _active_plan.steps[step_index].status = status
206 _active_plan.steps[step_index].result = result
207 return {
"ok":
True,
"step": step_index,
"status": status}
211 """Halt the active plan.
213 Call this when the goal is complete, impossible, or the user has
218 return {
"stopped":
True}
222 "create_plan": create_plan,
223 "get_status": get_status,
224 "mark_step": mark_step,
225 "stop_plan": stop_plan,
232 "name":
"create_plan",
233 "description":
"Create a multi-step plan for a complex goal.",
237 "goal": {
"type":
"string",
"description":
"The overall goal"},
240 "items": {
"type":
"string"},
241 "description":
"Ordered list of step descriptions",
244 "required": [
"goal",
"steps"],
251 "name":
"get_status",
252 "description":
"Return the current plan status including all step statuses.",
253 "parameters": {
"type":
"object",
"properties": {},
"required": []},
260 "description":
"Update the status of a plan step.",
264 "step_index": {
"type":
"integer",
"description":
"Zero-based step index"},
265 "status": {
"type":
"string",
"description":
"One of: pending, running, done, failed"},
266 "result": {
"type":
"string",
"description":
"Optional result or error message"},
268 "required": [
"step_index",
"status"],
276 "description":
"Halt the active plan. Call this when the goal is complete, impossible, or the user has requested a stop.",
277 "parameters": {
"type":
"object",
"properties": {},
"required": []},