#4853

feat(tools): add nano_timer core tool (time, timezone, calendar)

Openbill-kopp-ai-devwants to merge intofrom

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

FileStatusPurpose
nanobot/agent/tools/time.pynewNanoTimerTool implementation (264 LOC)
tests/agent/tools/test_time.pynew40 tests across 7 categories
nanobot/templates/AGENTS.mdmodifiedAdds the ## nano_timer Tool hint section
pyproject.tomlmodifiedAdds 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 to all in the handler).
  • No new runtime dependencies. zoneinfo is part of the standard library; freezegun is dev-only (for deterministic DST tests).

Tests

40 new tests in tests/agent/tools/test_time.py cover:

  1. 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)
  2. DST transitions for both hemispheres (NY spring forward, London BST, Sydney AEDT, BR no-DST since 2019)
  3. Server-tz detection (POSIX TZ=... env, Docker-slim scenarios)
  4. Output format filtering per info_type
  5. IANA validation (BRT, empty string, lowercase, numeric, case-sensitivity)
  6. ToolLoader discovery and no-name-collision with existing tools
  7. 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:30 not UTC+5.
  • Server timezone label uses tzinfo.key (IANA) when available; falls back to the TZ env var, then wraps the short name (e.g. CEST, -03) in server-local(...) so users see that the value is approximate. Avoids the Docker-slim failure mode where tzname() 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:

  1. 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 reports Timezone: UTC (commit 278f40c9).
  2. The warning footer was suppressed for timezone="" because if self._tz_fallback: is falsy for empty string. Renamed to _tz_fallback_name and switched to an explicit is not None check; empty input is now rendered as <empty> so the warning is still informative (commit 278f40c9).

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.md was 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/main at ea7f4679 — 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

9
chengyongrureviewed3mo ago
chengyongru3mo ago

Automated review from nanobot

I found one portability blocker for the new core time tool.

bill-kopp-ai-dev3mo ago

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:

  1. Pinned runtime dep on tzdata>=2025.2 in pyproject.toml. This is the recommendation in the Python docs and is what pandas/babel/arrow already do. It is pure-Python and ~300KB, so the wheel size cost is negligible compared to the cross-platform correctness win.

  2. Defensive UTC fallback: even with tzdata pinned, the inner ZoneInfo("UTC") lookup is now wrapped in a try/except ZoneInfoNotFoundError that falls back to datetime.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.

  3. Regression test in a new TestNanoTimerTzdbResilience class that patches ZoneInfo to 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.py pass (40 previous + 2 new).
  • 1293/1293 tests in tests/agent/ pass (no regressions).
  • ruff check clean 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.

bill-kopp-ai-dev3mo ago

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:

  1. Pinned runtime dep on tzdata>=2025.2 in pyproject.toml. This is the recommendation in the Python docs and is what pandas/babel/arrow already do. It is pure-Python and ~300KB, so the wheel size cost is negligible compared to the cross-platform correctness win.

  2. Defensive UTC fallback: even with tzdata pinned, the inner ZoneInfo("UTC") lookup is now wrapped in a try/except ZoneInfoNotFoundError that falls back to datetime.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.

  3. Regression test in a new TestNanoTimerTzdbResilience class that patches ZoneInfo to 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.py pass (40 previous + 2 new).
  • 1293/1293 tests in tests/agent/ pass (no regressions).
  • ruff check clean 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.