Include attributed previews in agent message board notifications (#47075)

## Why

Message board notifications exposed only post and thread IDs, requiring a read to see any post content.

## What changed

Deliver notifications as `agent_message` items with author and recipient attribution. Include a `CHANNEL_POST` payload with the sender, channel, message and thread IDs, and up to 150 characters of post text. Direct agents to `read_post` when the preview is truncated, and retain recognition of legacy user-role notices.

## Testing

Update the model-context scenario to assert the notification's author, recipient, and preview payload, and update the idle-agent scenario to check for the new notification format.

GitOrigin-RevId: e0e2794be82e38bba47f41fb4fc8128eb5e47619
This commit is contained in:
jif
2026-09-21 17:29:13 +00:00
committed by copyberry
parent f3037cafd3
commit e51aacad60
8 changed files with 80 additions and 42 deletions
+21 -9
View File
@@ -15,7 +15,7 @@ use codex_agent_message_board_extension::AgentMessageBoard;
use codex_agent_message_board_extension::LocalAgentMessageBoard;
use codex_agent_message_board_extension::MessageBoardHost;
use codex_agent_message_board_extension::NotificationDelivery;
use codex_agent_message_board_extension::PostMetadata;
use codex_agent_message_board_extension::PostPreview;
use codex_extension_api::ExtensionRegistryBuilder;
use codex_features::Feature;
use codex_protocol::AgentPath;
@@ -24,6 +24,7 @@ use codex_protocol::ThreadId;
use codex_protocol::error::CodexErr;
use codex_protocol::error::CodexErrorDetails;
use codex_protocol::error::Result;
use codex_protocol::protocol::InterAgentCommunication;
use futures::future::BoxFuture;
use std::sync::Arc;
use std::sync::Weak;
@@ -141,14 +142,14 @@ impl MessageBoardHost for LocalBoardHost {
fn notify(
&self,
recipient: ThreadId,
post: PostMetadata,
recipient_id: ThreadId,
post: PostPreview,
) -> BoxFuture<'_, Result<NotificationDelivery>> {
Box::pin(async move {
let Some(manager) = self.manager.upgrade() else {
return Ok(NotificationDelivery::SkippedInactive);
};
let recipient = match manager.get_thread(recipient).await {
let recipient = match manager.get_thread(recipient_id).await {
Ok(thread) => thread,
Err(error) if matches!(error.details(), CodexErrorDetails::ThreadNotFound(_)) => {
return Ok(NotificationDelivery::SkippedInactive);
@@ -160,13 +161,24 @@ impl MessageBoardHost for LocalBoardHost {
"notification recipient belongs to another board".into(),
));
}
let notice = AgentMessageBoardNotification {
message_id: post.message_id,
thread_id: post.thread_id,
};
let recipient_path = recipient
.session
.services
.agent_control
.ensure_agent_known(recipient_id)?
.agent_path
.ok_or_else(|| CodexErr::InvalidRequest("agent has no tree path".into()))?;
let notice = AgentMessageBoardNotification(post);
let communication = InterAgentCommunication::new(
notice.0.metadata.author.clone(),
recipient_path,
Vec::new(),
notice.render(),
/*trigger_turn*/ false,
);
Ok(
match recipient
.inject_if_running(vec![ContextualUserFragment::into(notice)])
.inject_if_running(vec![communication.to_model_input_item()])
.await
{
Ok(()) => NotificationDelivery::Accepted,
@@ -1,34 +1,42 @@
//! Fixed-size discussion notices. Post text is fetched through bounded tools.
//! Attributed discussion notices with bounded previews.
use super::ContextualUserFragment;
use codex_agent_message_board_extension::PostPreview;
use codex_protocol::models::ContentItemKind;
use uuid::Uuid;
pub(crate) struct AgentMessageBoardNotification {
pub(crate) message_id: Uuid,
pub(crate) thread_id: Uuid,
}
pub(crate) struct AgentMessageBoardNotification(pub(crate) PostPreview);
impl ContextualUserFragment for AgentMessageBoardNotification {
fn role(&self) -> &'static str {
"user"
"assistant"
}
fn content_kind(&self) -> ContentItemKind {
ContentItemKind("agent_message_board.notification".into())
}
fn markers(&self) -> (&'static str, &'static str) {
Self::type_markers()
("", "")
}
fn type_markers() -> (&'static str, &'static str) {
// Keep recognizing user-role notices in rollouts written before this format.
(
"<agent_message_board_notification>",
"</agent_message_board_notification>",
)
}
fn body(&self) -> String {
let post = &self.0;
let suffix = if post.truncated {
"\n[Use read_post for the rest.]"
} else {
""
};
format!(
"\nNew post {} in discussion {}. Use read_post or read_thread to read it.\n",
self.message_id, self.thread_id
"Message Type: CHANNEL_POST\nSender: {}\nChannel: {}\nMessage ID: {}\nThread ID: {}\nPayload:\n{}{suffix}",
post.metadata.author,
post.metadata.channel_name,
post.metadata.message_id,
post.metadata.thread_id,
post.text_preview,
)
}
}