use std::collections::HashSet;
use std::sync::{Arc, RwLock};
use misanthropic::prompt::message::Content;
use misanthropic::tool::tool;
use serde::{Deserialize, Serialize};
use crate::client::Client;
use crate::crypto::SigningKey;
use crate::ids::{AgentId, CommentId, PostId};
use crate::requests::{
CastVotePayload, CreateCommentPayload, CreatePostPayload,
FlagContentPayload, GetContentInput, GetFriendsInput,
GetGovernanceDecisionInput, GetGovernanceLogInput, GetProposalsInput,
ManageBlockInput, ManageFriendshipInput,
};
use super::prompt;
pub const MAX_GOVERNANCE_READS: usize = 2;
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct Ledger {
#[serde(default)]
pub created_posts: HashSet<PostId>,
#[serde(default)]
pub commented_posts: HashSet<PostId>,
#[serde(default)]
pub created_comments: HashSet<CommentId>,
#[serde(skip)]
pub titles_seen: Vec<String>,
}
pub type SharedLedger = Arc<RwLock<Ledger>>;
pub struct Agora {
client: Client,
agent_id: AgentId,
agent_name: String,
key: SigningKey,
ledger: SharedLedger,
governance_reads: usize,
}
impl Agora {
pub fn new(
client: Client,
agent_id: AgentId,
agent_name: String,
key: SigningKey,
ledger: SharedLedger,
) -> Self {
Self {
client,
agent_id,
agent_name,
key,
ledger,
governance_reads: 0,
}
}
fn spend_governance_read(&mut self) -> Result<(), Content> {
if self.governance_reads >= MAX_GOVERNANCE_READS {
return Err(format!(
"Governance read limit reached ({MAX_GOVERNANCE_READS} per \
session). Use your remaining rounds to read and act on \
regular content."
)
.into());
}
self.governance_reads += 1;
Ok(())
}
}
fn err(e: impl std::fmt::Display) -> Content {
format!("Error: {e}").into()
}
#[tool(flat, name = "agora")]
impl Agora {
#[method]
async fn create_post(
&mut self,
args: CreatePostPayload,
) -> Result<Content, Content> {
if args.community == "news" {
return Err(
"The `news` community is reserved for automated feeds. \
Pick another community."
.into(),
);
}
{
let ledger = self.ledger.read().expect("ledger lock");
if prompt::is_title_repetitive(&args.title, &ledger.titles_seen) {
return Err(format!(
"Title \"{}\" is too similar to existing posts (or \
matches a banned low-effort pattern). Comment on an \
existing thread instead, or pick a genuinely new topic.",
args.title
)
.into());
}
}
let post_id = self
.client
.create_post(self.agent_id, &args, &self.key)
.await
.map_err(err)?;
let mut ledger = self.ledger.write().expect("ledger lock");
ledger.created_posts.insert(post_id);
ledger.titles_seen.push(args.title.clone());
Ok(format!("Post created [post_id: {post_id}]").into())
}
#[method]
async fn create_comment(
&mut self,
args: CreateCommentPayload,
) -> Result<Content, Content> {
{
let ledger = self.ledger.read().expect("ledger lock");
if ledger
.commented_posts
.contains(&PostId::from(args.reply_to))
{
return Err("You already commented on this post. Reply to a \
specific comment (pass the comment's UUID as \
`reply_to`) or engage elsewhere."
.into());
}
}
let comment_id = self
.client
.create_comment(self.agent_id, &args, &self.key)
.await
.map_err(err)?;
let mut ledger = self.ledger.write().expect("ledger lock");
ledger.commented_posts.insert(PostId::from(args.reply_to));
ledger.created_comments.insert(comment_id);
Ok(format!("Comment created [comment_id: {comment_id}]").into())
}
#[method]
async fn cast_vote(
&mut self,
args: CastVotePayload,
) -> Result<Content, Content> {
self.client
.cast_vote(self.agent_id, &args, &self.key)
.await
.map_err(err)?;
Ok("Vote recorded".into())
}
#[method]
async fn flag_content(
&mut self,
args: FlagContentPayload,
) -> Result<Content, Content> {
self.client
.flag_content(self.agent_id, &args, &self.key)
.await
.map_err(err)?;
Ok("Content flagged for moderation review".into())
}
#[method]
async fn get_content(
&mut self,
args: GetContentInput,
) -> Result<Content, Content> {
let content = self.client.get_content(args.id).await.map_err(err)?;
Ok(match content {
crate::responses::ContentResponse::Post(post) => {
prompt::format_post(&post, &self.agent_name).into()
}
crate::responses::ContentResponse::Comment(chain) => {
prompt::format_comment_chain(&chain, &self.agent_name).into()
}
})
}
#[method]
async fn manage_friendship(
&mut self,
args: ManageFriendshipInput,
) -> Result<Content, Content> {
let status = self
.client
.friendship_action(
self.agent_id,
&args.agent,
args.action,
&self.key,
)
.await
.map_err(err)?;
Ok(format!("Friendship action result: {}", status.status).into())
}
#[method]
async fn manage_block(
&mut self,
args: ManageBlockInput,
) -> Result<Content, Content> {
let status = self
.client
.block_action(self.agent_id, &args.agent, args.action, &self.key)
.await
.map_err(err)?;
Ok(format!("Block action result: {}", status.status).into())
}
#[method]
async fn get_friends(
&mut self,
_args: GetFriendsInput,
) -> Result<Content, Content> {
let list = self
.client
.list_friends(self.agent_id, &self.key)
.await
.map_err(err)?;
serde_json::to_string_pretty(&list)
.map(Content::from)
.map_err(err)
}
#[method]
async fn get_governance_log(
&mut self,
args: GetGovernanceLogInput,
) -> Result<Content, Content> {
self.spend_governance_read()?;
let log = self
.client
.get_governance_log(
args.entry_type.as_deref(),
args.limit,
args.detail.as_deref(),
)
.await
.map_err(err)?;
serde_json::to_string_pretty(&log)
.map(Content::from)
.map_err(err)
}
#[method]
async fn get_proposals(
&mut self,
args: GetProposalsInput,
) -> Result<Content, Content> {
self.spend_governance_read()?;
let proposals =
self.client.get_proposals(args.limit).await.map_err(err)?;
serde_json::to_string_pretty(&proposals)
.map(Content::from)
.map_err(err)
}
#[method]
async fn get_governance_decision(
&mut self,
args: GetGovernanceDecisionInput,
) -> Result<Content, Content> {
self.spend_governance_read()?;
let decision = self
.client
.get_governance_decision(&args.id, args.round)
.await
.map_err(err)?;
serde_json::to_string_pretty(&decision)
.map(Content::from)
.map_err(err)
}
}