anyagent 0.0.3

One Rust interface to the coding agents installed on a machine.
Documentation
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
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
//! Everything that flows out of a session: events, tools, requests, answers,
//! usage, and the stop reasons that end a turn.

use std::collections::BTreeMap;
use std::path::PathBuf;
use std::time::SystemTime;

use serde::{Deserialize, Serialize};

use crate::agent::string_id;
use crate::session::{SessionInfo, SessionStatus};

string_id!(SessionId);
string_id!(TurnId);
string_id!(PromptId);
string_id!(MessageId);
string_id!(ToolId);
string_id!(RequestId);
string_id!(QuestionId);
string_id!(ChoiceId);

/// Namespaced provider data Anyagent has not normalized yet. Keys use
/// `provider/name`, for example `codex/thread_status`.
pub type Extensions = BTreeMap<String, serde_json::Value>;

/// One normalized event produced by anyagent.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct Event {
    /// Starts at 1 and increases for every session event.
    pub sequence: u64,
    /// When the engine produced this event — wall-clock, so transcripts can
    /// be stored and merged without the app stamping receive time. Order
    /// across events is guaranteed by `sequence`, not by this clock. A
    /// transcript stored before this field existed reads as the epoch.
    #[serde(default = "unix_epoch")]
    pub occurred_at: SystemTime,
    pub session_id: SessionId,
    /// Present when the event belongs to a turn.
    pub turn_info: Option<TurnContext>,
    pub kind: EventKind,
    /// Provider-specific data that has not been normalized.
    pub extensions: Extensions,
}

/// The `occurred_at` of a transcript older than the field.
fn unix_epoch() -> SystemTime {
    SystemTime::UNIX_EPOCH
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct TurnContext {
    pub id: TurnId,
    /// Present for events produced by a subagent spawned from this tool call.
    pub parent_tool_id: Option<ToolId>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub enum EventKind {
    TurnStarted {
        origin: TurnOrigin,
    },
    TextDelta {
        message_id: MessageId,
        text: String,
    },
    ReasoningDelta {
        message_id: MessageId,
        text: String,
    },
    /// Provider-originated user-role content, e.g. a parent agent steering
    /// a subagent. Never a replay of the caller's own prompt.
    UserMessage {
        message_id: MessageId,
        text: String,
    },
    /// No more deltas will arrive for this message.
    MessageEnded {
        message_id: MessageId,
    },
    /// Cumulative snapshot of one tool call: start, each change, completion.
    ToolUpdated(ToolUpdate),
    /// Streamed output of a running command tool.
    ToolOutputDelta {
        tool_id: ToolId,
        text: String,
    },
    /// The agent's full current task list; replaces the previous one.
    PlanUpdated {
        entries: Vec<PlanEntry>,
    },
    RequestOpened(Request),
    RequestClosed {
        request_id: RequestId,
    },
    SessionUpdated(SessionInfo),
    /// The session's UI state flipped: working, needing input, or idle.
    /// Emitted only on change; `session.status()` answers without the stream.
    StatusChanged(SessionStatus),
    /// Context-window occupancy for this session.
    ContextUsage {
        used_tokens: u64,
        window_tokens: Option<u64>,
        cost_usd: Option<f64>,
    },
    /// The agent compacted its context; the next `ContextUsage` drops.
    ContextCompacted,
    /// Plan quota for the account, pushed by the adapter.
    PlanUsageUpdated(PlanUsage),
    Diagnostic(Diagnostic),
    TurnEnded {
        stop: StopReason,
        background: Vec<ToolId>, // still running after the turn ended
    },
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub enum TurnOrigin {
    Prompt(PromptId),
    Agent,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub enum StopReason {
    Completed { source: CompletionSource },
    Cancelled,
    Refused,
    Failed { message: String },
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub enum CompletionSource {
    /// The provider protocol explicitly ended the turn.
    Protocol,
    /// Anyagent inferred completion after the provider went quiet.
    Inferred,
}

// ---------------------------------------------------------------------------
// Tools and plans
// ---------------------------------------------------------------------------

/// Cumulative snapshot of one tool call.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct ToolUpdate {
    pub id: ToolId,
    pub kind: ToolKind,
    pub title: String,
    pub status: ToolStatus,
    pub input: ToolInput,
    pub output: Option<String>,
    pub diffs: Vec<FileDiff>,
    pub locations: Vec<PathBuf>,
    /// Agent's own tool name and raw input, for unknown or MCP tools.
    pub raw: Option<RawTool>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub enum ToolKind {
    Read,
    Edit,
    Delete,
    Move,
    Search,
    Execute,
    Fetch,
    Think,
    Mcp {
        server: String,
        tool: String,
    },
    /// Spawned a subagent; nested events carry this id in `parent_tool_id`.
    Subagent,
    Other,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub enum ToolStatus {
    Pending,
    Running,
    Completed,
    Failed,
    Cancelled,
}

impl ToolStatus {
    /// Still occupying the agent: the turn is not over while this is true.
    pub fn is_active(self) -> bool {
        matches!(self, ToolStatus::Pending | ToolStatus::Running)
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub enum ToolInput {
    Path(PathBuf),
    Command {
        command: String,
        cwd: Option<PathBuf>,
    },
    Pattern(String),
    Url(String),
    Query(String),
    Text(String),
    None,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct RawTool {
    pub name: String,
    pub input: serde_json::Value,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct FileDiff {
    pub path: PathBuf,
    /// `None` means a new file.
    pub old_text: Option<String>,
    pub new_text: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct PlanEntry {
    pub text: String,
    pub status: PlanStatus,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub enum PlanStatus {
    Pending,
    InProgress,
    Completed,
}

// ---------------------------------------------------------------------------
// Requests and answers
// ---------------------------------------------------------------------------

/// Something the agent is waiting on the caller for. Answer once with
/// `Session::answer`.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
// Public request records stay by value; boxing would move allocation to callers.
#[allow(clippy::large_enum_variant)]
pub enum Request {
    Permission(PermissionRequest),
    Question(QuestionRequest),
}

impl Request {
    /// The id to answer with.
    pub fn id(&self) -> RequestId {
        match self {
            Request::Permission(r) => r.id.clone(),
            Request::Question(r) => r.id.clone(),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct PermissionRequest {
    pub id: RequestId,
    /// The tool call awaiting approval, as the app already saw it.
    pub tool: ToolUpdate,
    /// What the agent offers; answers outside this list are rejected.
    pub options: Vec<PermissionChoice>,
    pub detail: Option<String>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub enum PermissionChoice {
    AllowOnce,
    AllowAlways,
    DenyOnce,
    DenyAlways,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct QuestionRequest {
    pub id: RequestId,
    pub questions: Vec<Question>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct Question {
    pub id: QuestionId,
    pub text: String,
    pub header: Option<String>,
    /// Empty means free text only.
    pub choices: Vec<Choice>,
    pub multi_select: bool,
    pub allows_free_text: bool,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct Choice {
    pub id: ChoiceId,
    pub label: String,
    pub description: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub enum Answer {
    Permission(PermissionChoice),
    /// One entry per question, in order.
    Question(Vec<QuestionAnswer>),
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum QuestionAnswer {
    Choices(Vec<ChoiceId>),
    Text(String),
}

// ---------------------------------------------------------------------------
// Usage and diagnostics
// ---------------------------------------------------------------------------

/// Plan quota windows for the logged-in account.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct PlanUsage {
    /// The plan the quota belongs to ("max", "edu"), when the agent names it.
    pub plan: Option<String>,
    pub windows: Vec<UsageWindow>,
    pub fetched_at: SystemTime,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct UsageWindow {
    /// "Session" (5h), "Week", or an agent-provided label.
    pub label: String,
    pub used_percent: u8,
    pub resets_at: Option<SystemTime>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct Diagnostic {
    pub level: DiagnosticLevel,
    pub message: String,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub enum DiagnosticLevel {
    Info,
    Warning,
    Error,
}

/// Immediate result of submitting a prompt.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct Delivery {
    /// Stable across immediate delivery and later queue promotion.
    pub prompt_id: PromptId,
    pub kind: DeliveryKind,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub enum DeliveryKind {
    Started {
        turn_id: TurnId,
    },
    Steered {
        turn_id: TurnId,
    },
    Queued {
        /// Number of prompts ahead of this one.
        position: u32,
    },
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn an_event_stored_without_occurred_at_still_loads() {
        let event = Event {
            sequence: 1,
            occurred_at: SystemTime::UNIX_EPOCH,
            session_id: SessionId::new("s1"),
            turn_info: None,
            kind: EventKind::TurnStarted {
                origin: TurnOrigin::Agent,
            },
            extensions: Extensions::new(),
        };
        let mut stored = serde_json::to_value(&event).unwrap();
        stored.as_object_mut().unwrap().remove("occurred_at");
        assert_eq!(serde_json::from_value::<Event>(stored).unwrap(), event);
    }
}