镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

feat(policy): let a tool declare that it polls - #105

Open
YellowSnnowmann wants to merge 1 commit into
tinyhumansai:mainfrom
YellowSnnowmann:feat/tool-repeat-polling
Open

YellowSnnowmann wants to merge 1 commit into
tinyhumansai:mainfrom
YellowSnnowmann:feat/tool-repeat-polling

Conversation

@YellowSnnowmann

Copy link
Copy Markdown
Member

Summary

A wait or a status poll answers the same text until whatever it waits on moves. A host's loop guards count an
identical call that returns an identical result as a repeat, so a run that waits after each step gets stopped while it
is making progress. TinyAgents' repeat-progress guard did this to an OpenIntents shopping run: it halted the run on its
sixth identical browser_wait (same page, same "Done. Now on (<title>)" answer), one press before checkout. Today
the only exemption is a hard-coded tool name in TinyAgents (wait_subagent), so a host's own wait tools can't opt out.

This PR lets a tool declare the contract itself, on the declarative policy surface, the same way replay works:
ToolRuntime::repeat: ToolRepeat, either Counted (the default) or Polling. ToolPolicy::polling() sets it.

The TinyAgents side, which builds the exemption from these declarations, follows in a separate PR that moves the
vendor/tinytools gitlink to this commit.

Related issue

None.

API or behavior changes

  • New ToolRepeat enum (Counted default, Polling), exported from the crate root.
  • New ToolRuntime::repeat field, with #[serde(default)] and always serialized, like replay.
  • New ToolPolicy::polling() builder. Unlike the other builders it leaves classified unchanged: polling describes
    how a tool is called, not what it may touch.
  • Wire shape: runtime gains a "repeat" key ("counted" / "polling"). A payload written before the field existed
    decodes as counted.
  • Breaking for source: an exhaustive ToolRuntime { … } literal (one without ..Default::default()) no longer
    compiles, as happened when replay was added. Per the versioning rule this belongs in the next minor release; this
    PR does not bump the workspace version.

Validation

Commands actually run, with their outcome:

  • cargo fmt --all -- --check: clean
  • cargo clippy --all-targets --all-features -- -D warnings: clean
  • cargo build --all-targets --all-features: clean
  • cargo test --all-features: 739 passed, 0 failed
  • RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features: clean

Tests

In crates/tinytools/src/policy/mod_tests.rs:

  • Both pinned wire shapes now carry repeat (counted by default, polling when fully populated).
  • repeats_are_counted_unless_a_tool_says_it_polls: the default and the read-only baseline are Counted.
  • repeat_round_trips_through_json
  • the_polling_builder_marks_the_runtime_and_classifies_nothing: sets Polling, keeps classified and every other
    declaration.
  • legacy_runtime_json_without_repeat_counts_repeats

Documentation

docs/specs/tinytools-vocabulary.md has a new "Extension: repeat classification" section. Rustdoc covers the enum,
the field and the builder.

Checklist

  • The change is focused on one logical change
  • No new #[allow(...)], #[ignore], or relaxed lints. A fourth bool on ToolRuntime would have tripped
    clippy::struct_excessive_bools, which is one more reason this is an enum.
  • No secrets, tokens, or .env contents in the diff or the description

A wait or a status poll answers the same text until whatever it waits on
moves. A host's loop guards count an identical call that returns an
identical result as a repeat, so a run that waits after each step gets
stopped while it is making progress: TinyAgents' repeat-progress guard
halted an OpenIntents shopping run on its sixth identical browser_wait,
one press before checkout.

ToolRuntime gains `repeat: ToolRepeat`: Counted by default, Polling when
the tool waits or polls. ToolPolicy::polling() sets it and leaves
`classified` alone, since polling says how a tool is called, not what it
may touch. Payloads written before the field existed decode as Counted.
@coderabbitai

coderabbitai Bot commented Nov 12, 2026

Copy link
Copy Markdown

Important

  • Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 91c8a52c-caf0-4271-8ea2-55806322ca8d
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

@tinysweeper

tinysweeper Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

Tiny Sweeper review

Reviewed head: 15e500a5fbee1e1727327df933cb52d9ebab9cd2

Verdict: Ready for maintainer review · Priority: none
Coverage: Reviewed 5 of 5 files.
Pull request purpose (title): feat(policy): let a…

Diagnostics and supporting details

Full summary

Pull request purpose (title): feat(policy): let a tool declare that it polls. This pull request, titled "feat(policy): let a tool declare that it polls," adds a `ToolRepeat` enum (`Counted` default, `Polling`) and a `ToolRuntime::repeat` field so a tool can declare that an identical call returning an identical result is expected rather than a sign the caller is stuck. It also adds a `ToolPolicy::polling()` builder that sets `Polling` without touching `classified`, re-exports the new type from the policy module and crate root, pins the new wire shape in tests, and documents the extension in the vocabulary spec. The latest-push delta is unavailable, so no push-only comparison can be described.

Review snapshot

Change surface Files Review signal Count
Production 3 Active findings 0
Tests 1 Noted findings 0
Documentation 1 Resolved findings 0
Configuration 0 Pending checks/questions 0

Completeness: Complete
Test assessment: Test coverage is assessed from changed tests and lane evidence; execution is not claimed without trusted check data.

What changed

Adds the `ToolRepeat` enum with `Counted` as the serde/default value and `Polling` for wait or status-poll tools, and a `#[serde(default)] repeat` field on `ToolRuntime` so payloads written before the field existed decode as `Counted`. A new `ToolPolicy::polling()` builder sets `Polling` while deliberately leaving the `classified` flag unchanged. The type is re-exported through `policy` and the crate root. The pinned JSON wire shape tests on both sides now include the `repeat` key, new tests cover defaulting, round-tripping, the builder's preservation of other declarations, and legacy payload decoding, and the vocabulary spec gains an "Extension: repeat classification" section that notes the additive-on-wire but source-breaking nature of the new field for exhaustive `ToolRuntime` literals.

Features

  • Added — ToolPolicy::polling() builder: Provides a `#[must_use]` builder that sets `runtime.repeat = ToolRepeat::Polling` and, unlike other builders, leaves `classified` as-is because polling describes call mechanics rather than safety classification. (crates/tinytools/src/policy/types.rs impl ToolPolicy {)
  • Modified — Policy and crate-root re-exports: Re-exports `ToolRepeat` alongside the other policy types so callers can name the new enum without importing the internal module. (crates/tinytools/src/lib.rs pub use naming::{, crates/tinytools/src/policy/mod.rs)
  • Modified — Pinned JSON wire-shape tests: Updates the fully populated and default wire-shape assertions to include the `"repeat"` key (`"counted"` and `"polling"`), locking the serialized contract on both encode and decode sides. (crates/tinytools/src/policy/mod_tests.rs fn fully_populated_policy_has_a_pinned_json_wire_shape() {, crates/tinytools/src/policy/mod_tests.rs fn policy_round_trips_through_its_stable_json_shape() {)
  • Modified — Vocabulary spec extension: Documents the repeat classification, its default and legacy-decode behavior, the `polling()` builder's exception on `classified`, and the minor-release versioning implication for exhaustive `ToolRuntime` literals. (docs/specs/tinytools-vocabulary.md see "Versioning" — so they shipped as a `0.2.0` → `0.3.0` minor bump.)

Tests

  • unit — Serializes a `ToolRuntime` set to `Polling`, checks the encoded JSON contains `"repeat":"polling"`, and decodes it back.: Covers the round-trip of the new field through its wire representation. (crates/tinytools/src/policy/mod_tests.rs fn policy_round_trips_through_its_stable_json_shape() {)
  • unit — Decodes a `ToolRuntime` JSON literal lacking `repeat` and asserts it becomes `ToolRepeat::Counted`.: Covers backward-compatible decoding of persisted payloads, stated as enabled by `serde(default)`. (crates/tinytools/src/policy/mod_tests.rs fn legacy_runtime_json_without_replay_defaults_to_never() {)

Findings

No active actionable findings.

Before merge

None.

Agent review details

critique

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: Reviewed 5 files; 1 finding.

security

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: Reviewed 4 files; 0 findings. 1 file was not security-reviewed: docs/specs/tinytools-vocabulary.md (prose or tabular data).

tests

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: The change adds `ToolRepeat` with a `#[serde(default)]` field on `ToolRuntime` and a `polling()` builder, and the diff does carry real tests: default value, round-trip, builder behaviour (including that `classified` is preserved), legacy JSON without the field decoding to `Counted`, and the fully-populated pinned wire shape updated on both sides. Every stated invariant has a test that would fail if it broke, and the new field cannot fail to decode on older payloads because of `serde(default)`. No reportable defect found.

commits

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Positive: The commits lane found no sensitive content in committed files.
  • Lane summary: No sensitive content in committed files.

description

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Positive: The description lane reported the description matches the diff, including the new enum, field, builder, re-exports, pinned wire shapes, legacy-decode test, spec section, and the source-breaking note for exhaustive `ToolRuntime` literals.
  • Lane summary: The description matches the diff: the new `ToolRepeat` enum, the `ToolRuntime::repeat` field with `#[serde(default)]`, the `ToolPolicy::polling()` builder that leaves `classified` alone, the re-exports, the serde wire shapes pinned in tests, the legacy-payload decode test, and the spec section are all present as described, and the stated source-breaking change for exhaustive `ToolRuntime` literals is accurate. I found no defect worth reporting.
Evidence and run details
  • Models: ladder/vectors-oai3, deepseek-v4.1-flash
  • Spend: $0.001772
  • Tokens: 393999 input · 6361 output · 138240 cached · 376 embedding
  • Updated: 2026-11-12T20:54:53Z
Head State Pass summary
15e500a5fbee ready for maintainer review 0 active finding(s), 0 resolved finding(s) (at 2026-11-12T20:54:53Z)

tinysweeper 0.1.2

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tinysweeper found nothing blocking. Approving.

Diagnostics

Total spend: $0.0018

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant