Skip to content

Latest commit

 

History

History
316 lines (261 loc) · 14.8 KB

File metadata and controls

316 lines (261 loc) · 14.8 KB

Repository Guidelines

This file is the single source of truth for how humans and coding agents work in this repository. CLAUDE.md is a symlink to this file, so every agent reads the same instructions.

TinyTools is the vocabulary an agent tool is written against: the Tool trait, the ToolResult it returns, and the classifications a host enforces around a call. It holds no enforcement, no registry, and no execution loop — see README.md for why that line is where it is.

Project Structure

This is a Rust 2024 cargo workspace rooted at a virtual Cargo.toml. Every crate lives under crates/, one directory per package, each directory named for the package it holds. There is no root package.

Cargo.toml              # virtual workspace: members, [workspace.package],
                        # [workspace.dependencies], [workspace.lints]
crates/
└── tinytools/          # the vocabulary crate
    └── src/
        ├── lib.rs          # crate docs + the entire public re-export surface
        ├── tool/           # the `Tool` trait
        ├── result/         # `ToolResult`, `ToolContent`
        ├── spec/           # `ToolSpec`
        ├── permission/     # `PermissionLevel`
        ├── classification/ # `ToolScope`, `ToolCategory`
        ├── call/           # `ToolCallOptions`, `ToolTimeout`
        ├── context/        # `ToolRunContext`
        ├── progress/       # `ToolProgress`, `ProgressSink`
        ├── naming/         # rendering a call for a human
        ├── rules/          # `ToolRules`: declarative allow/deny/hide/approval rules
        └── shared/         # `SharedTool`: an `Arc<dyn Tool>` as an owned belt entry
                            # each: mod.rs / types.rs / mod_tests.rs
docs/
├── specs/              # behavior and architecture specifications
├── plans/              # test-first implementation plans
└── adr/                # immutable architecture decision records

The dependency edge points one way

An agent harness depends on this crate, not the other way round. That is the single invariant an edit here can break, and CI asserts it: the "Assert the vocabulary crate stays dependency-light" step fails the build if tinyagents (or a transport, runtime, HTTP client, or native library) appears anywhere in this crate's forward dependency tree.

The consequence shows up when a tool needs something a run knows — an isolated workspace root, the caller's thread. Naming the harness's context type here would be a cycle. src/context/ erases it behind ToolRunContext, a narrow trait the harness implements for its own type. Widening that trait means a tool has grown a dependency on run internals; treat it as a signal, not a routine change.

The rule for deciding where something goes: if it describes a tool or its result, it belongs here. If it decides something — whether a permission level is sufficient, whether a timeout applies, whether an external effect needs approval — it belongs to the host, whose threat model and configuration the decision depends on.

crates/tinytools-std holds host-independent tool building blocks (cross-agent file staleness tracking, SSRF-safe URL validation, PATH probing, and the filesystem tools, which ask a host-implemented FsGate before they act). It is not part of the dependency-light vocabulary crate and may pull in tokio, parking_lot and log.

Add a crate by creating crates/<name>/ — members = ["crates/*"] picks it up by existing. Inherit version, edition, rust-version, license, and repository from [workspace.package], take shared dependencies from [workspace.dependencies], and opt into the shared lint set with:

[lints]
workspace = true

Each feature area belongs in a focused module directory under a crate's src/. A module root explains the module, wires its pieces together, and exposes the smallest useful API. Move substantial type definitions into types.rs and put module-local unit tests in a sibling <module>_tests.rs, wired from the bottom of the module root with:

#[cfg(test)]
#[path = "mod_tests.rs"]
mod tests;

Do not write inline mod tests blocks in implementation files, do not name a test file test.rs, tests.rs or <module>_test.rs, and do not let a general-purpose utils.rs or helpers.rs grow — those are a symptom of a missing module. Prefer many small modules that each do one thing well over few broad ones.

Keep public exports centralized in each crate's src/lib.rs so downstream users have one predictable surface.

Build And Test

Run every command from the repository root. These four are the contract; CI runs exactly them, so a green local run should mean a green CI run.

cargo fmt --all -- --check
cargo clippy --all-targets --all-features -- -D warnings
cargo build --all-targets --all-features
cargo test --all-features

Supporting commands:

  • cargo fmt --all — format before committing.
  • cargo test <filter> — run a focused subset while iterating.
  • cargo doc --no-deps --all-features — build the rustdoc CI also builds with RUSTDOCFLAGS="-D warnings".
  • cargo test --doc — run doctests alone when editing documentation examples.

Never skip, ignore, or delete a failing test to make a command pass. Fix the root cause, or stop and report the blocker.

Coding Style

Use standard rustfmt output and Rust 2024 idioms. Do not hand-format around rustfmt, and do not add #[rustfmt::skip] without a comment explaining why.

  • snake_case for modules, files, functions, methods, fields, and locals.
  • PascalCase for types, traits, and enum variants; SCREAMING_SNAKE_CASE for constants and statics.
  • Name things for what they are, not for their layer: RetryPolicy, not RetryHelper.
  • Prefer small, typed APIs over stringly-typed ones. Accept &str and generic impl Into<String> at boundaries; return owned, concrete types.
  • Keep the public surface minimal: default to private, and export deliberately from src/lib.rs.
  • unsafe is forbidden workspace-wide by [workspace.lints] in the root Cargo.toml. If a project genuinely needs it, relax the lint in its own commit and document every invariant with a // SAFETY: comment.

Errors

  • This crate defines no error type. Tool::execute returns anyhow::Result<ToolResult> because a tool body calls arbitrary host code and has no useful closed error set of its own; a tool that ran and decided no returns Ok(ToolResult::error(..)) instead, so the model sees the reason.
  • Do not unwrap(), expect(), or panic! in library code paths. They are fine in tests, examples, and genuinely unreachable states — where expect must carry a message explaining the invariant.
  • Document a # Errors section on every public fallible function and a # Panics section on anything that can panic.

Dependencies

Adding a dependency is a design decision. Before adding one, check whether the standard library or an existing dependency already covers the need. When you do add one:

  • pin a caret range (serde = "1"), not an exact version;
  • enable only the features you need, with default-features = false when that meaningfully trims the tree;
  • gate anything optional behind a Cargo feature, documented in Cargo.toml;
  • declare it once in the root [workspace.dependencies] when more than one crate needs it, and take it with { workspace = true };
  • never add one that pulls in an agent harness, a transport, an async runtime, an HTTP client, or a native library — CI fails the build if you do;
  • leave a comment above the entry explaining why the crate is needed and what uses it — see the existing entries for the expected tone;
  • prefer well-maintained crates with a compatible license.

Keep Cargo.lock committed; this workspace ships a single lockfile so CI and releases are reproducible.

Testing

  • Module-local unit tests live in crates/<crate>/src/<feature>/mod_tests.rs and may touch private items.
  • Integration tests live in crates/<crate>/tests/ and exercise only the public API — they are the regression suite for the crate's contract.
  • Payload types pin their serde representation in a unit test. That representation is the wire form: a host and a module that disagree about a field name fail at runtime with a decode error.
  • Use descriptive, behavioral test names: rejects_an_empty_name, not test_greet_2.
  • Cover the failure paths, not just the happy path. Every new error variant needs a test that produces it.
  • For async behavior, standardize on one runtime (tokio as a dev-dependency for tests) rather than mixing runtimes.
  • Tests must be deterministic and independent of network, wall-clock time, and execution order. Gate any live/network test behind a feature or an env var and name it live_* so it is easy to exclude.
  • Maintain at least 90% line coverage in every source file. Add or update tests with every behavior change, and note any deliberately untested edge case in the pull request description.

Write the test first when fixing a bug: a failing test that reproduces the report, then the fix that turns it green.

Documentation

Write documentation for the reader who has never seen the code.

  • Every public item gets a rustdoc comment. missing_docs is a warning that CI treats as an error.
  • Start every mod.rs and *_tests.rs with a concise module-level //! description.
  • Each crate's src/lib.rs carries its crate-level overview: what the crate does, the primary entry points, and a short runnable example. It should also say what the crate deliberately does not hold, and why.
  • Prefer concrete examples over vague description. Doc examples are compiled and run by cargo test, so they cannot drift.
  • Complex modules must include a module-level README.md covering their design, public surface, and important operational constraints.
  • Keep README.md, docs/, and module docs aligned with code changes in the same commit that changes behavior.
  • Write accepted behavior and constraints in docs/specs/ before creating a linked, implementation-ordered plan in docs/plans/. Specs define what and why; plans define how and in what sequence.
  • Keep every Markdown file, including this one, at 500 lines or fewer. When a topic outgrows that, split it into focused files and link them from the nearest README.md.

Git Workflow

  • Never commit directly to main. Branch first, one branch per logical change.
  • Do feature work in a git worktree so the main checkout stays clean.
  • Commit subjects are concise and imperative: Add retry policy to the client. Keep the subject specific to the change and under ~72 characters.
  • Make small, focused commits. Each commit should cover one logical change, build independently, and avoid mixing formatting, refactors, and behavior changes unless they are inseparable.
  • Never commit secrets. .env is git-ignored; document new variables in .env.example with placeholder values.
  • Never force-push a shared branch, rewrite published history, or bypass hooks with --no-verify.

Pull Requests

Open pull requests ready for review, not as drafts, unless the work genuinely must not merge yet. A pull request should:

  • summarize what changed and why, in a few sentences;
  • call out public API or behavior changes explicitly, or state "None";
  • list the validation commands actually run, with their outcome;
  • link the related issue;
  • include updated tests, docs, and examples in the same change.

The template in .github/PULL_REQUEST_TEMPLATE.md encodes this checklist. Address review feedback by fixing it, and reply on each thread describing what changed. Do not resolve a thread whose feedback you have not addressed or explicitly declined with a reason.

Releases

There is no release workflow yet. The template's one built and packaged a loadable TinyBus module for every platform, which this repository does not produce, so it was removed rather than left to fail.

Until one lands, consumers take this crate by path — tinyagents vendors it as vendor/tinytools and depends on crates/tinytools. Two consequences:

  • A change here is visible to a consumer only when its gitlink moves. Land the change here, then bump the submodule pointer in the consuming repository as a separate commit.
  • tinyagents cannot be published to crates.io while its dependency on this crate is path-only. Publishing this crate is the prerequisite for that, and it needs a version requirement alongside the path on the consumer side.

Follow semantic versioning in the root [workspace.package] version. Any change to the public surface that is not purely additive is a breaking change and needs a major bump (pre-1.0: a minor bump).

Agent Working Agreement

For automated contributors specifically:

  1. Read before writing. Inspect the surrounding module and match its conventions, comment density, and idiom rather than importing a house style.
  2. Verify, do not assume. Run the four contract commands and read their output before reporting a task complete. Report failures with the output; never claim a check passed that you did not run.
  3. Stay in scope. Implement what was asked. Do not opportunistically refactor, reformat, upgrade dependencies, or "fix" unrelated code — raise it instead.
  4. No placeholders in delivered code. No todo!(), no stubbed functions, no commented-out alternatives left behind. If something cannot be finished, say so explicitly.
  5. Do not weaken the guardrails. Never add blanket #[allow(...)], relax a lint, mark a test #[ignore], or loosen CI to get a green run. Fix the cause.
  6. Secrets stay out. Never read, echo, or commit .env contents, tokens, or credentials, and never paste them into a pull request or issue.
  7. Ask only when blocked. Make routine judgment calls yourself; escalate only irreversible decisions or genuine forks with no clear default.

Tests live in *_tests.rs files

  • Unit tests are never inline. Do not write a #[cfg(test)] mod tests { ... } block in a source file. Put the tests in a sibling <module>_tests.rs (mod_tests.rs beside a mod.rs, lib_tests.rs beside lib.rs) and declare it at the bottom of the module:

    #[cfg(test)]
    #[path = "foo_tests.rs"]
    mod tests;
  • The test file starts with use super::*; and carries no #[cfg(test)] of its own. It is still a child module, so it reaches private items exactly as an inline module did.

  • Name test files <module>_tests.rs; a second group for the same module is <module>_<topic>_tests.rs. Never test.rs, tests.rs or <module>_test.rs.

  • Integration tests stay in the crate's tests/ directory.

  • OpenHuman's scripts/externalize-inline-tests.mjs <repo-root> --write moves inline test modules out mechanically; without --write it only reports.