pub struct Question {Show 16 fields
pub schema: u32,
pub id: String,
pub run: String,
pub node: String,
pub seat: String,
pub summary: String,
pub detail: String,
pub choices: Vec<String>,
pub panel: bool,
pub assets: Vec<String>,
pub status: QuestionStatus,
pub asked_at: Timestamp,
pub answered_at: Option<Timestamp>,
pub answer: Option<Answer>,
pub thread: Vec<Turn>,
pub answer_timeout: u64,
}Expand description
One decision magi will not take on the owner’s behalf.
Fields§
§schema: u32On-disk format version.
id: StringQuestion id, e.g. 20260902-231501-ab12. Same shape as a run’s and a
task’s, so the operator can paste any of them at any prefix argument.
run: StringRun that is parked behind this question.
node: StringGraph node the asking agent was working in, e.g. implement.
seat: StringSeat that asked, e.g. impl-A. Recorded because “which agent needs
this” decides whether the answer unblocks one candidate or all of them.
summary: StringOne line: the question itself. This is what a notification carries and what the phone shows above the answer controls.
detail: StringThe reasoning behind the question, as markdown. May be long, may be empty. Rendered as text nodes by the UI, never as markup.
choices: Vec<String>The admissible answers. Empty means free text - that one condition
is the whole difference between the two kinds of question, on disk, in
the UI, and in Question::answer’s validation.
panel: boolDoes this question have an agent-authored HTML panel beside it?
Serialised with a default so a question written by an older magi - or
by hand - still deserialises rather than failing the whole store, which
under Questions::list’s skip-unreadable rule would quietly hide the
open question the operator was looking for.
assets: Vec<String>Files copied in beside the panel’s html, by base name, sorted.
The list exists so a reader knows what a panel is made of without
walking the directory, and every entry satisfies valid_asset_name.
Sorted because it is compared - a question re-asked with the same
assets in a different argument order is not a different question.
status: QuestionStatusCurrent state.
asked_at: TimestampWhen the agent asked.
answered_at: Option<Timestamp>When the owner answered, if they did.
answer: Option<Answer>What they said.
thread: Vec<Turn>Everything said after the question itself, oldest first: the owner
asking back, the agent replying, as many times as it takes before an
Answer lands.
#[serde(default)] so a question written before this field existed -
every question on disk before this build - still deserialises as one
with no conversation yet, rather than failing Questions::list’s
read and quietly hiding an open question from the operator.
answer_timeout: u64The answer_timeout, in seconds, that was in force when this question
was first asked. 0 means unrecorded - a question written before this
field existed, or one filed by a flow (land’s merge-approval gate)
that never sets it because it never resumes a sliced wait.
Question::new cannot know this - the effective timeout (--timeout,
or the config default) is decided by the caller, after the question
already exists - so it starts at 0 here and whoever files a fresh
question sets it once, the same way Question::panel is set by
Questions::put_panel rather than by the constructor. It is never
touched again: magi ask --wait reads it as the one deadline it is
allowed to enforce, precisely so that a --timeout given (or omitted)
on a later call can never quietly extend or shrink the budget the
question was actually asked with.
Implementations§
Source§impl Question
impl Question
Sourcepub fn new(
run: String,
node: String,
seat: String,
summary: String,
detail: String,
choices: Vec<String>,
) -> Self
pub fn new( run: String, node: String, seat: String, summary: String, detail: String, choices: Vec<String>, ) -> Self
Ask something. Persist it with Questions::put, or hand it to
ask_and_wait, which files it and waits.
Sourcepub fn short(&self) -> &str
pub fn short(&self) -> &str
Short form used in reports and on the phone, matching a run’s short id.
Sourcepub fn answer(&mut self, answer: Answer) -> Result<()>
pub fn answer(&mut self, answer: Answer) -> Result<()>
Record an answer. Rejects a choice the question does not offer, free text on a multiple-choice question, an empty answer, and a second answer.
Every rejection here is a case where accepting would put a fabrication in front of an agent as if the owner had said it. The messages are distinct because the caller is a web handler that shows them verbatim, and “that is not one of the choices” and “this question is multiple choice” are different mistakes with different fixes.
Sourcepub fn abandon(&mut self, why: impl Into<String>)
pub fn abandon(&mut self, why: impl Into<String>)
Give up on an answer, keeping the record of what was asked.
An answered question is left alone, which matters at exactly one moment: the owner answering in the same second the wait’s deadline passes. The answer is the thing worth keeping there, and it has already been written by another process.
The reason is appended to Question::detail because the on-disk shape
is a contract with the front end and has no field of its own for it -
and “asked at 3am, nobody home for a day” belongs with the question, not
only in a log the operator will never open.
Sourcepub fn resolution(&self) -> Option<String>
pub fn resolution(&self) -> Option<String>
The answer as the asking agent should read it.
One string for both kinds of question: the agent’s prompt says “the
owner answered:”, and a chosen option and a typed sentence are the same
thing at that point. None while the question is open or abandoned, so
a caller cannot mistake silence for a decision.
Sourcepub fn say(&mut self, body: impl Into<String>) -> Result<()>
pub fn say(&mut self, body: impl Into<String>) -> Result<()>
The owner speaking back without answering: a request for context, a clarifying question, anything short of a decision.
Rejects the same two states Question::answer does, and for the same
reason - a question with a recorded Answer or an abandoned one has
no run left listening for a reply - and an empty turn, which would tell
the agent nothing it didn’t already know. Never changes status: the
question stays QuestionStatus::Open, because the owner did not
decide anything, they only spoke, and count_open/open_for must keep
counting this as the one question it always was.
Sourcepub fn reply(
&mut self,
body: impl Into<String>,
choices: Vec<String>,
) -> Result<()>
pub fn reply( &mut self, body: impl Into<String>, choices: Vec<String>, ) -> Result<()>
The agent replying to the owner’s last word, in place of an answer: same question, same id, another round.
choices replaces Question::choices wholesale rather than merging,
on the same reasoning Questions::put_panel replaces a panel
wholesale: the whole point of asking back is that what should be
offered next may have changed, and a caller that wanted the old set
unchanged can simply pass it again. An empty Vec means free text,
exactly as it does when the question is first asked.
Sourcepub fn waiting_on_agent(&self) -> bool
pub fn waiting_on_agent(&self) -> bool
Is the ball in the agent’s court?
True from the moment the owner speaks back until the agent’s next
Question::reply, and never on a fresh or an already-settled
question. QuestionStatus does not move for either side of this -
see Question::say - so this is the one place that state is
readable at all, which is why [crate::web::QuestionView] carries it
separately rather than asking the phone to infer it from the thread.
Trait Implementations§
Source§impl<'de> Deserialize<'de> for Question
impl<'de> Deserialize<'de> for Question
Source§fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
impl Eq for Question
impl StructuralPartialEq for Question
Auto Trait Implementations§
impl Freeze for Question
impl RefUnwindSafe for Question
impl Send for Question
impl Sync for Question
impl Unpin for Question
impl UnsafeUnpin for Question
impl UnwindSafe for Question
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> DeserializeOwned for Twhere
T: for<'de> Deserialize<'de>,
Source§impl<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
Source§fn equivalent(&self, key: &K) -> bool
fn equivalent(&self, key: &K) -> bool
key and return true if they are equal.Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§fn in_current_span(self) -> Instrumented<Self> ⓘ
fn in_current_span(self) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more