Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/aios-protocol/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ pub use ports::{
ApprovalPort, ApprovalRequest, ApprovalResolution, ApprovalTicket, EventRecordStream,
EventStorePort, ModelCompletion, ModelCompletionRequest, ModelDirective, ModelProviderPort,
ModelStopReason, PolicyGateDecision, PolicyGatePort, ToolExecutionReport, ToolExecutionRequest,
ToolHarnessPort,
ToolHarnessPort, ConversationTurn,
};
pub use sandbox::{NetworkPolicy, SandboxLimits, SandboxTier};
pub use session::{
Expand Down
11 changes: 11 additions & 0 deletions crates/aios-protocol/src/ports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ pub struct ModelCompletionRequest {
/// Tool whitelist from active skill. When set, only these tools are sent to the LLM.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allowed_tools: Option<Vec<String>>,
/// Conversation history from prior turns in this session.
/// Built by the runtime from the event journal before each provider call.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub conversation_history: Vec<ConversationTurn>,
}

/// A single turn in the conversation history (user message + assistant response).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConversationTurn {
pub role: String,
pub content: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand Down
96 changes: 96 additions & 0 deletions crates/aios-runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,13 @@ impl KernelRuntime {
.await?;
emitted += 1;

// Build conversation history from prior events in this session.
// This reconstructs user objectives and assistant responses so the LLM
// has multi-turn context.
let conversation_history = self
.build_conversation_history(session_id, branch_id)
.await;

let completion = if let Some(call) = input.proposed_tool.clone() {
Ok(aios_protocol::ModelCompletion {
provider: "inline-proposed-tool".to_owned(),
Expand All @@ -356,6 +363,7 @@ impl KernelRuntime {
proposed_tool: None,
system_prompt: input.system_prompt.clone(),
allowed_tools: input.allowed_tools.clone(),
conversation_history,
})
.await
.map_err(|error| anyhow::anyhow!(error.to_string()))
Expand Down Expand Up @@ -935,6 +943,94 @@ impl KernelRuntime {
.map_err(|error| anyhow::anyhow!(error.to_string()))
}

/// Build conversation history from the session's event journal.
///
/// Reads prior events and extracts user objectives (from `DeliberationProposed`)
/// and assistant responses (from `Message` with role=assistant, or aggregated
/// `TextDelta` events). Returns a list of `ConversationTurn` entries in
/// chronological order, capped at the most recent 50 turns to avoid
/// context overflow.
async fn build_conversation_history(
&self,
session_id: &SessionId,
branch_id: &BranchId,
) -> Vec<aios_protocol::ConversationTurn> {
let events = match self
.event_store
.read(session_id.clone(), branch_id.clone(), 0, 10_000)
.await
{
Ok(events) => events,
Err(err) => {
debug!(%err, "failed to read events for conversation history");
return Vec::new();
}
};

let mut turns = Vec::new();
let mut current_assistant_text = String::new();

for record in &events {
match &record.kind {
EventKind::DeliberationProposed { summary, .. } => {
// Flush any pending assistant text before the next user turn.
if !current_assistant_text.is_empty() {
turns.push(aios_protocol::ConversationTurn {
role: "assistant".to_owned(),
content: std::mem::take(&mut current_assistant_text),
});
}
if !summary.is_empty() {
turns.push(aios_protocol::ConversationTurn {
role: "user".to_owned(),
content: summary.clone(),
});
}
}
EventKind::Message {
role, content, ..
} if role == "assistant" => {
current_assistant_text.push_str(content);
}
EventKind::TextDelta { delta, .. } => {
current_assistant_text.push_str(delta);
}
EventKind::RunFinished { final_answer, .. } => {
// If we have a final answer and no accumulated text, use it.
if current_assistant_text.is_empty() {
if let Some(answer) = final_answer {
current_assistant_text = answer.clone();
}
}
// Flush assistant text at run boundary.
if !current_assistant_text.is_empty() {
turns.push(aios_protocol::ConversationTurn {
role: "assistant".to_owned(),
content: std::mem::take(&mut current_assistant_text),
});
}
}
_ => {}
}
}

// Flush any remaining assistant text.
if !current_assistant_text.is_empty() {
turns.push(aios_protocol::ConversationTurn {
role: "assistant".to_owned(),
content: current_assistant_text,
});
}

// Cap to most recent 50 turns to avoid context overflow.
let max_turns = 50;
if turns.len() > max_turns {
turns.drain(..turns.len() - max_turns);
}

turns
}

fn estimate_mode(&self, state: &AgentStateVector, pending_approvals: usize) -> OperatingMode {
if pending_approvals > 0 {
return OperatingMode::AskHuman;
Expand Down
Loading