ReMe/reme/steps/file_io/stat.py
Ziyang Guo 0024d3a8de fix(stat): put mtime in the answer where the job description promises it
The ``stat`` job is advertised to agents as "Stat path (size, mtime, exists,
is_dir, is_file)", but the answer only ever carried the path, type and size —
``mtime`` lived solely in metadata, and MCP returns only ``response.answer``.
So an agent asking how fresh a file is got no answer, even though the tool
description told it the field was there.

Render mtime in both branches of the answer:

    stat: topics/n.md (file, 20 bytes, mtime 2026-09-21T15:50:52.880316)
    stat: topics (dir, mtime 2026-09-21T15:50:52.880316)

The value is hoisted into a local so the answer and ``metadata["mtime"]`` are
guaranteed to come from the same ``stat()`` call rather than two reads. The
module docstring claimed the agent receives "size, mtime, mime type, and ...
frontmatter"; it now states that size and mtime are in the answer while mime
and frontmatter stay in metadata, matching reality.

Metadata is unchanged, so programmatic consumers see no difference. This is
the same answer-completeness class as #564, found while auditing which
advertised job fields actually reach the LLM.
2026-09-21 15:53:43 +08:00

104 lines
4.4 KiB
Python

"""``file_stat`` — peek at file metadata under the workspace without copying it.
Cheap inspection alternative to ``file_download``: the agent gets
size and mtime in the answer, with mime type and (for markdown files)
the parsed frontmatter alongside in metadata — enough to decide whether
to download / parse / skip without paying the copy cost.
Returns a uniform envelope:
{exists, type, size, mtime, ctime, mime, frontmatter}
``exists=False`` short-circuits everything else to ``None``. ``type``
is ``"file"`` / ``"dir"`` (covers event workspace probes too).
``frontmatter`` is populated only for ``.md`` files and only when
parsing succeeds — schema validity is a lint concern.
``path`` accepts a file path or directory path relative to the workspace.
Joined with ``file_store.workspace_path`` and inspected on disk.
"""
import mimetypes
from datetime import datetime
from pathlib import Path
import frontmatter
from ._path import display_path, gate_md, resolve_path
from ..base_step import BaseStep
from ...components import R
@R.register("stat_step")
class StatStep(BaseStep):
"""Return metadata for a file or directory under the workspace."""
async def execute(self):
assert self.context is not None
path: str = self.context.get("path", "") or ""
assert path, "path is required"
workspace_dir = Path(self.file_store.workspace_path or ".").resolve()
target, err = resolve_path(workspace_dir, path)
if err or target is None:
self.context.response.success = False
self.context.response.answer = f"Error: {err or 'invalid path'}"
self.context.response.metadata.update({"path": path, "exists": False, "error": err or "invalid path"})
self.logger.info(f"[{self.name}] path={path} error={err!r}")
return
# Auto-append .md for suffix-less paths (consistent with edit). An
# existing directory wins top priority: the daily layout keeps
# <daily_dir>/<date>/ and its <daily_dir>/<date>.md index side by
# side, and directory probing is a supported use of this step.
# Otherwise fall back to the original when the .md candidate is
# missing but the original exists (e.g., an extension-less file).
original_target = target
target, _is_md = gate_md(target)
if original_target.is_dir() or (not target.exists() and original_target.exists()):
target = original_target
if not target.exists():
probed = display_path(workspace_dir, target)
self.context.response.success = False
self.context.response.answer = f"stat: {probed} not found"
not_found: dict = {"path": path, "exists": False}
if target != original_target:
not_found["resolved_path"] = probed
self.context.response.metadata.update(not_found)
self.logger.info(f"[{self.name}] path={path} exists=False")
return
st = target.stat()
mtime = datetime.fromtimestamp(st.st_mtime).isoformat()
payload: dict = {
"path": path,
"absolute_path": str(target),
"exists": True,
"type": "dir" if target.is_dir() else "file",
"mtime": mtime,
"ctime": datetime.fromtimestamp(st.st_ctime).isoformat(),
}
if target != original_target:
payload["resolved_path"] = display_path(workspace_dir, target)
if target.is_file():
payload["size"] = st.st_size
is_md = target.suffix.lower() == ".md"
payload["mime"] = (
"text/markdown" if is_md else (mimetypes.guess_type(target.name)[0] or "application/octet-stream")
)
if is_md:
try:
meta = dict(frontmatter.loads(target.read_text(encoding="utf-8")).metadata)
except Exception:
meta = {}
payload["frontmatter"] = meta
answer = f"stat: {path} (file, {st.st_size} bytes, mtime {mtime})"
else:
answer = f"stat: {path} (dir, mtime {mtime})"
self.context.response.success = True
self.context.response.answer = answer
self.context.response.metadata.update(payload)
self.logger.info(
f"[{self.name}] path={path} type={payload['type']} "
f"size={payload.get('size', '-')} mime={payload.get('mime', '-')}",
)