Files
openworker/coworker/environment.py
Devika Verma c1e8b8cdf3 Make the time a tool instead of a line in every turn (OPE-192, final fix)
The per-turn `<system-context>` block opened with `Now: YYYY-MM-DD HH:MM` (2c9fb74,
2026-08-20) so that absolute scheduling had a clock. The block is glued onto the last
user message — the task prompt in a tool loop — and a provider's prompt cache is
reusable only up to the first byte that differs, so every minute crossing rewrote
message one and re-processed the whole conversation. 9d6a716 worked around it with a
trailing-message shape, an Anthropic-only relocation of that message, and a
`live_clock` switch that headless runs set off. This commit removes the cause instead
and deletes the workarounds; the shape is the same on every provider again.

- The block carries no clock, on any provider. What is left (folders, skill menu,
  mode notices, memory-off notice, disable countermand) changes only when the user
  changes something, so the outbound history is byte-identical turn to turn.
- `current_time` tool (coworker/clock.py), registered for every persona: local time
  with offset and zone, the same instant in UTC, weekday. The environment block's
  "Today's date" now says it is a session-start snapshot and points at the tool.
- `sleep_for(seconds)` is back beside `sleep_until` (a relative wait needs no clock
  arithmetic; the four lead personas' manifests still told the model to call
  sleep_for, which 2c9fb74 had removed). `sleep_until`'s description points at
  current_time for the date/timezone. Timer wake messages carry their fire time.
- Deleted: `live_clock` config + COWORKER_LIVE_CLOCK, the trailing-message shape in
  `_outbound_messages`, `_relocate_ephemeral_note` in the Anthropic provider. The
  cache-diagnostics instrument (off by default) stays.

Validation runs (one long tool-heavy task, one attempt each on Fable 5.1 and Kimi K3,
compared with the 9d6a716 runs of the same task) are recorded on OPE-192.

Tests: tests/test_clock_tool.py (new), tests/test_context_block_placement.py and
tests/test_cache_breakpoint_placement.py rewritten for the single shape,
tests/test_live_clock.py removed; self-wake, attachments, message-source and
multiroot tests updated. Full suite unchanged against the Windows baseline.
2026-09-17 18:25:47 +05:30

78 lines
2.9 KiB
Python

"""Session environment context — injected into the system prompt at engine build.
Saves the agent 3-4 discovery tool calls every session (pwd, uname, git status, git log)
by telling it up front where it is and what state the workspace is in. The git snapshot is
point-in-time; the prompt labels it as such so the agent re-checks before relying on it.
"""
from __future__ import annotations
import platform as _platform
import subprocess
import sys
from datetime import date
from pathlib import Path
from typing import Optional
def _git(workspace: Path, *args: str) -> Optional[str]:
try:
out = subprocess.run(
["git", "-C", str(workspace), *args],
capture_output=True,
text=True,
timeout=5,
)
except (OSError, subprocess.SubprocessError):
return None
if out.returncode != 0:
return None
return out.stdout.strip()
def _git_snapshot(workspace: Path) -> list[str]:
if _git(workspace, "rev-parse", "--is-inside-work-tree") != "true":
return ["Git: not a git repository"]
lines = []
branch = _git(workspace, "rev-parse", "--abbrev-ref", "HEAD") or "(unknown)"
lines.append(f"Git branch: {branch}")
status = _git(workspace, "status", "--porcelain")
if status is not None:
changed = status.splitlines()
if not changed:
lines.append("Git status: clean")
else:
shown = "\n".join(changed[:20])
more = f"\n… and {len(changed) - 20} more" if len(changed) > 20 else ""
lines.append(f"Git status ({len(changed)} changed):\n{shown}{more}")
log = _git(workspace, "log", "-n5", "--pretty=format:%h %s")
if log:
lines.append(f"Recent commits:\n{log}")
return lines
def environment_context(workspace: str | Path) -> str:
"""A system-prompt block describing the session's environment and git state."""
ws = Path(workspace).expanduser().resolve()
mac = _platform.mac_ver()[0]
os_name = f"macOS {mac}" if mac else f"{_platform.system()} {_platform.release()}"
lines = [
f"Workspace: {ws}",
f"Platform: {sys.platform} ({os_name})",
f"Today's date: {date.today().isoformat()} (at session start; call current_time for the live clock)",
*_git_snapshot(ws),
]
body = "\n".join(lines)
return (
"Environment (snapshot from session start — verify before relying on git "
f"state):\n<environment>\n{body}\n</environment>\n"
"Folder scope: work inside the workspace and any folders the user has granted. Do not "
"read or list other locations (home directory sweeps, ~/Desktop, ~/Downloads, photo "
"libraries, etc.) — not even via shell commands like find/ls/grep. On macOS every such "
"touch fires an OS permission prompt the user can't connect to any action they took. "
"If a task needs files elsewhere, ask first with request_directory."
)