1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
//! `ToolIdentityPolicy` — typed declaration each tool gives the runtime
//! about how to recognize "two calls of this tool that are the same
//! operation on the same target".
//!
//! ## Why this lives next to tool definitions
//!
//! The `SemanticLoopDetector` and any future plugin that needs to ask
//! "is this call a repeat of the last one?" must agree with the tool's
//! own dispatch contract. The historical pattern — a hand-curated
//! `match tool_name { ... }` inside the detector — drifts every time a
//! new action-dispatched tool is registered: the detector's allowlist
//! and the tool's `#[serde(tag = "action")]` enum live in different
//! files with no compile-time link. The `office` tool shipped without
//! the matching allowlist update; every `office.pdf_fields` repeat
//! collapsed to `(office, default)` instead of `(office, pdf_fields)`,
//! so the recovery message went generic.
//!
//! `ToolIdentityPolicy` is the single source of truth for that
//! identity contract. Each tool declares it on the `AgentTool` trait;
//! the detector reads the declaration from the `ToolRegistry`. Adding
//! a tool can no longer drift the detector because the detector reads
//! the tool's own declaration.
//!
//! ## Three independent declarations
//!
//! - `operation_arg`: top-level arg whose value names the operation
//! for action-dispatched tools. Two distinct operations of the same
//! tool get distinct identities.
//! - `target`: how to extract a "persistent target" string (file
//! path, URL, shell command prefix). Used both as a display token in
//! recovery messages and as the key for per-target error counters
//! that survive intervening successes.
//! - `args_key_fn`: tool-specific override for the full identity
//! string used to recognize repeats. Defaults to
//! `target={tool_name}:{target}` when only `target` is declared,
//! and to a canonical-JSON hash of the normalized args when nothing
//! else applies.
use Value;
/// Tool-specific target extractor. Returns the "persistent target" of
/// a call (file path, URL, shell-command prefix) — the thing whose
/// repeat would mean "the same work is being attempted again." `None`
/// means the call has no persistent target (e.g. `office.pdf_fields`
/// — the action itself is what repeats).
pub type TargetFn = fn ;
/// Tool-specific full-identity extractor. Returns the opaque identity
/// string the detector compares for equality, or `None` to fall back
/// to the default `target={tool}:{target}` / canonical-JSON paths.
pub type ArgsKeyFn = fn ;
/// How to extract the persistent target of a call.
/// Typed identity contract for one tool. All fields default to `None`
/// — a tool that does not override `identity_policy` is treated as
/// "single operation, no persistent target, opaque args" (matches the
/// historical fall-through behavior for unknown tools).
/// Operation key for a call. Returns `"default"` when the tool has
/// not declared an `operation_arg` — same value the old hardcoded
/// fall-through used, so downstream messaging is unchanged for
/// non-action tools.
/// Persistent target string for a call (file path, URL, shell command
/// prefix). `None` means the tool has no persistent target — the
/// detector then keys per-target counters on the full args identity
/// instead.
/// Full args/identity key for two calls of a tool. `None` means the
/// tool declares neither a custom args-key fn nor a persistent target
/// — the caller should fall back to its own canonical-JSON identity
/// (see `SemanticLoopDetector::semantic_args_key`).
///
/// `tool_name` is threaded through so the `target`-shaped default
/// emits the historical `target={tool_name}:{target}` format without
/// duplicating the tool name on every declaration.