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
250
251
252
253
254
255
256
257
258
259
// Task-centric domain structs for aid task storage and display.
// Exports: Task, Workgroup, Finding, TaskEvent, TaskFilter, CompletionInfo.
// Deps: chrono, serde, and parent `crate::types` enums/IDs.
use chrono::{DateTime, Local};
use serde::Serialize;
use super::{
provider_for_cli, AgentKind, AttributionSource, DeliveryAssessment, EventKind, Route, TaskId,
TaskOutcome, TaskStatus, VerifyStatus, WorkgroupId,
};
#[derive(Debug, Clone, Serialize)]
pub struct Task {
pub id: TaskId,
pub agent: AgentKind,
pub custom_agent_name: Option<String>,
pub prompt: String,
pub resolved_prompt: Option<String>,
pub category: Option<String>,
pub status: TaskStatus,
pub parent_task_id: Option<String>,
pub workgroup_id: Option<String>,
pub caller_kind: Option<String>,
pub caller_session_id: Option<String>,
pub agent_session_id: Option<String>,
pub repo_path: Option<String>,
/// Stable project identity recorded at dispatch. `None` is the explicit
/// unattributed bucket — never invent a project for historical rows.
pub project_id: Option<String>,
pub worktree_path: Option<String>,
pub worktree_branch: Option<String>,
pub final_head_sha: Option<String>,
pub final_branch: Option<String>,
pub start_sha: Option<String>,
pub log_path: Option<String>,
pub output_path: Option<String>,
pub tokens: Option<i64>,
pub prompt_tokens: Option<i64>,
pub duration_ms: Option<i64>,
/// The model aid dispatched with — a request, not an outcome. Set at
/// dispatch from `--model`, the configured default, budget mode or smart
/// routing, and kept as aid passed it even when the CLI refused to serve
/// it: `t-bd455a68` asked the `claude` CLI for `gemini-3.6-flash-low` and
/// failed, and the request is still the honest record of what was asked.
pub requested_model: Option<String>,
/// The model the CLI reported it actually ran. `None` means the CLI never
/// said so — which is not the same as the requested model having run.
/// Collapsing the two is what stored a cursor model on an `agy` task
/// (`t-702f7bcb`) and `auto`, a router, as though it were a model.
///
/// Cost, capability history and model-level routing must read this, never
/// `requested_model`. Per-family quota marking is the one legitimate reader
/// of the request: it asks which family aid *aimed at*, and plain-text CLIs
/// such as agy never echo a model at all.
pub observed_model: Option<String>,
/// How `observed_model` was established. Always `None` when
/// `observed_model` is `None` — the two move together.
pub attribution_source: Option<AttributionSource>,
pub cost_usd: Option<f64>,
pub exit_code: Option<i32>,
pub created_at: DateTime<Local>,
pub completed_at: Option<DateTime<Local>>,
pub verify: Option<String>,
pub verify_status: VerifyStatus,
pub pending_reason: Option<String>,
pub read_only: bool,
pub budget: bool,
pub audit_verdict: Option<String>,
pub audit_report_path: Option<String>,
pub delivery_assessment: Option<DeliveryAssessment>,
}
impl Task {
pub fn outcome(&self) -> TaskOutcome {
TaskOutcome::derive(
self.status,
self.verify_status,
super::verify_required(self.verify.as_deref()),
)
.with_delivery_assessment(self.delivery_assessment)
}
/// The model an outcome may be attributed to: what the CLI reported, never
/// what aid asked for. `None` means nobody knows, and it must stay unknown
/// — capability history, per-model success rates and model-level routing
/// read this, and a guessed model there poisons the advice built on it.
pub fn attributed_model(&self) -> Option<&str> {
self.observed_model.as_deref()
}
/// The model, but only when the CLI itself said so. Capability scoring and
/// an agent's learned default model read this rather than
/// `attributed_model`, because a model inferred from a run merely not
/// failing is not evidence that model performed well — or even that a
/// substitution did not happen behind a successful exit.
pub fn conclusive_model(&self) -> Option<&str> {
self.attribution_source
.filter(|source| source.is_conclusive())
.and(self.observed_model.as_deref())
}
/// The model to price against, and the model a derived dispatch should ask
/// for again: the observation when there is one, otherwise the original
/// request. The fallback is legitimate here only because both values are
/// stored, so a reader can see which basis was used.
pub fn costing_model(&self) -> Option<&str> {
self.observed_model
.as_deref()
.or(self.requested_model.as_deref())
}
/// What to show a human. A request that was never confirmed is marked, and
/// an observation that contradicts the request is shown as both — that
/// disagreement means the CLI served something other than what was asked.
pub fn display_model(&self) -> Option<String> {
format_model_display(
self.observed_model.as_deref(),
self.requested_model.as_deref(),
self.attribution_source,
)
}
/// The structured route for this task. Model is the observation only — a
/// request that was never confirmed stays `None` rather than being filled in.
pub fn route(&self) -> Route {
Route::for_cli(self.agent).with_model(self.attributed_model())
}
/// Human route label: `cli/provider/model` with attribution on the model
/// segment. An unknown model stays `unknown` — never a default or the CLI
/// name. Provider comes from `provider_for_cli`, the single source.
pub fn display_route(&self) -> String {
let (provider, _) = provider_for_cli(self.agent);
let model = self
.display_model()
.unwrap_or_else(|| "unknown".to_string());
format!(
"{}/{}/{}",
self.agent_display_name(),
provider.as_str(),
model
)
}
pub fn agent_display_name(&self) -> &str {
if self.agent == AgentKind::Custom {
self.custom_agent_name.as_deref().unwrap_or("custom")
} else {
self.agent.as_str()
}
}
pub fn delivery_assessment(&self) -> Option<DeliveryAssessment> {
self.delivery_assessment
}
pub fn has_verify_failure(&self) -> bool {
self.verify_status == VerifyStatus::Failed
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Workgroup {
pub id: WorkgroupId,
pub name: String,
pub shared_context: String,
pub created_by: Option<String>,
pub created_at: DateTime<Local>,
pub updated_at: DateTime<Local>,
}
#[derive(Debug, Clone, Serialize)]
pub struct Finding {
pub id: i64,
pub workgroup_id: String,
pub content: String,
pub source_task_id: Option<String>,
pub severity: Option<String>,
pub title: Option<String>,
pub file: Option<String>,
pub lines: Option<String>,
pub category: Option<String>,
pub confidence: Option<String>,
pub verdict: Option<String>,
pub score: Option<String>,
pub note: Option<String>,
pub created_at: DateTime<Local>,
pub updated_at: Option<DateTime<Local>>,
}
#[derive(Debug, Clone, Serialize)]
pub struct TaskEvent {
pub task_id: TaskId,
pub timestamp: DateTime<Local>,
pub event_kind: EventKind,
pub detail: String,
pub metadata: Option<serde_json::Value>,
}
impl TaskEvent {
/// Untruncated detail: parsers stash text over the display cap in
/// metadata under `"full"`.
pub fn full_detail(&self) -> &str {
self.metadata
.as_ref()
.and_then(|meta| meta.get("full"))
.and_then(|value| value.as_str())
.unwrap_or(&self.detail)
}
}
#[derive(Debug, Clone, Copy)]
pub enum TaskFilter {
All,
Active,
Running,
Today,
}
#[derive(Debug, Clone)]
pub struct CompletionInfo {
pub tokens: Option<i64>,
pub status: TaskStatus,
/// The model the CLI named in its own output, if it named one at all. An
/// adapter must never put the dispatched request here.
pub model: Option<String>,
pub cost_usd: Option<f64>,
pub exit_code: Option<i32>,
}
/// Renders model attribution for humans. An unconfirmed request is marked with
/// `?` so a reader can tell a guess from an observation at a glance, and a
/// disagreement is shown in full because it means the CLI served something
/// other than what was asked for.
pub fn format_model_display(
observed: Option<&str>,
requested: Option<&str>,
source: Option<AttributionSource>,
) -> Option<String> {
match (observed, requested) {
(Some(observed), Some(requested)) if observed != requested => {
Some(format!("{observed} (asked {requested})"))
}
// Inferred from the run not failing rather than from the CLI saying so.
// Rendering it identically to an echo would hide the weaker evidence,
// which is the whole reason the grade is stored.
(Some(observed), _) if source == Some(AttributionSource::ConfirmedBySuccess) => {
Some(format!("{observed} (inferred)"))
}
(Some(observed), _) => Some(observed.to_string()),
(None, Some(requested)) => Some(format!("{requested}?")),
(None, None) => None,
}
}
#[cfg(test)]
#[path = "task_display_tests.rs"]
mod tests;