oc_chat
Loading...
Searching...
No Matches
tool_results.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# oc_chat/core/tool_results.py
4
5"""
6TypedDict shapes for tool return values (CONTRACTS.md §4).
7
8Purpose
9-------
10Every public tool function returns a JSON-serialisable ``dict`` — but
11until now those dicts were typed only as ``dict`` or ``dict[str, Any]``,
12leaving callers (the ReAct loop, the dependency healer, the presenter)
13without static guarantees about which keys exist. This module gives
14each conventional shape a TypedDict so callers and tool authors share
15a single source of truth.
16
17These are plain TypedDicts — at runtime they are still ``dict``s, so
18no behavior changes. Annotations only.
19
20Responsibilities
21----------------
22- :class:`CommandResult` — shell / subprocess output
23 (``run_command``, all ``oct_*`` tools).
24- :class:`ReadFileResult` — successful ``read_file`` payload.
25- :class:`WriteFileResult` — successful ``write_file`` payload.
26- :class:`ToolError` — the universal ``{"error": "..."}`` fallback.
27- :class:`ConfirmationRequired` — Tier-2 gating prompt returned by
28 destructive tools before the user has confirmed.
29- :class:`MissingDependency` — structured hint emitted when a tool's
30 optional dependency (httpx, oct binary, etc.) is not installed; the
31 :mod:`~oc_chat.core.dependency_healer` watches for these.
32- :data:`ToolReturn` — the union; the return type for tool dispatch.
33
34Diagnostics
35-----------
36Domain: OC_CHAT.TOOLS
37Levels:
38 L2 — lifecycle events
39 L3 — semantic details
40 L4 — deep tracing
41
42Contracts
43---------
44- Every TypedDict here mirrors a return shape currently produced by an
45 existing tool. The classes themselves carry no validation logic — at
46 runtime the underlying ``dict`` may still have extra keys (TypedDicts
47 are structural).
48- ``total=False`` is used wherever a key is conditionally present at
49 runtime. Required keys are kept required.
50- Consumers should branch on key presence (e.g. ``"error" in result``)
51 rather than ``isinstance`` checks; TypedDict isinstance is a
52 type-checker-only construct.
53
54Dependencies
55------------
56- oc_diagnostics (structured debug logging via _dbg)
57"""
58from __future__ import annotations
59
60from typing import Any, NotRequired, TypedDict, Union
61
62from oc_diagnostics import _dbg
63
64_dbg("OC_CHAT.TOOLS", "module", "loaded", 4)
65
66
67class CommandResult(TypedDict, total=False):
68 """Result of a subprocess invocation (run_command, oct_* tools).
69
70 Required on success: ``stdout``, ``stderr``, ``returncode``.
71 Optional metadata: ``command_info`` (run_command only), ``warnings``
72 (run_command only).
73 """
74
75 stdout: str
76 stderr: str
77 returncode: int
78 command_info: dict[str, Any]
79 warnings: list[str]
80
81
82class ReadFileResult(TypedDict):
83 """Successful ``read_file`` return shape.
84
85 ``next_offset`` is ``None`` when the file is fully read; otherwise
86 it is the byte offset to pass to a follow-up
87 ``read_file(path, offset=N)`` call.
88
89 ``path`` is the RESOLVED absolute path actually read — model-independent
90 ground truth (plan 172 / F13/F25). When the discovery fallback rescued a
91 different path than the one requested, ``resolved_via_discovery`` is ``True``
92 and ``requested_path`` carries the original argument, so a silent rescue is
93 visible instead of masking a wrong path.
94 """
95
96 content: str
97 truncated: bool
98 total_size: int
99 offset: int
100 next_offset: int | None
101 lines_count: int
102 path: NotRequired[str]
103 resolved_via_discovery: NotRequired[bool]
104 requested_path: NotRequired[str]
105
106
107class WriteFileResult(TypedDict):
108 """Successful ``write_file`` return shape.
109
110 ``redirected`` is true when the tool re-routed the write to a safer
111 path (e.g. ``logs/<name>`` for protected directories); ``actual_path``
112 is then the path actually written. ``chunks`` is non-empty when the
113 write was split (``chunked=True``).
114 """
115
116 written: str
117 original_path: str
118 redirected: bool
119 actual_path: str
120 size_bytes: int
121 timestamp: str
122 chunked: bool
123 chunks: list[dict[str, Any]]
124
125
126class ToolError(TypedDict):
127 """Universal error fallback: ``{"error": "<human-readable message>"}``.
128
129 Returned by any tool that can fail before producing a typed success
130 payload. Callers should detect failures by ``"error" in result``
131 (not by absence of success keys).
132 """
133
134 error: str
135
136
137class ConfirmationRequired(TypedDict):
138 """Tier-2 gating prompt from destructive tools (e.g. ``rm``, ``mv``).
139
140 Emitted by :func:`~oc_chat.tools.shell.run_command` and similar
141 callers when a sensitive command would proceed without explicit
142 user confirmation. Callers re-invoke with ``confirmed=True``.
143 """
144
145 requires_confirmation: bool
146 command: str
147 message: str
148
149
150class MissingDependency(TypedDict):
151 """Signal that a tool's optional dependency is not installed.
152
153 Consumed by :mod:`~oc_chat.core.dependency_healer`, which offers to
154 install the dependency on the user's behalf. ``missing_dependency``
155 is the package name; ``install_hint`` is a copy-pasteable pip
156 command.
157 """
158
159 missing_dependency: str
160 install_hint: str
161
162
163ToolReturn = Union[
164 CommandResult,
165 ReadFileResult,
166 WriteFileResult,
167 ToolError,
168 ConfirmationRequired,
169 MissingDependency,
170]
171"""Union of every conventional tool return shape.
172
173Tool functions whose success shape is none of the six (e.g.
174``list_dir``, ``text_search``, ``fetch_url``) declare their own
175TypedDict locally or remain ``dict[str, Any]`` — they are
176single-purpose and do not need the six-way union.
177"""
178
179
180__all__ = [
181 "CommandResult",
182 "ReadFileResult",
183 "WriteFileResult",
184 "ToolError",
185 "ConfirmationRequired",
186 "MissingDependency",
187 "ToolReturn",
188]