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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
//! One agent branch as the view-model carries it (§7.1, §2.3) — the fattest of
//! [`super`]'s inert structures, on its own file for §12's pre-split rule.
//!
//! Every field is a fact read off refs or disk on the tick that built it (§3.5
//! stateless re-read); the two `impl` folds beside them derive nothing new,
//! they only state one ladder in one place.
use super::{StepCommit, ToolCall};
use crate::git_tree::{AgentState, Stream};
/// One agent branch (`agents/<agent-id>`, §2.3). Named `Agent` — every
/// row is an agent, not an "unmerged conversation branch"; nothing merges
/// (§2.6), so the merged/unmerged framing is gone.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Agent {
/// Branch name (`agents/<agent-id>`). Held separately so the renderer
/// can label rows without re-deriving.
pub branch_name: String,
/// The agent id (`agents/` prefix stripped) — the identity everywhere
/// (steps/, inbox/, worktree dir, descent). The descent tree keys off
/// this (§2.3).
pub agent_id: String,
pub tip_oid: String,
pub tip_short_oid: String,
pub tip_timestamp_unix: i64,
/// The agent's **last action of any kind** (§11 recency, bl-cad5): the
/// newest of the tip commit timestamp, the newest `messages/` entry mtime
/// and the latest step's `response.json` mtime (the live streaming tail).
/// Committing is only *one* way an agent acts, so
/// [`tip_timestamp_unix`](Agent::tip_timestamp_unix) alone leaves a
/// streaming or just-messaged conversation looking stale. Gathered here at
/// snapshot time (§3.5 stateless re-read) so the §11 list's sort and its
/// age label are one fact read once — never a stat from the render path.
pub last_action_unix: i64,
/// How many messages have **ever landed** in this agent's `messages/`
/// directory (§5.1 #12): the monotonic `NNN` counter's high-water mark,
/// read off the very readdir
/// [`last_action_unix`](Agent::last_action_unix) already performs, so the
/// two facts cost one directory walk (the §5.1 #10 discipline). Not a
/// count of files present (bl-fde5): compaction deletes entries below the
/// surviving counter, so a file count goes *down* mid-flight while this
/// fact never does.
///
/// Its one consumer is §7.2's pending echo: the operator's just-sent
/// message is superseded when this count passes the baseline the echo
/// recorded — a landed message advances the counter, a compaction never
/// lowers it, so the predicate has one reading. Nothing cheaper says it
/// honestly — [`last_action_unix`](Agent::last_action_unix) moves on a
/// streaming token and [`tip_oid`](Agent::tip_oid) on any step commit, so
/// either would retire the echo while the message was still missing.
pub messages: usize,
/// When the latest step's **model call began** (§5.1 #28 elapsed, bl-9dfb):
/// the mtime of that step's `request.json`. litany writes that file exactly
/// once, immediately before handing the request to the adapter — the same
/// instant it later records as `meta.json`'s `started_at` — and never
/// appends to it, so its mtime *is* the call's start rather than its last
/// sign of life. `meta.json` itself cannot serve: it lands only after the
/// call returns, so it is absent for exactly the call the strip is timing.
/// `None` when no step has written a request yet, or its stamp is
/// unreadable — the strip then omits elapsed rather than inventing one.
/// Gathered here at snapshot time (§3.5) like every other disk fact; the
/// render path never stats.
pub call_start_unix: Option<i64>,
/// Commits on this branch past every config lineage, oldest to newest
/// (each with its subject).
pub steps: Vec<StepCommit>,
pub preview: Option<String>,
/// What the latest step's live `response.json` says (§5.1 #10, #28b): the
/// answer text so far, the reasoning text so far, and the kind of the last
/// content delta. **One value off one read** — three fields filled from
/// one pass could otherwise be filled from three, and then they would be
/// three mid-write states of one file rather than its answer.
///
/// Re-derived from `<workspace>/steps/<agent-id>/<NNN>/response.json` on
/// every `from_repo` call (§3.5: stateless re-read on each tick) — and, on
/// the **focused** conversation, superseded per frame by the §7.2 live-tail
/// follower's fresher fold of the same file (`app::live`), which is the one
/// place a rendered `Agent` carries something no derivation put there.
pub stream: Stream,
/// Tool calls under this branch's latest step's `tools/` directory
/// (ARCH §3.3), derived purely from `input.json` / `output.json`
/// presence. Re-derived on every `from_repo` call (§3.5).
pub tool_calls: Vec<ToolCall>,
/// §3.5 agent-state classification, derived from the executor lock and
/// the latest step's `response.json` terminal segment. Re-derived on
/// every `from_repo` call (§3.5).
pub state: AgentState,
/// The latest turn was **cut off at the output limit** (§4.4, bl-fb87):
/// at rest, with the latest step's settled tail framing cleanly around a
/// `finish` whose canonical reason is `length`. Transport completion is not
/// task completion — the segment kept every wire promise while the turn it
/// framed ran out of `max_tokens` mid-utterance, so nothing more is coming
/// and no continuation exists to run.
///
/// Its one consumer is §8.2's Nudge gate
/// ([`nudge_enabled`](crate::actions::nudge_enabled)): linked litany reads
/// this shape as `NothingDue` and exits without creating a step, so
/// offering the control would be a control that fires and does nothing
/// (QUALITY H4's theater). The *recovery* is Message, which is offered
/// unconditionally, and the §7.3 step wound says so in words. `false`
/// under a held lock, where the question is not asked (§3.5).
pub truncated: bool,
/// **Why the latest model call failed**, in the provider's own words, or
/// `None` when it did not (bl-b43b's `refused`, widened by bl-9b88). Read
/// off the same bytes and in the same pass as
/// [`truncated`](Agent::truncated), and at rest for its reason exactly — a
/// driver holding the lease is itself the answer to "what now".
///
/// Two shapes of one fact, and until bl-9b88 only the first was read: the
/// in-band `error` event brazen speaks on stdout, and — when the adapter
/// died before reaching that contract, which is what a credential-less
/// provider row does — the step's own `stderr.log`. The second shape is the
/// one the live sighting wore: every conversation in a workspace launched a
/// driver, every model call refused, and every seat painted a conversation
/// that simply never answers.
///
/// It is the *why* riding beside the state rather than a state of its own:
/// the badge set is frozen at four (§5.1 #9), so a failed call comes to rest
/// [`Stopped`](AgentState::Stopped) like every other wound, and what
/// separates it from an operator's own `/stop` is this sentence. The §11 row
/// says its [`clause`](crate::git_tree::clause) and paints `Tone::Bad`; the
/// provider **row** to sign in to is the steps surface's `auth_row` — one
/// fact, one home, one query deeper.
pub failure: Option<String>,
/// A liveness probe could not observe (DESIGN §10): the lock probe
/// returned `Unknown`, or the writer probe did under a held lock. The
/// [`state`](Agent::state) is then the best framing-only reading, never a
/// false definite, and the renderer flags it with an uncertainty ("?")
/// suffix. Always `false` on Linux, where `/proc` is authoritative.
pub state_uncertain: bool,
/// Pending (undelivered) deposits in the agent's inbox
/// (`<workspace>/inbox/<agent-id>/*.md`, §2.11), oldest-first — the §5.1
/// #11 derivation gathered at snapshot time (§3.5 stateless re-read) so
/// its three seats (the `✉n` badge, the Inbox tab, the inbox-composer's
/// pending queue, bl-929d) read one listing and the render path never
/// touches disk. The count is this listing's length, never a second
/// stored fact.
pub pending: Vec<crate::inboxview::InboxEntry>,
/// `refs/litany/conflicted/<agent-id>` oid, or `None` when unmarked —
/// a work-product transfer was declined (§2.6). Rendered as an
/// orthogonal mark alongside the state (§3.5, §7.1); the oid is the §6
/// attention watermark evidence (rule 4).
pub conflicted_oid: Option<String>,
/// `refs/litany/budget-exhausted/<agent-id>` oid, or `None` — the
/// agent tree hit a spend ceiling (§6). Rendered alongside the state
/// (§3.5, §7.1); the oid is the §6 watermark evidence (rule 3).
pub budget_oid: Option<String>,
/// `refs/litany/abandoned/<agent-id>` oid, or `None` — the policy
/// assertion that a stopped branch will not be retried (ARCH §8). Its
/// presence suppresses the stop-attention signal (§6 rule 2).
pub abandoned_oid: Option<String>,
/// `refs/litany/notify/<agent-id>` oid, or `None` — the branch asked
/// the UI to raise a notification (ARCH §8). The oid is the §6
/// watermark evidence (rule 1: unseen = oid ≠ watermark).
pub notify_oid: Option<String>,
/// The invocation the capability control **parked** before it executed
/// (`refs/litany/held/<agent-id>`, ARCH §3.3, DESIGN §8.6): which
/// `tool_use`, which tool, and the control's reason. `None` for every
/// branch nothing is holding, which is nearly all of them.
///
/// The **value**, not an oid, and that is §6 rule 6's shape: a park is not
/// acknowledgeable — hiding a parked drone behind a watermark would hide a
/// drone that cannot move — so there is nothing to seen-gate, and what the
/// operator needs is what the blob says.
pub held: Option<crate::control::hold::Held>,
/// The newest **flag** raised on this agent (VISION §4.9, §6 rule 7,
/// bl-6f2f): when, and why in the raiser's own words. `None` for every
/// conversation nobody has flagged, which is nearly all of them.
///
/// **The one field on this type the workspace does not answer.** Every
/// other fact here is read off `<workspace>/…` by [`super::super::from_repo`];
/// a flag lives in yog's own `ops.jsonl`, and this is stamped on by
/// [`crate::monitor::flag::fold`] at the one place the ops trail and the
/// derived trees are both final. It rides *here* rather than arriving as a
/// seventh parameter because §6's predicate and its acknowledgement both
/// take `&Agent` and nothing else — that is the shape of a signal, and a
/// signal that did not fit it would have to be threaded through the rank
/// sort, both rollups, the roster walk and every caller of each.
pub flagged: Option<crate::monitor::Flag>,
/// The start-flow ball id stamped in this agent's `goal.md` (DESIGN §3.3),
/// parsed back by [`crate::start::parse_ball_stamp`] — the *derived*
/// conversation↔ball association, never stored (§3.2, §5.1: a fact whose
/// one home is the goal content). Only a root the yog start flow composed
/// carries one; a sub-agent or a hand-typed conversation reads `None`.
pub goal_ball: Option<String>,
/// The **litany-stored name fact** (DESIGN §3.3 as ruled by bl-50f3): the
/// `name` blob on this agent's own branch, committed beside `goal.md` at
/// dispatch and read back `git show agents/<id>:name` — the `agents/*`
/// refs stay the only registry, so this is a query, never a stored index.
/// The one durable home of the name since lernie 0.0.4; any agent may
/// carry it — a yog-fired root (`--name` at fire) or a litany-dispatched
/// child alike, no special case. `None` for an unnamed agent and for a
/// pre-0.0.4 branch with no blob.
pub name: Option<String>,
/// The **legacy** `You are <x>.` stamp on this agent's `goal.md` first line
/// (DESIGN §3.3), parsed back by [`crate::start::parse_identity_stamp`].
/// Demoted by bl-08f2 from the name's home to its fallback: it covers only
/// pre-0.0.4 roots (no [`name`](Agent::name) blob) until litany's 30-day
/// retention ages them out — then the rung is deleted. Not a fact home;
/// since bl-6920 nothing composes the stamp, so new roots never carry one.
pub goal_name: Option<String>,
}
impl Agent {
/// The agent's display identity, or `None` when it has none — the top of
/// the §3.3 ladder in one fold: the litany-stored [`name`](Agent::name)
/// fact, else the legacy [`goal_name`](Agent::goal_name) stamp parse.
/// Every seat that names an agent (the §11 row title and in-flight strip,
/// the center header, the §3.6 deletion gate, the mint's occupied set)
/// reads this one fold, so retiring the legacy rung is one deletion here.
/// Whether this row exists **only in memory** — §7.2's pending
/// conversation, the row a fired start paints before its driver has written
/// a branch. A derived agent comes off `git for-each-ref`, so it always has
/// a tip; an empty one cannot be derived, which makes this a query rather
/// than a flag (§5.1's discipline: no fact stored twice). Its one consumer
/// is the §11 tone — faded while a send is only yog's word for it,
/// brightening when the derivation makes it a statement.
pub fn in_memory(&self) -> bool {
self.tip_oid.is_empty()
}
/// Was the latest model call **refused at the provider rung** (bl-b43b)?
/// The auth-shaped reading of [`failure`](Agent::failure)'s sentence — a
/// query, never a second stored flag, so the fact and the reading of it
/// cannot disagree. §6's `AttentionKind::Refused` is the word it earns and
/// the remedy (sign a provider in) is what makes it worth telling apart
/// from every other failed call.
pub fn refused(&self) -> bool {
self.failure
.as_deref()
.is_some_and(crate::login::auth::looks_auth)
}
pub fn name_fact(&self) -> Option<String> {
self.name.clone().or_else(|| self.goal_name.clone())
}
/// Whether [`name_fact`](Agent::name_fact)'s answer is the **legacy
/// display-only rung** (bl-8068): a goal-stamp parse with no litany-stored
/// [`name`](Agent::name) blob behind it. Such a name renders as the title,
/// but litany resolves message targets by id or *stored* name only — a
/// peer addressing this name gets `no agent "<x>" in this workspace`. The
/// seats that show the name hover this fact so an operator never reads an
/// unaddressable name as an addressable one. `false` for a fact-named
/// agent and for one with no name claim at all.
pub fn name_display_only(&self) -> bool {
self.name.is_none() && self.goal_name.is_some()
}
}