agentmux_srv/ambient/purpose.rs
1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Every ambient purpose, in one place: its tag (the second half of an
5//! `AmbientCallKey`, the outcome-log purpose and the token-usage category), how
6//! long its CLI call may run, and which queue it waits in. Each caller keeps its OWN purpose: two callers under
7//! one purpose cancel each other.
8//!
9//! docs/reports/REPORT_AMBIENT_FRAMEWORK_REASSESSMENT_2026_10_08.md section 6.3.
10
11use std::time::Duration;
12
13use super::limits::Class;
14
15/// One kind of ambient call.
16#[derive(Debug, PartialEq, Eq)]
17pub struct Purpose {
18 pub tag: &'static str,
19 /// The CLI call's time limit, spawn to exit.
20 pub timeout: Duration,
21 /// Whether the outcome log may carry the reply's text. Off for a purpose whose
22 /// reply restates the conversation itself (the continuity state), which must
23 /// not be copied into the srv log.
24 pub logs_reply: bool,
25 /// Which queue its calls wait in (`limits`).
26 pub class: Class,
27}
28
29/// The time limits. A call that answers in about 1.5 s on a quiet account can
30/// take much longer when many agents share one login and the CLI retries on
31/// overload, and a reply cut off at the limit has been paid for and is thrown
32/// away. 15 s timed out a third of suggestion calls. A suggestion or a title
33/// that arrives after 25 s is still useful.
34const INTERACTIVE_TIMEOUT: Duration = Duration::from_secs(30);
35const BACKGROUND_TIMEOUT: Duration = Duration::from_secs(45);
36
37/// The per-turn session title the pane requests (`session:activity_summary`).
38pub const ACTIVITY_SUMMARY: Purpose =
39 Purpose { tag: "activity_summary", timeout: INTERACTIVE_TIMEOUT, class: Class::Interactive, logs_reply: true };
40
41/// A missing session title, recovered by the background sweep
42/// (`backend::reactive::activity_watcher`). Its own purpose so it never cancels,
43/// or is cancelled by, the pane's own title request.
44pub const ACTIVITY_SUMMARY_PUSHED: Purpose =
45 Purpose { tag: "activity_summary_pushed", timeout: BACKGROUND_TIMEOUT, class: Class::Background, logs_reply: true };
46
47/// The once-per-definition conversation preview for the AgentPicker's "My Agents"
48/// list (`tasks::generate_definition_activity_summary`).
49pub const DEFINITION_SUMMARY: Purpose =
50 Purpose { tag: "definition_summary", timeout: BACKGROUND_TIMEOUT, class: Class::Background, logs_reply: true };
51
52/// A subagent's display name (`tasks::generate_subagent_name`), when its parent
53/// gave no description. One-shot: the name is cached on `SubAgent.display_name`,
54/// so its generation is always `1`. The backlog pass runs it as background work.
55pub const SUBAGENT_NAME: Purpose =
56 Purpose { tag: "subagent_name", timeout: INTERACTIVE_TIMEOUT, class: Class::Interactive, logs_reply: true };
57
58/// A Workflow dispatch's display name. Separate from `SUBAGENT_NAME` so the two
59/// are counted apart. docs/specs/SPEC_SWARM_DISPATCH_NAMING_AND_ROW_MODEL_2026_07_19.md.
60pub const DISPATCH_NAME: Purpose =
61 Purpose { tag: "dispatch_name", timeout: INTERACTIVE_TIMEOUT, class: Class::Interactive, logs_reply: true };
62
63/// The composer's ghost-text next-prompt suggestion.
64/// docs/specs/SPEC_AMBIENT_GHOST_TEXT_NEXT_PROMPT_2026_07_03.md.
65pub const NEXT_PROMPT_SUGGESTION: Purpose =
66 Purpose { tag: "next_prompt_suggestion", timeout: INTERACTIVE_TIMEOUT, class: Class::Interactive, logs_reply: true };
67
68/// A line narrating an autonomous AgentMux action in the pane's conversation.
69pub const NARRATION: Purpose =
70 Purpose { tag: "ambient_narration", timeout: BACKGROUND_TIMEOUT, class: Class::Background, logs_reply: true };
71
72/// The rolling continuity state (`backend::continuity_state`): off any
73/// user-facing path, and a long multi-section reply, so it may take longer.
74pub const CONTINUITY_STATE: Purpose =
75 Purpose { tag: "continuity_state", timeout: Duration::from_secs(90), class: Class::Background, logs_reply: false };
76
77/// The note a pane shows when a fresh session continues its conversation from
78/// AgentMux's record (`backend::continued_note`): where things stand, then "How
79/// can I help?". It waits in the interactive queue, since the user is looking
80/// at the pane, but gets the longer limit: its material runs to 12k characters
81/// and a note that lands late still helps. It restates the conversation, so,
82/// like the continuity state, its text stays out of the srv log.
83pub const SESSION_CONTINUED_NOTE: Purpose =
84 Purpose { tag: "session_continued_note", timeout: BACKGROUND_TIMEOUT, class: Class::Interactive, logs_reply: false };
85
86/// Every purpose, for anything that lists them.
87#[cfg(test)]
88pub const ALL: &[&Purpose] = &[
89 &ACTIVITY_SUMMARY,
90 &ACTIVITY_SUMMARY_PUSHED,
91 &DEFINITION_SUMMARY,
92 &SUBAGENT_NAME,
93 &DISPATCH_NAME,
94 &NEXT_PROMPT_SUGGESTION,
95 &NARRATION,
96 &CONTINUITY_STATE,
97 &SESSION_CONTINUED_NOTE,
98];
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn every_purpose_has_its_own_tag() {
106 let mut tags: Vec<&str> = ALL.iter().map(|p| p.tag).collect();
107 tags.sort_unstable();
108 tags.dedup();
109 assert_eq!(tags.len(), ALL.len(), "two purposes share a tag, so they would cancel each other");
110 }
111
112 /// The continuity state and the continued-session note restate the
113 /// conversation (its goal, the latest request, exact identifiers): their
114 /// text never goes to the srv log (#4501).
115 #[test]
116 fn only_replies_that_restate_the_conversation_stay_out_of_the_log() {
117 let quiet: Vec<&str> = ALL.iter().filter(|p| !p.logs_reply).map(|p| p.tag).collect();
118 assert_eq!(quiet, vec![CONTINUITY_STATE.tag, SESSION_CONTINUED_NOTE.tag]);
119 }
120}