Swiftide 0.27 - Easy human-in-the-loop flows for agentic AI

Published: at by Timon Vonk

Swiftide is a library for building agentic AI applications in Rust. Index, query, run agents, and bring your experiments right to production.

To get started with Swiftide, head over to swiftide.rs, check us out on github, or hit us up on discord.

Human-in-the-loop

Human-in-the-loop is an agentic behavior where confirmation or feedback is required before executing a tool. Swiftide now has build in support to implement this behavior.

High-over, it can work like this:

Feedback can be approval or refusal, and both the request for and the given feedback support an optional serde_json::Value.

A minified example with the existing decorator:

let guess_with_approval = ApprovalRequired::new(guess_a_number());
let mut agent = agents::Agent::builder()
.llm(&openai)
.tools(vec![guess_with_approval])
// There is also the `.on_stop` lifecycle hook which can achieve the same
.build()?;
// First query the agent, the agent will stop with a reason that feedback is required
agent
.query("Guess a number between 0 and 100 using the `guess_a_number` tool")
.await?;
// The agent stopped, lets get the tool call
let Some(StopReason::FeedbackRequired { tool_call, .. }) = agent.stop_reason().unwrap();
// Register that this tool call is OK.
agent
.context()
.feedback_received(tool_call, &ToolFeedback::approved())
.await
.unwrap();
// Run the agent again and it will pick up where it stopped.
agent.run().await.unwrap();

If you need a more custom approach, rolling your own is straightforward:

#[swiftide::tool(
description = "Guess a number",
param(name = "number", description = "Number to guess")
)]
async fn guess_a_number(
context: &dyn AgentContext,
number: usize,
) -> Result<ToolOutput, ToolError> {
let Some(feedback) = context.has_received_feedback(tool_call).await else {
return Ok(ToolOutput::FeedbackRequired(Some(json!({
"hint": "The agent would like a hint"
}))))
};
match feedback {
ToolFeedback::Approved { payload } => {
let actual_number = 42;
if number == actual_number {
Ok("You guessed it!".into())
} else {
// Format to your leisure
Ok(format!("Try again, the user provided a hint: {}", payload.to_string()).into())
}
},
ToolFeedback::Refused { payload } => {
Ok(format!("You are not guessing numbers today. Feedback: {}", payload.to_string()).into())
}
}
}

Persisted message history for agents

Technically it was already possible to implement persisted agents, either by using serde on the message history, or implementing your own AgentContext (which is involved; but fun if you like atomics).

The context of an agent is now dyn generic over its MessageHistory. If you need persistence for an agent, all you need to do this implement this trait, and provide it to the context before setting up the agent.

I’ve added support for Redis, and can be used like this:

// You can customize keys more, see the documentation
let redis = Redis::try_from_url(redis_url, "swiftide")?;
let context = DefaultContext::default()
.with_message_history(redis)
.to_owned();
// And then when building the agent
Agent::builder().context(context);

If you want to implement your own, you will need to implement the following trait:

pub trait MessageHistory: Send + Sync + std::fmt::Debug {
/// Returns the history of messages
async fn history(&self) -> Result<Vec<ChatMessage>>;
/// Add a message to the history
async fn push_owned(&self, item: ChatMessage) -> Result<()>;
/// Overwrite the history with the given items
async fn overwrite(&self, items: Vec<ChatMessage>) -> Result<()>;
}

So much more

There is a lot more in this release, some highlights:

You can find the full changelog here.


To get started with Swiftide, head over to swiftide.rs or check us out on github.