Summary
Adds a small, dependency-free core tool (nano_timer) that returns the
current UTC time, the user's local time (converted via IANA timezone with
automatic DST handling), and calendar fields (weekday, week-of-year,
day-of-year, weekend flag).
The tool reads the user's timezone from ctx.timezone (sourced from
agents.defaults.timezone in config.json), so it is correctly
initialised in any agent session without further configuration. When the
configured IANA timezone is invalid, the tool falls back to UTC and
renders an inline warning that names the bad value.
This is the first tool that follows the "natural-language hint in AGENTS.md" pattern — see the second commit.
Changes
| File | Status | Purpose |
|---|---|---|
nanobot/agent/tools/time.py | new | NanoTimerTool implementation (264 LOC) |
tests/agent/tools/test_time.py | new | 40 tests across 7 categories |
nanobot/templates/AGENTS.md | modified | Adds the ## nano_timer Tool hint section |
pyproject.toml | modified | Adds freezegun>=1.5,<2.0 to dev |
Tool surface
- Name:
nano_timer - Auto-discovered via the existing
ToolLoader.discover()path — no manual registry entry, no schema plumbing in__init__.py. - One parameter:
info_type(nullable,time | timezone | location | calendar | all; defaults toallin the handler). - No new runtime dependencies.
zoneinfois part of the standard library;freezegunis dev-only (for deterministic DST tests).
Tests
40 new tests in tests/agent/tools/test_time.py cover:
- Timezone math (UTC, São Paulo, India UTC+5:30, Nepal UTC+5:45,
Chatham Islands UTC+12:45, with
round_trip_utc_to_user_to_utc) - DST transitions for both hemispheres (NY spring forward, London BST, Sydney AEDT, BR no-DST since 2019)
- Server-tz detection (POSIX
TZ=...env, Docker-slim scenarios) - Output format filtering per
info_type - IANA validation (BRT, empty string, lowercase, numeric, case-sensitivity)
- ToolLoader discovery and no-name-collision with existing tools
- RequestContext binding (
set_context,create)
Wider regression: 326/326 adjacent tests pass across
tests/agent/tools/, tests/tools/, tests/cron/, and
tests/agent/test_context_aware.py. ruff check nanobot/ is clean.
Edge cases handled
- Fractional-hour offsets (India UTC+5:30, Nepal UTC+5:45, Chatham
UTC+12:45) render as
UTC+5:30notUTC+5. - Server timezone label uses
tzinfo.key(IANA) when available; falls back to theTZenv var, then wraps the short name (e.g.CEST,-03) inserver-local(...)so users see that the value is approximate. Avoids the Docker-slim failure mode wheretzname()returns a bare numeric offset. - Output formatter guards against double blank lines between
blocks when
info_type="all"or a fallback warning is appended. - Invalid IANA ("BRT", empty string, lowercase, numeric) is
handled gracefully: math layer falls back to UTC, rendered output
always reports
Timezone: UTC, and the warning footer names the bad input for traceability.
Bug fixes already in branch history
Two bugs surfaced when the self-test harness was actually executed end-to-end against the live fallback path:
- The User Local Time block echoed the bad input back as the
timezone label (
Timezone: BRT) when the configured IANA was invalid. Now it always reportsTimezone: UTC(commit278f40c9). - The warning footer was suppressed for
timezone=""becauseif self._tz_fallback:is falsy for empty string. Renamed to_tz_fallback_nameand switched to an explicitis not Nonecheck; empty input is now rendered as<empty>so the warning is still informative (commit278f40c9).
A third small fix corrects the sign of diff_from_utc_hours so that
São Paulo reports -3h (not +3h) (commit 250a4d43).
Why a separate template commit?
The AGENTS template change is intentionally kept in a separate commit
(466f11b6, plus a follow-up translation cleanup abc14053) so that
maintainer review can focus on the user-visible wording before it
reaches every workspace via nanobot onboard. The tool itself does not
require this template change to function; the section is a
natural-language hint that influences when the model chooses to call
the tool.
Notes for reviewers
CONTRIBUTING.mdwas followed: topic branch, no unrelated changes, no CI workflow changes, ruff E/F/I/N/W clean, line length ≤ 100.- The branch is rebased directly on top of
upstream/mainatea7f4679— no merge conflicts. - I have not opened an issue first; the design is documented in this PR description. Happy to split into smaller PRs or move pieces around based on feedback.
Test plan for reviewers
A self-test harness and result file are at the repository root
(NANO_TIMER_TEST_PROMPT.md and NANO_TIMER_TEST_RESULTS.md). The
relevant entry point is Layer C (direct instantiation of
NanoTimerTool with different timezones), which is fully
deterministic and reports 18/18 PASS for the entire timezone matrix
including DST and IANA-fallback cases.
Activity
9Thanks for catching this — you are right, this is a real portability gap. I verified your reproduction locally: with PYTHONTZPATH="" and tzdata not installed, both ZoneInfo("America/Sao_Paulo") and the inner ZoneInfo("UTC") fallback raise ZoneInfoNotFoundError, and the tool returns the generic "Error getting time information" instead of the promised UTC report.
Fixed in commit 4cd6f138:
-
Pinned runtime dep on
tzdata>=2025.2inpyproject.toml. This is the recommendation in the Python docs and is whatpandas/babel/arrowalready do. It is pure-Python and ~300KB, so the wheel size cost is negligible compared to the cross-platform correctness win. -
Defensive UTC fallback: even with
tzdatapinned, the innerZoneInfo("UTC")lookup is now wrapped in atry/except ZoneInfoNotFoundErrorthat falls back todatetime.timezone.utc(the stdlib constant, which never depends on the tzdb). The tool will therefore never hard-fail on the no-tzdb path — it just degrades to UTC. -
Regression test in a new
TestNanoTimerTzdbResilienceclass that patchesZoneInfoto raise for both the user tz and the"UTC"key, then asserts the full markdown report still renders with the inline warning footer naming the bad input.
Validation:
- 42/42 tests in
tests/agent/tools/test_time.pypass (40 previous + 2 new). - 1293/1293 tests in
tests/agent/pass (no regressions). ruff checkclean on the touched files.
The 47 unrelated failures elsewhere (test_feishu_streaming, test_mcp_presets_api) reproduce on upstream/main without my branch, so they are pre-existing and out of scope for this PR.
PR #4853 is now updated with the new commit on top of the original 5.
Thanks for catching this — you are right, this is a real portability gap. I verified your reproduction locally: with PYTHONTZPATH="" and tzdata not installed, both ZoneInfo("America/Sao_Paulo") and the inner ZoneInfo("UTC") fallback raise ZoneInfoNotFoundError, and the tool returns the generic "Error getting time information" instead of the promised UTC report.
Fixed in commit 4cd6f138:
-
Pinned runtime dep on
tzdata>=2025.2inpyproject.toml. This is the recommendation in the Python docs and is whatpandas/babel/arrowalready do. It is pure-Python and ~300KB, so the wheel size cost is negligible compared to the cross-platform correctness win. -
Defensive UTC fallback: even with
tzdatapinned, the innerZoneInfo("UTC")lookup is now wrapped in atry/except ZoneInfoNotFoundErrorthat falls back todatetime.timezone.utc(the stdlib constant, which never depends on the tzdb). The tool will therefore never hard-fail on the no-tzdb path — it just degrades to UTC. -
Regression test in a new
TestNanoTimerTzdbResilienceclass that patchesZoneInfoto raise for both the user tz and the"UTC"key, then asserts the full markdown report still renders with the inline warning footer naming the bad input.
Validation:
- 42/42 tests in
tests/agent/tools/test_time.pypass (40 previous + 2 new). - 1293/1293 tests in
tests/agent/pass (no regressions). ruff checkclean on the touched files.
The 47 unrelated failures elsewhere (test_feishu_streaming, test_mcp_presets_api) reproduce on upstream/main without my branch, so they are pre-existing and out of scope for this PR.
PR #4853 is now updated with the new commit on top of the original 5.
I found one portability blocker for the new core time tool.