Synced from monorepo
Changes: - Stop hooks for session lifecycle - Add x.ai/session/state and x.ai/session/import ACP methods - Deny-and-continue for auto-mode classifier blocks with denial limits - Drop codebase-upload from dhat soak test - scheduler_create upsert via task_id; retire one-shot tasks - Clipboard: copy file fallback + honest toasts for SSH/Apple Terminal - Polarity-safe syntax colors in minimal mode - Auto mode classifies unvetted env prefixes instead of hard-prompting - Add GROK_CLIPBOARD_NO_OSC52 kill switch to force OSC 52 off
This commit is contained in:
parent
7cfcb20d2b
commit
ba76b0a683
143 changed files with 9465 additions and 3419 deletions
|
|
@ -368,6 +368,9 @@ impl ChatStateActor {
|
|||
ChatStateCommand::GetLastAssistantText { reply } => {
|
||||
let _ = reply.send(self.get_last_assistant_text());
|
||||
}
|
||||
ChatStateCommand::GetLastAssistantTextInTurn { reply } => {
|
||||
let _ = reply.send(self.get_last_assistant_text_in_turn());
|
||||
}
|
||||
ChatStateCommand::GetFirstUserText { reply } => {
|
||||
let _ = reply.send(self.get_first_user_text());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -151,6 +151,37 @@ impl ChatStateActor {
|
|||
})
|
||||
}
|
||||
|
||||
/// Return the current turn's last assistant message with non-empty text, or
|
||||
/// `None` when the turn produced none.
|
||||
///
|
||||
/// Like [`Self::get_last_assistant_text`], but the backwards walk stops at the
|
||||
/// turn boundary (a user item with `prompt_index` set, a genuine user message,
|
||||
/// or a synthetic reason with [`SyntheticReason::starts_prompt_turn`]); mid-turn
|
||||
/// synthetic injections are walked past.
|
||||
///
|
||||
/// [`SyntheticReason::starts_prompt_turn`]: xai_grok_sampling_types::SyntheticReason::starts_prompt_turn
|
||||
pub(super) fn get_last_assistant_text_in_turn(&self) -> Option<String> {
|
||||
for item in self.state.conversation.iter().rev() {
|
||||
match item {
|
||||
xai_grok_sampling_types::ConversationItem::Assistant(a)
|
||||
if !a.content.trim().is_empty() =>
|
||||
{
|
||||
return Some(a.content.as_ref().to_owned());
|
||||
}
|
||||
xai_grok_sampling_types::ConversationItem::User(u)
|
||||
if u.prompt_index.is_some()
|
||||
|| u.synthetic_reason
|
||||
.as_ref()
|
||||
.is_none_or(|r| r.starts_prompt_turn()) =>
|
||||
{
|
||||
return None;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Return the text of the **first content part** of the first `User` message,
|
||||
/// if and only if that part is `ContentPart::Text`.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -2716,6 +2716,43 @@ async fn get_last_assistant_text_skips_whitespace_only() {
|
|||
assert_eq!(text.as_deref(), Some("real answer"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_last_assistant_text_in_turn_stops_at_boundary() {
|
||||
let h = TestHarness::new();
|
||||
h.handle.push_user_message(ConversationItem::user("q1"));
|
||||
h.handle
|
||||
.push_assistant_response(ConversationItem::assistant("previous turn answer"));
|
||||
h.handle.push_user_message(ConversationItem::user("q2"));
|
||||
|
||||
assert!(h.handle.get_last_assistant_text_in_turn().await.is_none());
|
||||
assert_eq!(
|
||||
h.handle.get_last_assistant_text().await.as_deref(),
|
||||
Some("previous turn answer"),
|
||||
"the unbounded sibling still sees prior turns"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_last_assistant_text_in_turn_walks_past_synthetic_injections() {
|
||||
let h = TestHarness::new();
|
||||
h.handle.push_user_message(ConversationItem::user("q"));
|
||||
h.handle
|
||||
.push_assistant_response(ConversationItem::assistant("turn answer"));
|
||||
h.handle
|
||||
.push_user_message(ConversationItem::stop_hook_feedback("keep working"));
|
||||
|
||||
assert_eq!(
|
||||
h.handle.get_last_assistant_text_in_turn().await.as_deref(),
|
||||
Some("turn answer"),
|
||||
"synthetic mid-turn items must not act as turn boundaries"
|
||||
);
|
||||
|
||||
// A turn-starting synthetic item (auto-wake) IS a boundary.
|
||||
h.handle
|
||||
.push_user_message(ConversationItem::task_completed("task done"));
|
||||
assert!(h.handle.get_last_assistant_text_in_turn().await.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_last_assistant_text_no_assistant_messages() {
|
||||
let h = TestHarness::new();
|
||||
|
|
|
|||
|
|
@ -297,6 +297,13 @@ pub enum ChatStateCommand {
|
|||
reply: oneshot::Sender<Option<String>>,
|
||||
},
|
||||
|
||||
/// Like `GetLastAssistantText`, but bounded to the current prompt turn:
|
||||
/// returns `None` when the turn produced no assistant text (the walk stops
|
||||
/// at the first turn-starting user item).
|
||||
GetLastAssistantTextInTurn {
|
||||
reply: oneshot::Sender<Option<String>>,
|
||||
},
|
||||
|
||||
/// Get the text of the first `Text` content part in the first `User` message.
|
||||
/// Returns `None` if the conversation has no user messages or the first user
|
||||
/// message has no text content part.
|
||||
|
|
@ -461,6 +468,9 @@ mod tests {
|
|||
let (tx, _rx) = oneshot::channel();
|
||||
let _ = ChatStateCommand::GetLastAssistantText { reply: tx };
|
||||
|
||||
let (tx, _rx) = oneshot::channel();
|
||||
let _ = ChatStateCommand::GetLastAssistantTextInTurn { reply: tx };
|
||||
|
||||
let (tx, _rx) = oneshot::channel();
|
||||
let _ = ChatStateCommand::GetFirstUserText { reply: tx };
|
||||
|
||||
|
|
|
|||
|
|
@ -556,6 +556,20 @@ impl ChatStateHandle {
|
|||
.flatten()
|
||||
}
|
||||
|
||||
/// Get the current turn's last assistant message text, or `None` when the
|
||||
/// turn produced none (or the actor is dead). Turn-scoped, unlike
|
||||
/// [`get_last_assistant_text`], and cheaper than [`get_conversation`].
|
||||
///
|
||||
/// [`get_conversation`]: Self::get_conversation
|
||||
/// [`get_last_assistant_text`]: Self::get_last_assistant_text
|
||||
pub async fn get_last_assistant_text_in_turn(&self) -> Option<String> {
|
||||
self.query("GetLastAssistantTextInTurn", |reply| {
|
||||
ChatStateCommand::GetLastAssistantTextInTurn { reply }
|
||||
})
|
||||
.await
|
||||
.flatten()
|
||||
}
|
||||
|
||||
/// Get the text of the first `Text` content part in the first `User` message.
|
||||
///
|
||||
/// Returns `None` if no user message with text content exists or the actor
|
||||
|
|
|
|||
Loading…
Reference in a new issue