make the unrolled loop state machine generic (#11139)
Signed-off-by: Jack Amadeo <jackamadeo@squareup.com>
This commit is contained in:
@@ -27,16 +27,18 @@ use crate::agents::platform_extensions::MANAGE_EXTENSIONS_TOOL_NAME_COMPLETE;
|
||||
use crate::agents::prompt_manager::PromptManager;
|
||||
use crate::agents::retry::{RetryManager, RetryResult};
|
||||
use crate::agents::state_machine::{
|
||||
BangShellOperation, CompactionOperation, DoctorOperation, Emitter, ExitOnErrorOperation,
|
||||
InferenceRunner, MaxTurnsOperation, Operation, ProjectOperation, RecipeOperation,
|
||||
RetryOperation, SkillOperation, SlashCommandOperation, StateMachine, SteerOperation,
|
||||
SteerQueue, Step, StopHookOperation, ToolApprovalOperation, ToolExecutionOperation,
|
||||
ToolPairCompactionOperation, UnknownToolOperation, MAX_TURNS_MESSAGE,
|
||||
run_goose, BangShellOperation, CompactionOperation, DoctorOperation, Emitter,
|
||||
EntryHookOperation, ExitOnErrorOperation, GooseEffect, InferenceRunner, MaxTurnsOperation,
|
||||
Operation, ProjectOperation, RecipeOperation, RetryOperation, SkillOperation,
|
||||
SlashCommandOperation, StateMachine, SteerOperation, SteerQueue, Step, StopHookOperation,
|
||||
ToolApprovalOperation, ToolExecutionOperation, ToolPairCompactionOperation,
|
||||
UnknownToolOperation, MAX_TURNS_MESSAGE,
|
||||
};
|
||||
use crate::agents::types::{
|
||||
FrontendTool, SessionConfig, SharedProvider, ToolResultReceiver,
|
||||
DEFAULT_ON_FAILURE_TIMEOUT_SECONDS, DEFAULT_RETRY_TIMEOUT_SECONDS,
|
||||
};
|
||||
use crate::agents::AgentEvent;
|
||||
use crate::config::extensions::name_to_key;
|
||||
use crate::config::permission::PermissionManager;
|
||||
use crate::config::{get_enabled_extensions, Config, GooseMode};
|
||||
@@ -71,7 +73,7 @@ use goose_providers::thinking::ThinkingEffort;
|
||||
use regex::Regex;
|
||||
use rmcp::model::{
|
||||
CallToolRequestParams, CallToolResult, ContentBlock, ElicitationAction, ErrorCode, ErrorData,
|
||||
GetPromptResult, Prompt, ServerNotification, Tool,
|
||||
GetPromptResult, Prompt, Tool,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
@@ -282,18 +284,6 @@ pub struct Agent {
|
||||
steer_queues: Mutex<HashMap<String, SteerQueue>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum AgentEvent {
|
||||
Message(Message),
|
||||
Usage(crate::providers::base::ProviderUsage),
|
||||
MessageUsage {
|
||||
message_id: Option<String>,
|
||||
usage: MessageUsage,
|
||||
},
|
||||
McpNotification((String, ServerNotification)),
|
||||
HistoryReplaced(Conversation),
|
||||
}
|
||||
|
||||
fn ensure_message_event_id(event: AgentEvent) -> AgentEvent {
|
||||
match event {
|
||||
AgentEvent::Message(message) => AgentEvent::Message(message.with_generated_id_if_missing()),
|
||||
@@ -1600,7 +1590,7 @@ impl Agent {
|
||||
max_turns: Option<u32>,
|
||||
cancel: CancellationToken,
|
||||
steer_queue: SteerQueue,
|
||||
) -> StateMachine<'_> {
|
||||
) -> StateMachine<'_, Session, GooseEffect> {
|
||||
let max_turns = max_turns.unwrap_or_else(|| {
|
||||
Config::global()
|
||||
.get_param::<u32>("GOOSE_MAX_TURNS")
|
||||
@@ -1633,7 +1623,7 @@ impl Agent {
|
||||
let tool_pair_compaction_enabled = crate::context_mgmt::tool_pair_summarization_enabled()
|
||||
&& !provider.manages_own_context();
|
||||
|
||||
let operations: Vec<Arc<dyn Operation + '_>> = vec![
|
||||
let operations: Vec<Arc<dyn Operation<Session, GooseEffect> + '_>> = vec![
|
||||
Arc::new(SteerOperation::new(steer_queue, self.hook_manager.clone())),
|
||||
Arc::new(MaxTurnsOperation::new(max_turns)),
|
||||
Arc::new(BangShellOperation::new()),
|
||||
@@ -1686,9 +1676,12 @@ impl Agent {
|
||||
));
|
||||
let mut command_handlers = operations.clone();
|
||||
command_handlers.push(inference.clone());
|
||||
let command_operation: Arc<dyn Operation + '_> =
|
||||
let command_operation: Arc<dyn Operation<Session, GooseEffect> + '_> =
|
||||
Arc::new(SlashCommandOperation::new(command_handlers));
|
||||
let operations: Vec<_> = std::iter::once(command_operation)
|
||||
let operations: Vec<_> =
|
||||
std::iter::once(Arc::new(EntryHookOperation::new(self.hook_manager.clone()))
|
||||
as Arc<dyn Operation<Session, GooseEffect> + '_>)
|
||||
.chain(std::iter::once(command_operation))
|
||||
.chain(operations)
|
||||
.collect();
|
||||
|
||||
@@ -1698,7 +1691,7 @@ impl Agent {
|
||||
.chain(std::iter::once(Step::Inference(inference)))
|
||||
.collect();
|
||||
|
||||
StateMachine::new(steps, cancel).with_hook_manager(self.hook_manager.clone())
|
||||
StateMachine::new(steps, cancel)
|
||||
}
|
||||
|
||||
pub(crate) async fn reply_with_state_machine(
|
||||
@@ -1784,7 +1777,7 @@ impl Agent {
|
||||
let (tx, mut rx) = mpsc::channel::<AgentEvent>(32);
|
||||
let emit = Emitter::new(tx, cancel.clone());
|
||||
let result = {
|
||||
let run = machine.run(session_manager.as_ref(), &session_id, &emit);
|
||||
let run = run_goose(&machine, session_manager.as_ref(), &session_id, &emit);
|
||||
tokio::pin!(run);
|
||||
loop {
|
||||
tokio::select! {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
use rmcp::model::ServerNotification;
|
||||
|
||||
use crate::conversation::message::{Message, MessageUsage};
|
||||
use crate::conversation::Conversation;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum AgentEvent {
|
||||
Message(Message),
|
||||
Usage(crate::providers::base::ProviderUsage),
|
||||
MessageUsage {
|
||||
message_id: Option<String>,
|
||||
usage: MessageUsage,
|
||||
},
|
||||
McpNotification((String, ServerNotification)),
|
||||
HistoryReplaced(Conversation),
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
mod agent;
|
||||
pub mod container;
|
||||
mod events;
|
||||
pub mod execute_commands;
|
||||
pub mod extension;
|
||||
pub mod extension_malware_check;
|
||||
pub mod extension_manager;
|
||||
pub mod final_output_tool;
|
||||
mod gen_ai_telemetry;
|
||||
pub(crate) mod gen_ai_telemetry;
|
||||
mod large_response_handler;
|
||||
pub mod mcp_client;
|
||||
pub mod moim;
|
||||
@@ -25,8 +26,9 @@ mod tool_schema_normalize;
|
||||
pub mod types;
|
||||
pub mod validate_extensions;
|
||||
|
||||
pub use agent::{Agent, AgentConfig, AgentEvent, ExtensionLoadResult, GoosePlatform};
|
||||
pub use agent::{Agent, AgentConfig, ExtensionLoadResult, GoosePlatform};
|
||||
pub use container::Container;
|
||||
pub use events::AgentEvent;
|
||||
pub use execute_commands::COMPACT_TRIGGERS;
|
||||
pub use extension::{ExtensionConfig, ExtensionError};
|
||||
pub use extension_manager::ExtensionManager;
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
use crate::agents::state_machine::operation::{ConversationEffect, MachineEffect};
|
||||
use crate::conversation::message::Message;
|
||||
use crate::conversation::Conversation;
|
||||
use crate::providers::base::ProviderUsage;
|
||||
use crate::recipe::Recipe;
|
||||
use crate::session::ExtensionData;
|
||||
|
||||
pub enum GooseEffect {
|
||||
Conversation(ConversationEffect),
|
||||
ReplaceConversation {
|
||||
conversation: Conversation,
|
||||
usage: Option<ProviderUsage>,
|
||||
},
|
||||
SetRecipe(Box<Option<Recipe>>),
|
||||
SetExtensionData(ExtensionData),
|
||||
RecordUsage(ProviderUsage),
|
||||
}
|
||||
|
||||
impl MachineEffect for GooseEffect {
|
||||
fn ensure_message_ids(&mut self) {
|
||||
match self {
|
||||
GooseEffect::Conversation(effect) => effect.ensure_message_ids(),
|
||||
GooseEffect::ReplaceConversation { conversation, .. } => {
|
||||
for message in conversation.messages_mut() {
|
||||
if message.id.is_none() {
|
||||
message.id = Some(format!("msg_{}", uuid::Uuid::new_v4()));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ConversationEffect> for GooseEffect {
|
||||
fn from(effect: ConversationEffect) -> Self {
|
||||
GooseEffect::Conversation(effect)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Message> for GooseEffect {
|
||||
fn from(message: Message) -> Self {
|
||||
ConversationEffect::from(message).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Conversation> for GooseEffect {
|
||||
fn from(conversation: Conversation) -> Self {
|
||||
ConversationEffect::from(conversation).into()
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,43 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::agents::state_machine::operation::{
|
||||
messages_since_kickoff, Emitter, Inference, InferenceInput, Operation, OperationFuture,
|
||||
OperationResult, StateEffect, StepResult,
|
||||
ConversationEffect, Emitter, Inference, InferenceInput, MachineEffect, Operation,
|
||||
OperationFuture, OperationResult, StepResult,
|
||||
};
|
||||
use crate::agents::state_machine::usage;
|
||||
use crate::agents::AgentEvent;
|
||||
use crate::conversation::message::Message;
|
||||
use crate::conversation::Conversation;
|
||||
use crate::hooks::{HookContext, HookEvent, HookManager};
|
||||
use crate::session::{Session, SessionManager};
|
||||
|
||||
pub enum Step<'a> {
|
||||
Operation(Arc<dyn Operation + 'a>),
|
||||
Inference(Arc<dyn Inference + 'a>),
|
||||
pub trait MachineSession: Send + Sync {
|
||||
fn id(&self) -> &str;
|
||||
fn conversation(&self) -> Option<&Conversation>;
|
||||
}
|
||||
|
||||
impl Step<'_> {
|
||||
fn operation(&self) -> &dyn Operation {
|
||||
#[async_trait]
|
||||
pub trait SessionLoader<S>: Send + Sync {
|
||||
async fn load(&self, session_id: &str) -> Result<S>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait EffectHandler<S, E>: Send + Sync {
|
||||
async fn apply_effects(&self, session: &S, effects: &mut [E], emit: &Emitter) -> Result<()>;
|
||||
}
|
||||
|
||||
pub trait EffectUsage<E>: Send + Sync {
|
||||
fn usage(&self, _effect: &E) -> Option<goose_providers::conversation::token_usage::Usage> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Step<'a, S, E = ConversationEffect> {
|
||||
Operation(Arc<dyn Operation<S, E> + 'a>),
|
||||
Inference(Arc<dyn Inference<S, E> + 'a>),
|
||||
}
|
||||
|
||||
impl<S, E: Send> Step<'_, S, E> {
|
||||
fn operation(&self) -> &dyn Operation<S, E> {
|
||||
match self {
|
||||
Step::Operation(operation) => operation.as_ref(),
|
||||
Step::Inference(inference) => inference.as_ref(),
|
||||
@@ -28,87 +45,31 @@ impl Step<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StateMachine<'a> {
|
||||
steps: Vec<Step<'a>>,
|
||||
pub struct StateMachine<'a, S, E = ConversationEffect> {
|
||||
steps: Vec<Step<'a, S, E>>,
|
||||
cancel: CancellationToken,
|
||||
hook_manager: HookManager,
|
||||
}
|
||||
|
||||
impl<'a> StateMachine<'a> {
|
||||
pub fn new(steps: Vec<Step<'a>>, cancel: CancellationToken) -> Self {
|
||||
Self {
|
||||
steps,
|
||||
cancel,
|
||||
hook_manager: HookManager::default(),
|
||||
}
|
||||
impl<'a, S, E> StateMachine<'a, S, E>
|
||||
where
|
||||
S: MachineSession,
|
||||
E: MachineEffect + Send + 'static,
|
||||
{
|
||||
pub fn new(steps: Vec<Step<'a, S, E>>, cancel: CancellationToken) -> Self {
|
||||
Self { steps, cancel }
|
||||
}
|
||||
|
||||
pub fn with_hook_manager(mut self, hook_manager: HookManager) -> Self {
|
||||
self.hook_manager = hook_manager;
|
||||
self
|
||||
}
|
||||
|
||||
async fn emit_entry_hooks(&self, session: &Session, conversation: &Conversation) -> Result<()> {
|
||||
let messages = messages_since_kickoff(conversation)?;
|
||||
if Self::has_agent_reply(messages) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let messages_before_kickoff =
|
||||
&conversation.messages()[..conversation.len() - messages.len()];
|
||||
if !messages_before_kickoff.iter().any(|message| {
|
||||
message.role == rmcp::model::Role::User
|
||||
&& message.is_user_visible()
|
||||
&& !message.is_tool_response()
|
||||
}) {
|
||||
self.hook_manager
|
||||
.emit(
|
||||
HookEvent::SessionStart,
|
||||
HookContext::new(HookEvent::SessionStart, &session.id)
|
||||
.with_working_dir(session.working_dir.to_string_lossy().to_string()),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
let prompt = messages
|
||||
.first()
|
||||
.map(Message::as_concat_text)
|
||||
.unwrap_or_default();
|
||||
if !prompt.is_empty() {
|
||||
self.hook_manager
|
||||
.emit(
|
||||
HookEvent::UserPromptSubmit,
|
||||
HookContext::new(HookEvent::UserPromptSubmit, &session.id)
|
||||
.with_message(prompt)
|
||||
.with_working_dir(session.working_dir.to_string_lossy().to_string()),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn has_agent_reply(messages: &[Message]) -> bool {
|
||||
messages.iter().any(|message| {
|
||||
message.role == rmcp::model::Role::Assistant
|
||||
&& ((message.is_user_visible() && message.is_agent_visible())
|
||||
|| message.error_kind().is_some())
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn step(&self, session: &Session, emit: &Emitter) -> Result<Option<StepResult>> {
|
||||
pub async fn step(&self, session: &S, emit: &Emitter) -> Result<Option<StepResult<E>>> {
|
||||
let conversation = session
|
||||
.conversation
|
||||
.as_ref()
|
||||
.conversation()
|
||||
.ok_or_else(|| anyhow!("state-machine session loaded without conversation"))?;
|
||||
|
||||
self.emit_entry_hooks(session, conversation).await?;
|
||||
|
||||
for step in &self.steps {
|
||||
let name = step.operation().name();
|
||||
let result = if self.cancel.is_cancelled() {
|
||||
OperationResult::NotApplicable
|
||||
} else {
|
||||
let step_fut: OperationFuture<'_, Result<OperationResult>> = match step {
|
||||
let step_fut: OperationFuture<'_, Result<OperationResult<E>>> = match step {
|
||||
Step::Operation(operation) => operation.run(session, conversation, emit),
|
||||
Step::Inference(inference) => {
|
||||
let mut input = InferenceInput::default();
|
||||
@@ -140,13 +101,13 @@ impl<'a> StateMachine<'a> {
|
||||
match result {
|
||||
OperationResult::NotApplicable => {}
|
||||
OperationResult::Applied(mut result) => {
|
||||
// `step` and `apply` are separate entry points; a caller that
|
||||
// drives steps itself still gets effects it can persist.
|
||||
result.ensure_message_ids();
|
||||
result.applied_step = Some(name);
|
||||
for effect in &mut result.effects {
|
||||
effect.ensure_message_ids();
|
||||
}
|
||||
if cancelled {
|
||||
result.yield_to_client = true;
|
||||
}
|
||||
tracing::debug!(target: "goose::state_machine", step = name, "applied step");
|
||||
return Ok(Some(result));
|
||||
}
|
||||
}
|
||||
@@ -155,173 +116,38 @@ impl<'a> StateMachine<'a> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub async fn apply(
|
||||
pub async fn apply<R>(
|
||||
&self,
|
||||
session_manager: &SessionManager,
|
||||
session: &Session,
|
||||
result: &mut StepResult,
|
||||
runtime: &R,
|
||||
session: &S,
|
||||
result: &mut StepResult<E>,
|
||||
emit: &Emitter,
|
||||
) -> Result<()> {
|
||||
result.ensure_message_ids();
|
||||
usage::enrich(session, &mut result.effects);
|
||||
|
||||
for effect in &result.effects {
|
||||
match effect {
|
||||
StateEffect::AppendMessage(message) => {
|
||||
session_manager.add_message(&session.id, message).await?;
|
||||
}
|
||||
StateEffect::ReplaceConversation {
|
||||
conversation,
|
||||
usage: replacement_usage,
|
||||
} => {
|
||||
if let Some(usage) = replacement_usage {
|
||||
usage::record(session_manager, session, usage, true).await?;
|
||||
}
|
||||
session_manager
|
||||
.replace_conversation(&session.id, conversation)
|
||||
.await?;
|
||||
session_manager
|
||||
.update(&session.id)
|
||||
.usage(usage::estimate_context(conversation).await?)
|
||||
.apply()
|
||||
.await?;
|
||||
}
|
||||
StateEffect::PatchToolRequestMeta {
|
||||
tool_call_id,
|
||||
patch,
|
||||
} => {
|
||||
session_manager
|
||||
.update_tool_request_meta(&session.id, tool_call_id, patch.clone())
|
||||
.await?;
|
||||
}
|
||||
StateEffect::SetMessageVisibility {
|
||||
message_id,
|
||||
user_visible,
|
||||
agent_visible,
|
||||
} => {
|
||||
session_manager
|
||||
.update_message_metadata(&session.id, message_id, |mut metadata| {
|
||||
metadata.user_visible = *user_visible;
|
||||
metadata.agent_visible = *agent_visible;
|
||||
metadata
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
StateEffect::SetRecipe(recipe) => {
|
||||
session_manager
|
||||
.update(&session.id)
|
||||
.recipe(recipe.as_ref().clone())
|
||||
.apply()
|
||||
.await?;
|
||||
}
|
||||
StateEffect::SetExtensionData(extension_data) => {
|
||||
session_manager
|
||||
.update(&session.id)
|
||||
.extension_data(extension_data.clone())
|
||||
.apply()
|
||||
.await?;
|
||||
}
|
||||
StateEffect::RecordUsage(usage) => {
|
||||
usage::record(session_manager, session, usage, false).await?;
|
||||
}
|
||||
}
|
||||
) -> Result<()>
|
||||
where
|
||||
R: EffectHandler<S, E>,
|
||||
{
|
||||
for effect in &mut result.effects {
|
||||
effect.ensure_message_ids();
|
||||
}
|
||||
|
||||
for effect in &result.effects {
|
||||
match effect {
|
||||
StateEffect::AppendMessage(message) => {
|
||||
if let Some(usage) = message
|
||||
.metadata
|
||||
.usage
|
||||
.as_deref()
|
||||
.filter(|_| !message.user_visible_content().content.is_empty())
|
||||
.cloned()
|
||||
{
|
||||
emit.emit(AgentEvent::MessageUsage {
|
||||
message_id: message.id.clone(),
|
||||
usage,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
StateEffect::ReplaceConversation { conversation, .. } => {
|
||||
emit.emit(AgentEvent::HistoryReplaced(conversation.clone()))
|
||||
.await;
|
||||
}
|
||||
StateEffect::RecordUsage(usage) => {
|
||||
emit.emit(AgentEvent::Usage(usage.clone())).await
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
runtime
|
||||
.apply_effects(session, &mut result.effects, emit)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn run(
|
||||
&self,
|
||||
session_manager: &SessionManager,
|
||||
session_id: &str,
|
||||
emit: &Emitter,
|
||||
) -> Result<Session> {
|
||||
let entry_session = session_manager.get_session(session_id, true).await?;
|
||||
if let Some(input) = entry_session
|
||||
.conversation
|
||||
.as_ref()
|
||||
.and_then(|conversation| messages_since_kickoff(conversation).ok())
|
||||
.and_then(|messages| messages.first())
|
||||
.map(Message::user_visible_content)
|
||||
.map(|message| message.as_concat_text())
|
||||
.filter(|text| !text.is_empty())
|
||||
{
|
||||
tracing::Span::current().record("trace_input", input.as_str());
|
||||
}
|
||||
|
||||
let mut turn_usage = goose_providers::conversation::token_usage::Usage::default();
|
||||
pub async fn run<R>(&self, runtime: &R, session_id: &str, emit: &Emitter) -> Result<S>
|
||||
where
|
||||
R: SessionLoader<S> + EffectHandler<S, E>,
|
||||
{
|
||||
loop {
|
||||
let session = session_manager.get_session(session_id, true).await?;
|
||||
let session = runtime.load(session_id).await?;
|
||||
let Some(mut result) = self.step(&session, emit).await? else {
|
||||
break;
|
||||
};
|
||||
for effect in &result.effects {
|
||||
match effect {
|
||||
StateEffect::RecordUsage(usage)
|
||||
| StateEffect::ReplaceConversation {
|
||||
usage: Some(usage), ..
|
||||
} => turn_usage += usage.usage,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
self.apply(session_manager, &session, &mut result, emit)
|
||||
.await?;
|
||||
self.apply(runtime, &session, &mut result, emit).await?;
|
||||
if result.yield_to_client {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let session = session_manager.get_session(session_id, true).await?;
|
||||
let last_assistant_text = session
|
||||
.conversation
|
||||
.as_ref()
|
||||
.and_then(|conversation| messages_since_kickoff(conversation).ok())
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.rev()
|
||||
.filter(|message| message.role == rmcp::model::Role::Assistant)
|
||||
.map(Message::user_visible_content)
|
||||
.map(|message| message.as_concat_text())
|
||||
.find(|text| !text.is_empty())
|
||||
.unwrap_or_default();
|
||||
if !last_assistant_text.is_empty() {
|
||||
let span = tracing::Span::current();
|
||||
span.record("trace_output", last_assistant_text.as_str());
|
||||
if crate::agents::gen_ai_telemetry::capture_message_content() {
|
||||
let output =
|
||||
crate::agents::gen_ai_telemetry::simple_output_json(&last_assistant_text);
|
||||
span.record("gen_ai.output.messages", output.as_str());
|
||||
}
|
||||
}
|
||||
crate::agents::gen_ai_telemetry::record_usage(&tracing::Span::current(), &turn_usage);
|
||||
|
||||
Ok(session)
|
||||
runtime.load(session_id).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
//! `StateMachine::run`. Goose's concrete operations remain internal because their
|
||||
//! configuration is part of `Agent::reply`, not the state-machine protocol.
|
||||
|
||||
mod effects;
|
||||
mod machine;
|
||||
mod operation;
|
||||
mod ops_bang_shell;
|
||||
mod ops_compaction;
|
||||
mod ops_doctor;
|
||||
mod ops_entry_hook;
|
||||
mod ops_exit_on_error;
|
||||
mod ops_llm;
|
||||
mod ops_maxturns;
|
||||
@@ -24,20 +26,25 @@ mod ops_tool_approval;
|
||||
mod ops_tool_pair_compaction;
|
||||
mod ops_toolcalling;
|
||||
mod ops_unknown_tool;
|
||||
mod session;
|
||||
pub(crate) use session::run as run_goose;
|
||||
mod usage;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub use machine::{StateMachine, Step};
|
||||
pub use effects::GooseEffect;
|
||||
pub use machine::{EffectHandler, EffectUsage, MachineSession, SessionLoader, StateMachine, Step};
|
||||
pub use operation::{
|
||||
applied, not_applicable, yielded, yielded_with, Emitter, Inference, InferenceInput, Operation,
|
||||
OperationResult, SlashCommand, StateEffect, StepResult,
|
||||
applied, assistant_turn_count, ends_turn, last_effective_role, messages_since_kickoff,
|
||||
not_applicable, trailing_error, yielded, yielded_with, ConversationEffect, Emitter, Inference,
|
||||
InferenceInput, MachineEffect, Operation, OperationResult, SlashCommand, StepResult,
|
||||
};
|
||||
|
||||
pub(super) use ops_bang_shell::{bang_shell_command, BangShellOperation};
|
||||
pub(super) use ops_compaction::CompactionOperation;
|
||||
pub(super) use ops_doctor::DoctorOperation;
|
||||
pub(super) use ops_entry_hook::EntryHookOperation;
|
||||
pub(super) use ops_exit_on_error::ExitOnErrorOperation;
|
||||
pub(super) use ops_llm::InferenceRunner;
|
||||
pub(super) use ops_maxturns::{MaxTurnsOperation, MAX_TURNS_MESSAGE};
|
||||
|
||||
@@ -8,9 +8,6 @@ use tokio_util::sync::CancellationToken;
|
||||
use crate::agents::AgentEvent;
|
||||
use crate::conversation::message::{Message, MessageContent, MessageErrorKind};
|
||||
use crate::conversation::{effective_role, Conversation, EffectiveRole};
|
||||
use crate::providers::base::ProviderUsage;
|
||||
use crate::recipe::Recipe;
|
||||
use crate::session::{ExtensionData, Session};
|
||||
use rmcp::model::Tool;
|
||||
|
||||
pub type OperationFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
|
||||
@@ -76,7 +73,7 @@ pub fn ends_turn(messages: &[Message]) -> bool {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait Operation: Send + Sync {
|
||||
pub trait Operation<S, E: Send + 'static = ConversationEffect>: Send + Sync {
|
||||
fn name(&self) -> &'static str;
|
||||
|
||||
/// Note on a message something this operation did, so that a pipeline rebuilt
|
||||
@@ -93,50 +90,46 @@ pub trait Operation: Send + Sync {
|
||||
|
||||
async fn cancel(
|
||||
&self,
|
||||
_session: &Session,
|
||||
_session: &S,
|
||||
_conversation: &Conversation,
|
||||
result: OperationResult,
|
||||
result: OperationResult<E>,
|
||||
_emit: &Emitter,
|
||||
) -> Result<OperationResult> {
|
||||
) -> Result<OperationResult<E>> {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn run_command(
|
||||
&self,
|
||||
_command: &SlashCommand<'_>,
|
||||
_session: &Session,
|
||||
_session: &S,
|
||||
_conversation: &Conversation,
|
||||
_emit: &Emitter,
|
||||
) -> Result<OperationResult> {
|
||||
) -> Result<OperationResult<E>> {
|
||||
not_applicable()
|
||||
}
|
||||
|
||||
async fn inference_tools(&self, _session: &Session) -> Result<Vec<Tool>> {
|
||||
async fn inference_tools(&self, _session: &S) -> Result<Vec<Tool>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
async fn prompt_parts(
|
||||
&self,
|
||||
_session: &Session,
|
||||
_session: &S,
|
||||
_conversation: &Conversation,
|
||||
) -> Result<Vec<(String, String)>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
async fn moim_parts(
|
||||
&self,
|
||||
_session: &Session,
|
||||
_conversation: &Conversation,
|
||||
) -> Result<Vec<String>> {
|
||||
async fn moim_parts(&self, _session: &S, _conversation: &Conversation) -> Result<Vec<String>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
_session: &Session,
|
||||
_session: &S,
|
||||
_conversation: &Conversation,
|
||||
_emit: &Emitter,
|
||||
) -> Result<OperationResult> {
|
||||
) -> Result<OperationResult<E>> {
|
||||
not_applicable()
|
||||
}
|
||||
}
|
||||
@@ -149,69 +142,66 @@ pub struct InferenceInput {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait Inference: Operation {
|
||||
pub trait Inference<S, E: Send + 'static = ConversationEffect>: Operation<S, E> {
|
||||
/// Whether the next step would reach the provider. The machine asks before
|
||||
/// firing the hooks that mark the start of a turn.
|
||||
fn applies(&self, conversation: &Conversation) -> bool;
|
||||
|
||||
async fn infer(
|
||||
&self,
|
||||
session: &Session,
|
||||
session: &S,
|
||||
conversation: &Conversation,
|
||||
input: InferenceInput,
|
||||
emit: &Emitter,
|
||||
) -> Result<OperationResult>;
|
||||
) -> Result<OperationResult<E>>;
|
||||
}
|
||||
|
||||
pub struct StepResult {
|
||||
pub effects: Vec<StateEffect>,
|
||||
pub struct StepResult<E = ConversationEffect> {
|
||||
pub effects: Vec<E>,
|
||||
pub applied_step: Option<&'static str>,
|
||||
pub yield_to_client: bool,
|
||||
}
|
||||
|
||||
impl StepResult {
|
||||
pub(super) fn ensure_message_ids(&mut self) {
|
||||
for effect in &mut self.effects {
|
||||
effect.ensure_message_ids();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum OperationResult {
|
||||
pub enum OperationResult<E = ConversationEffect> {
|
||||
NotApplicable,
|
||||
Applied(StepResult),
|
||||
Applied(StepResult<E>),
|
||||
}
|
||||
|
||||
pub fn not_applicable() -> Result<OperationResult> {
|
||||
pub fn not_applicable<E>() -> Result<OperationResult<E>> {
|
||||
Ok(OperationResult::NotApplicable)
|
||||
}
|
||||
|
||||
pub fn applied(effects: impl IntoIterator<Item = StateEffect>) -> Result<OperationResult> {
|
||||
pub fn applied<E>(effects: impl IntoIterator<Item = E>) -> Result<OperationResult<E>> {
|
||||
Ok(OperationResult::Applied(StepResult {
|
||||
effects: effects.into_iter().collect(),
|
||||
applied_step: None,
|
||||
yield_to_client: false,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn yielded() -> Result<OperationResult> {
|
||||
pub fn yielded<E>() -> Result<OperationResult<E>> {
|
||||
Ok(OperationResult::Applied(StepResult {
|
||||
effects: Vec::new(),
|
||||
applied_step: None,
|
||||
yield_to_client: true,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn yielded_with(effects: impl IntoIterator<Item = StateEffect>) -> Result<OperationResult> {
|
||||
pub fn yielded_with<E>(effects: impl IntoIterator<Item = E>) -> Result<OperationResult<E>> {
|
||||
Ok(OperationResult::Applied(StepResult {
|
||||
effects: effects.into_iter().collect(),
|
||||
applied_step: None,
|
||||
yield_to_client: true,
|
||||
}))
|
||||
}
|
||||
|
||||
pub enum StateEffect {
|
||||
pub trait MachineEffect {
|
||||
fn ensure_message_ids(&mut self);
|
||||
}
|
||||
|
||||
pub enum ConversationEffect {
|
||||
AppendMessage(Message),
|
||||
ReplaceConversation {
|
||||
conversation: Conversation,
|
||||
usage: Option<ProviderUsage>,
|
||||
},
|
||||
ReplaceConversation(Conversation),
|
||||
PatchToolRequestMeta {
|
||||
tool_call_id: String,
|
||||
patch: serde_json::Value,
|
||||
@@ -221,16 +211,13 @@ pub enum StateEffect {
|
||||
user_visible: bool,
|
||||
agent_visible: bool,
|
||||
},
|
||||
SetRecipe(Box<Option<Recipe>>),
|
||||
SetExtensionData(ExtensionData),
|
||||
RecordUsage(ProviderUsage),
|
||||
}
|
||||
|
||||
impl StateEffect {
|
||||
pub(super) fn ensure_message_ids(&mut self) {
|
||||
impl MachineEffect for ConversationEffect {
|
||||
fn ensure_message_ids(&mut self) {
|
||||
let messages = match self {
|
||||
StateEffect::AppendMessage(message) => std::slice::from_mut(message),
|
||||
StateEffect::ReplaceConversation { conversation, .. } => {
|
||||
ConversationEffect::AppendMessage(message) => std::slice::from_mut(message),
|
||||
ConversationEffect::ReplaceConversation(conversation) => {
|
||||
conversation.messages_mut().as_mut_slice()
|
||||
}
|
||||
_ => return,
|
||||
@@ -243,18 +230,15 @@ impl StateEffect {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Message> for StateEffect {
|
||||
impl From<Message> for ConversationEffect {
|
||||
fn from(message: Message) -> Self {
|
||||
StateEffect::AppendMessage(message)
|
||||
ConversationEffect::AppendMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Conversation> for StateEffect {
|
||||
impl From<Conversation> for ConversationEffect {
|
||||
fn from(conversation: Conversation) -> Self {
|
||||
StateEffect::ReplaceConversation {
|
||||
conversation,
|
||||
usage: None,
|
||||
}
|
||||
ConversationEffect::ReplaceConversation(conversation)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use rmcp::model::CallToolRequestParams;
|
||||
|
||||
use crate::agents::state_machine::operation::{
|
||||
use crate::agents::state_machine::effects::GooseEffect;
|
||||
use crate::agents::state_machine::{
|
||||
applied, last_effective_role, messages_since_kickoff, not_applicable, yielded, Emitter,
|
||||
Operation, OperationResult,
|
||||
};
|
||||
@@ -31,7 +32,7 @@ impl BangShellOperation {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Operation for BangShellOperation {
|
||||
impl Operation<Session, GooseEffect> for BangShellOperation {
|
||||
fn name(&self) -> &'static str {
|
||||
"bang_shell"
|
||||
}
|
||||
@@ -41,7 +42,7 @@ impl Operation for BangShellOperation {
|
||||
_session: &Session,
|
||||
conversation: &Conversation,
|
||||
emit: &Emitter,
|
||||
) -> Result<OperationResult> {
|
||||
) -> Result<OperationResult<GooseEffect>> {
|
||||
let messages = messages_since_kickoff(conversation)?;
|
||||
let Some(kickoff) = messages.first() else {
|
||||
return not_applicable();
|
||||
|
||||
@@ -6,11 +6,12 @@ use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use tracing_futures::Instrument;
|
||||
|
||||
use crate::agents::state_machine::operation::{
|
||||
applied, last_effective_role, messages_since_kickoff, not_applicable, trailing_error, yielded,
|
||||
yielded_with, Emitter, Operation, OperationResult, SlashCommand, StateEffect,
|
||||
};
|
||||
use crate::agents::state_machine::ops_llm::{chat_span, record_chat_usage};
|
||||
use crate::agents::state_machine::{
|
||||
applied, last_effective_role, messages_since_kickoff, not_applicable, trailing_error, yielded,
|
||||
yielded_with, ConversationEffect, Emitter, GooseEffect, Operation, OperationResult,
|
||||
SlashCommand,
|
||||
};
|
||||
use crate::context_mgmt::compact_messages;
|
||||
use crate::conversation::message::{Message, MessageErrorKind, SystemNotificationType};
|
||||
use crate::conversation::{Conversation, EffectiveRole};
|
||||
@@ -79,7 +80,7 @@ impl CompactionOperation {
|
||||
conversation: &Conversation,
|
||||
message: String,
|
||||
emit: &Emitter,
|
||||
) -> Result<OperationResult> {
|
||||
) -> Result<OperationResult<GooseEffect>> {
|
||||
let command = messages_since_kickoff(conversation)?
|
||||
.first()
|
||||
.cloned()
|
||||
@@ -95,16 +96,20 @@ impl CompactionOperation {
|
||||
emit.message(command).await;
|
||||
let response = emit.message(response).await;
|
||||
yielded_with([
|
||||
StateEffect::SetMessageVisibility {
|
||||
ConversationEffect::SetMessageVisibility {
|
||||
message_id,
|
||||
user_visible: true,
|
||||
agent_visible: false,
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
response.into(),
|
||||
])
|
||||
}
|
||||
|
||||
async fn clear(conversation: &Conversation, emit: &Emitter) -> Result<OperationResult> {
|
||||
async fn clear(
|
||||
conversation: &Conversation,
|
||||
emit: &Emitter,
|
||||
) -> Result<OperationResult<GooseEffect>> {
|
||||
let command = messages_since_kickoff(conversation)?
|
||||
.first()
|
||||
.cloned()
|
||||
@@ -124,7 +129,7 @@ impl CompactionOperation {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Operation for CompactionOperation {
|
||||
impl Operation<Session, GooseEffect> for CompactionOperation {
|
||||
fn name(&self) -> &'static str {
|
||||
"compaction"
|
||||
}
|
||||
@@ -135,7 +140,7 @@ impl Operation for CompactionOperation {
|
||||
session: &Session,
|
||||
conversation: &Conversation,
|
||||
emit: &Emitter,
|
||||
) -> Result<OperationResult> {
|
||||
) -> Result<OperationResult<GooseEffect>> {
|
||||
match command.command {
|
||||
"clear" => return Self::clear(conversation, emit).await,
|
||||
"compact" => {}
|
||||
@@ -179,7 +184,7 @@ impl Operation for CompactionOperation {
|
||||
emit.message(command).await;
|
||||
let response = emit.message(response).await;
|
||||
yielded_with([
|
||||
StateEffect::ReplaceConversation {
|
||||
GooseEffect::ReplaceConversation {
|
||||
conversation: compacted,
|
||||
usage: Some(usage),
|
||||
},
|
||||
@@ -209,7 +214,7 @@ impl Operation for CompactionOperation {
|
||||
session: &Session,
|
||||
conversation: &Conversation,
|
||||
emit: &Emitter,
|
||||
) -> Result<OperationResult> {
|
||||
) -> Result<OperationResult<GooseEffect>> {
|
||||
if self.manages_own_context {
|
||||
return not_applicable();
|
||||
}
|
||||
@@ -294,7 +299,7 @@ impl Operation for CompactionOperation {
|
||||
"Compaction complete",
|
||||
))
|
||||
.await;
|
||||
applied([StateEffect::ReplaceConversation {
|
||||
applied([GooseEffect::ReplaceConversation {
|
||||
conversation: compacted,
|
||||
usage: Some(usage),
|
||||
}])
|
||||
|
||||
@@ -4,9 +4,9 @@ use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use rmcp::model::Role;
|
||||
|
||||
use crate::agents::state_machine::operation::{
|
||||
applied, messages_since_kickoff, not_applicable, yielded_with, Emitter, Operation,
|
||||
OperationResult, SlashCommand, StateEffect,
|
||||
use crate::agents::state_machine::{
|
||||
applied, messages_since_kickoff, not_applicable, yielded_with, ConversationEffect, Emitter,
|
||||
GooseEffect, Operation, OperationResult, SlashCommand,
|
||||
};
|
||||
use crate::conversation::message::Message;
|
||||
use crate::conversation::Conversation;
|
||||
@@ -15,7 +15,7 @@ use crate::session::Session;
|
||||
pub struct DoctorOperation;
|
||||
|
||||
#[async_trait]
|
||||
impl Operation for DoctorOperation {
|
||||
impl Operation<Session, GooseEffect> for DoctorOperation {
|
||||
fn name(&self) -> &'static str {
|
||||
"doctor"
|
||||
}
|
||||
@@ -26,7 +26,7 @@ impl Operation for DoctorOperation {
|
||||
session: &Session,
|
||||
conversation: &Conversation,
|
||||
emit: &Emitter,
|
||||
) -> Result<OperationResult> {
|
||||
) -> Result<OperationResult<GooseEffect>> {
|
||||
if command.command != "doctor" {
|
||||
return not_applicable();
|
||||
}
|
||||
@@ -55,21 +55,23 @@ impl Operation for DoctorOperation {
|
||||
emit.message(command_message).await;
|
||||
let result = emit.message(result).await;
|
||||
return yielded_with([
|
||||
StateEffect::SetMessageVisibility {
|
||||
ConversationEffect::SetMessageVisibility {
|
||||
message_id,
|
||||
user_visible: true,
|
||||
agent_visible: false,
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
result.into(),
|
||||
]);
|
||||
}
|
||||
|
||||
applied([
|
||||
StateEffect::SetMessageVisibility {
|
||||
ConversationEffect::SetMessageVisibility {
|
||||
message_id,
|
||||
user_visible: true,
|
||||
agent_visible: false,
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
result.with_visibility(false, true).into(),
|
||||
])
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::agents::state_machine::effects::GooseEffect;
|
||||
use crate::agents::state_machine::{
|
||||
messages_since_kickoff, not_applicable, Emitter, Operation, OperationResult,
|
||||
};
|
||||
use crate::conversation::message::Message;
|
||||
use crate::conversation::Conversation;
|
||||
use crate::hooks::{HookContext, HookEvent, HookManager};
|
||||
use crate::session::Session;
|
||||
|
||||
pub struct EntryHookOperation {
|
||||
hook_manager: HookManager,
|
||||
}
|
||||
|
||||
impl EntryHookOperation {
|
||||
pub fn new(hook_manager: HookManager) -> Self {
|
||||
Self { hook_manager }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Operation<Session, GooseEffect> for EntryHookOperation {
|
||||
fn name(&self) -> &'static str {
|
||||
"entry_hook"
|
||||
}
|
||||
|
||||
async fn run(
|
||||
&self,
|
||||
session: &Session,
|
||||
conversation: &Conversation,
|
||||
_emit: &Emitter,
|
||||
) -> Result<OperationResult<GooseEffect>> {
|
||||
let messages = messages_since_kickoff(conversation)?;
|
||||
if messages.iter().any(|message| {
|
||||
message.role == rmcp::model::Role::Assistant
|
||||
&& ((message.is_user_visible() && message.is_agent_visible())
|
||||
|| message.error_kind().is_some())
|
||||
}) {
|
||||
return not_applicable();
|
||||
}
|
||||
|
||||
let messages_before_kickoff =
|
||||
&conversation.messages()[..conversation.len() - messages.len()];
|
||||
if !messages_before_kickoff.iter().any(|message| {
|
||||
message.role == rmcp::model::Role::User
|
||||
&& message.is_user_visible()
|
||||
&& !message.is_tool_response()
|
||||
}) {
|
||||
self.hook_manager
|
||||
.emit(
|
||||
HookEvent::SessionStart,
|
||||
HookContext::new(HookEvent::SessionStart, &session.id)
|
||||
.with_working_dir(session.working_dir.to_string_lossy().to_string()),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
let prompt = messages
|
||||
.first()
|
||||
.map(Message::as_concat_text)
|
||||
.unwrap_or_default();
|
||||
if !prompt.is_empty() {
|
||||
self.hook_manager
|
||||
.emit(
|
||||
HookEvent::UserPromptSubmit,
|
||||
HookContext::new(HookEvent::UserPromptSubmit, &session.id)
|
||||
.with_message(prompt)
|
||||
.with_working_dir(session.working_dir.to_string_lossy().to_string()),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
not_applicable()
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,8 @@
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::agents::state_machine::operation::{
|
||||
use crate::agents::state_machine::effects::GooseEffect;
|
||||
use crate::agents::state_machine::{
|
||||
not_applicable, trailing_error, yielded, Emitter, Operation, OperationResult,
|
||||
};
|
||||
use crate::conversation::Conversation;
|
||||
@@ -12,7 +13,7 @@ use crate::session::Session;
|
||||
pub struct ExitOnErrorOperation;
|
||||
|
||||
#[async_trait]
|
||||
impl Operation for ExitOnErrorOperation {
|
||||
impl Operation<Session, GooseEffect> for ExitOnErrorOperation {
|
||||
fn name(&self) -> &'static str {
|
||||
"exit_on_error"
|
||||
}
|
||||
@@ -22,7 +23,7 @@ impl Operation for ExitOnErrorOperation {
|
||||
_session: &Session,
|
||||
conversation: &Conversation,
|
||||
_emit: &Emitter,
|
||||
) -> Result<OperationResult> {
|
||||
) -> Result<OperationResult<GooseEffect>> {
|
||||
if trailing_error(conversation).is_none() {
|
||||
return not_applicable();
|
||||
}
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::agents::state_machine::operation::{
|
||||
applied, messages_since_kickoff, not_applicable, trailing_error, yielded_with, Emitter,
|
||||
Inference, InferenceInput, Operation, OperationResult, SlashCommand, StateEffect,
|
||||
};
|
||||
use crate::agents::state_machine::ops_unknown_tool::UNCLAIMED_TOOL_ERROR;
|
||||
use crate::agents::state_machine::{
|
||||
applied, messages_since_kickoff, not_applicable, trailing_error, yielded_with,
|
||||
ConversationEffect, Emitter, GooseEffect, Inference, InferenceInput, Operation,
|
||||
OperationResult, SlashCommand,
|
||||
};
|
||||
use crate::agents::{ExtensionManager, PromptManager};
|
||||
use crate::config::GooseMode;
|
||||
use crate::conversation::message::{InferenceMetadata, Message, MessageContent};
|
||||
@@ -182,7 +183,7 @@ impl<'a> InferenceRunner<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
async fn error_outcome(&self, err: &ProviderError, emit: &Emitter) -> Vec<StateEffect> {
|
||||
async fn error_outcome(&self, err: &ProviderError, emit: &Emitter) -> Vec<GooseEffect> {
|
||||
#[cfg(feature = "telemetry")]
|
||||
crate::posthog::emit_error(err.telemetry_type(), &err.to_string());
|
||||
tracing::Span::current().record("error.type", err.telemetry_type());
|
||||
@@ -194,7 +195,7 @@ impl<'a> InferenceRunner<'a> {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Operation for InferenceRunner<'_> {
|
||||
impl Operation<Session, GooseEffect> for InferenceRunner<'_> {
|
||||
fn name(&self) -> &'static str {
|
||||
"llm"
|
||||
}
|
||||
@@ -203,9 +204,9 @@ impl Operation for InferenceRunner<'_> {
|
||||
&self,
|
||||
_session: &Session,
|
||||
conversation: &Conversation,
|
||||
result: OperationResult,
|
||||
result: OperationResult<GooseEffect>,
|
||||
emit: &Emitter,
|
||||
) -> Result<OperationResult> {
|
||||
) -> Result<OperationResult<GooseEffect>> {
|
||||
let mut answered = conversation
|
||||
.messages()
|
||||
.iter()
|
||||
@@ -235,7 +236,9 @@ impl Operation for InferenceRunner<'_> {
|
||||
}
|
||||
if let OperationResult::Applied(step) = &result {
|
||||
for effect in &step.effects {
|
||||
if let StateEffect::AppendMessage(message) = effect {
|
||||
if let GooseEffect::Conversation(ConversationEffect::AppendMessage(message)) =
|
||||
effect
|
||||
{
|
||||
collect(message);
|
||||
}
|
||||
}
|
||||
@@ -273,7 +276,7 @@ impl Operation for InferenceRunner<'_> {
|
||||
session: &Session,
|
||||
conversation: &Conversation,
|
||||
emit: &Emitter,
|
||||
) -> Result<OperationResult> {
|
||||
) -> Result<OperationResult<GooseEffect>> {
|
||||
if command.command != "status" {
|
||||
return not_applicable();
|
||||
}
|
||||
@@ -320,18 +323,19 @@ impl Operation for InferenceRunner<'_> {
|
||||
.await;
|
||||
let response = emit.message(response).await;
|
||||
yielded_with([
|
||||
StateEffect::SetMessageVisibility {
|
||||
ConversationEffect::SetMessageVisibility {
|
||||
message_id,
|
||||
user_visible: true,
|
||||
agent_visible: false,
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
response.into(),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Inference for InferenceRunner<'_> {
|
||||
impl Inference<Session, GooseEffect> for InferenceRunner<'_> {
|
||||
fn applies(&self, conversation: &Conversation) -> bool {
|
||||
let Ok(turn) = messages_since_kickoff(conversation) else {
|
||||
return false;
|
||||
@@ -346,7 +350,7 @@ impl Inference for InferenceRunner<'_> {
|
||||
conversation: &Conversation,
|
||||
mut input: InferenceInput,
|
||||
emit: &Emitter,
|
||||
) -> Result<OperationResult> {
|
||||
) -> Result<OperationResult<GooseEffect>> {
|
||||
let messages = messages_since_kickoff(conversation)?;
|
||||
if trailing_error(conversation).is_some() {
|
||||
return not_applicable();
|
||||
@@ -471,8 +475,8 @@ impl Inference for InferenceRunner<'_> {
|
||||
messages_for_provider.push(event.clone());
|
||||
}
|
||||
let conversation_for_provider = Conversation::new_unvalidated(messages_for_provider);
|
||||
let mut usage_effects: Vec<StateEffect> =
|
||||
turn_context.into_iter().map(StateEffect::from).collect();
|
||||
let mut usage_effects: Vec<GooseEffect> =
|
||||
turn_context.into_iter().map(GooseEffect::from).collect();
|
||||
|
||||
let stream = crate::agents::reply_parts::stream_response_from_provider(
|
||||
self.provider.clone(),
|
||||
@@ -519,7 +523,7 @@ impl Inference for InferenceRunner<'_> {
|
||||
let (msg_opt, usage_opt) = match result {
|
||||
Ok(chunk) => chunk,
|
||||
Err(err) => {
|
||||
usage_effects.extend(accumulator.into_iter().map(StateEffect::from));
|
||||
usage_effects.extend(accumulator.into_iter().map(GooseEffect::from));
|
||||
usage_effects.extend(self.error_outcome(&err, emit).await);
|
||||
return applied(usage_effects);
|
||||
}
|
||||
@@ -527,7 +531,7 @@ impl Inference for InferenceRunner<'_> {
|
||||
if let Some(usage) = usage_opt {
|
||||
let span = tracing::Span::current();
|
||||
record_chat_usage(&span, &usage);
|
||||
usage_effects.push(StateEffect::RecordUsage(usage));
|
||||
usage_effects.push(GooseEffect::RecordUsage(usage));
|
||||
}
|
||||
if let Some(mut chunk) = msg_opt {
|
||||
if let Some(inference) = &inference {
|
||||
@@ -573,7 +577,7 @@ impl Inference for InferenceRunner<'_> {
|
||||
|
||||
let has_recorded_usage = usage_effects
|
||||
.iter()
|
||||
.any(|effect| matches!(effect, StateEffect::RecordUsage(_)));
|
||||
.any(|effect| matches!(effect, GooseEffect::RecordUsage(_)));
|
||||
if !has_recorded_usage {
|
||||
let mut usage = ProviderUsage::new(
|
||||
self.model_config.model_name.clone(),
|
||||
@@ -589,7 +593,7 @@ impl Inference for InferenceRunner<'_> {
|
||||
)
|
||||
.await?;
|
||||
record_chat_usage(&tracing::Span::current(), &usage);
|
||||
usage_effects.push(StateEffect::RecordUsage(usage));
|
||||
usage_effects.push(GooseEffect::RecordUsage(usage));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Ends the turn when the agent has used its autonomous turn budget.
|
||||
|
||||
use crate::agents::state_machine::operation::{
|
||||
use crate::agents::state_machine::effects::GooseEffect;
|
||||
use crate::agents::state_machine::{
|
||||
assistant_turn_count, messages_since_kickoff, not_applicable, yielded_with, Emitter, Operation,
|
||||
OperationResult,
|
||||
};
|
||||
@@ -33,7 +34,7 @@ impl MaxTurnsOperation {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Operation for MaxTurnsOperation {
|
||||
impl Operation<Session, GooseEffect> for MaxTurnsOperation {
|
||||
fn name(&self) -> &'static str {
|
||||
"max_turns"
|
||||
}
|
||||
@@ -54,7 +55,7 @@ impl Operation for MaxTurnsOperation {
|
||||
_session: &Session,
|
||||
conversation: &Conversation,
|
||||
emit: &Emitter,
|
||||
) -> Result<OperationResult> {
|
||||
) -> Result<OperationResult<GooseEffect>> {
|
||||
let messages = messages_since_kickoff(conversation)?;
|
||||
if assistant_turn_count(messages) < self.max_turns {
|
||||
return not_applicable();
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::agents::state_machine::effects::GooseEffect;
|
||||
use crate::agents::state_machine::Operation;
|
||||
use crate::conversation::Conversation;
|
||||
use crate::session::Session;
|
||||
@@ -10,7 +11,7 @@ use crate::session::Session;
|
||||
pub struct ProjectOperation;
|
||||
|
||||
#[async_trait]
|
||||
impl Operation for ProjectOperation {
|
||||
impl Operation<Session, GooseEffect> for ProjectOperation {
|
||||
fn name(&self) -> &'static str {
|
||||
"project"
|
||||
}
|
||||
|
||||
@@ -10,13 +10,13 @@ use tracing_futures::Instrument;
|
||||
use crate::agents::final_output_tool::{
|
||||
FinalOutputTool, FINAL_OUTPUT_CONTINUATION_MESSAGE, FINAL_OUTPUT_TOOL_NAME,
|
||||
};
|
||||
use crate::agents::state_machine::operation::{
|
||||
applied, ends_turn, last_effective_role, messages_since_kickoff, not_applicable, yielded_with,
|
||||
Emitter, Operation, OperationResult, SlashCommand, StateEffect,
|
||||
};
|
||||
use crate::agents::state_machine::ops_toolcalling::{
|
||||
pending_tool_requests, tool_span, ToolDisposition,
|
||||
};
|
||||
use crate::agents::state_machine::{
|
||||
applied, ends_turn, last_effective_role, messages_since_kickoff, not_applicable, yielded_with,
|
||||
ConversationEffect, Emitter, GooseEffect, Operation, OperationResult, SlashCommand,
|
||||
};
|
||||
use crate::conversation::message::{Message, MessageContent};
|
||||
use crate::conversation::{Conversation, EffectiveRole};
|
||||
use crate::session::Session;
|
||||
@@ -77,7 +77,7 @@ impl RecipeOperation {
|
||||
conversation: &Conversation,
|
||||
message: String,
|
||||
emit: &Emitter,
|
||||
) -> Result<OperationResult> {
|
||||
) -> Result<OperationResult<GooseEffect>> {
|
||||
let command = messages_since_kickoff(conversation)?
|
||||
.first()
|
||||
.cloned()
|
||||
@@ -93,18 +93,19 @@ impl RecipeOperation {
|
||||
emit.message(command).await;
|
||||
let response = emit.message(response).await;
|
||||
yielded_with([
|
||||
StateEffect::SetMessageVisibility {
|
||||
ConversationEffect::SetMessageVisibility {
|
||||
message_id,
|
||||
user_visible: true,
|
||||
agent_visible: false,
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
response.into(),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Operation for RecipeOperation {
|
||||
impl Operation<Session, GooseEffect> for RecipeOperation {
|
||||
fn name(&self) -> &'static str {
|
||||
"recipe"
|
||||
}
|
||||
@@ -115,7 +116,7 @@ impl Operation for RecipeOperation {
|
||||
_session: &Session,
|
||||
conversation: &Conversation,
|
||||
emit: &Emitter,
|
||||
) -> Result<OperationResult> {
|
||||
) -> Result<OperationResult<GooseEffect>> {
|
||||
let (recipe, prompt) = match crate::slash_commands::recipe_slash_command::resolve_command(
|
||||
command.command,
|
||||
command.params_str,
|
||||
@@ -144,12 +145,13 @@ impl Operation for RecipeOperation {
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("Persisted slash command message has no id"))?;
|
||||
applied([
|
||||
StateEffect::SetMessageVisibility {
|
||||
ConversationEffect::SetMessageVisibility {
|
||||
message_id,
|
||||
user_visible: true,
|
||||
agent_visible: false,
|
||||
},
|
||||
StateEffect::SetRecipe(Box::new(Some(recipe))),
|
||||
}
|
||||
.into(),
|
||||
GooseEffect::SetRecipe(Box::new(Some(recipe))),
|
||||
Message::user()
|
||||
.with_text(prompt)
|
||||
.with_visibility(false, true)
|
||||
@@ -182,7 +184,7 @@ impl Operation for RecipeOperation {
|
||||
session: &Session,
|
||||
conversation: &Conversation,
|
||||
emit: &Emitter,
|
||||
) -> Result<OperationResult> {
|
||||
) -> Result<OperationResult<GooseEffect>> {
|
||||
let Some(mut final_output) = Self::final_output(session)? else {
|
||||
return not_applicable();
|
||||
};
|
||||
|
||||
@@ -7,9 +7,9 @@ use std::time::Duration;
|
||||
use crate::agents::retry::{
|
||||
execute_on_failure_command_with_timeout, execute_success_checks_with_timeout,
|
||||
};
|
||||
use crate::agents::state_machine::operation::{
|
||||
applied, ends_turn, messages_since_kickoff, not_applicable, yielded_with, Emitter, Operation,
|
||||
OperationResult, SlashCommand, StateEffect,
|
||||
use crate::agents::state_machine::{
|
||||
applied, ends_turn, messages_since_kickoff, not_applicable, yielded_with, ConversationEffect,
|
||||
Emitter, GooseEffect, Operation, OperationResult, SlashCommand,
|
||||
};
|
||||
use crate::agents::types::RetryConfig;
|
||||
use crate::conversation::message::{Message, MessageErrorKind, SystemNotificationType};
|
||||
@@ -83,7 +83,7 @@ impl<'a> RetryOperation<'a> {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Operation for RetryOperation<'_> {
|
||||
impl Operation<Session, GooseEffect> for RetryOperation<'_> {
|
||||
fn name(&self) -> &'static str {
|
||||
"retry"
|
||||
}
|
||||
@@ -94,7 +94,7 @@ impl Operation for RetryOperation<'_> {
|
||||
_session: &Session,
|
||||
conversation: &Conversation,
|
||||
emit: &Emitter,
|
||||
) -> Result<OperationResult> {
|
||||
) -> Result<OperationResult<GooseEffect>> {
|
||||
let target = match command.command {
|
||||
"goal" => &self.goal,
|
||||
"grind" => &self.grind,
|
||||
@@ -152,11 +152,12 @@ impl Operation for RetryOperation<'_> {
|
||||
let response = emit.message(response).await;
|
||||
|
||||
let mut effects = vec![
|
||||
StateEffect::SetMessageVisibility {
|
||||
ConversationEffect::SetMessageVisibility {
|
||||
message_id,
|
||||
user_visible: true,
|
||||
agent_visible: false,
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
response.into(),
|
||||
];
|
||||
if starts_turn {
|
||||
@@ -179,7 +180,7 @@ impl Operation for RetryOperation<'_> {
|
||||
session: &Session,
|
||||
conversation: &Conversation,
|
||||
emit: &Emitter,
|
||||
) -> Result<OperationResult> {
|
||||
) -> Result<OperationResult<GooseEffect>> {
|
||||
let messages = messages_since_kickoff(conversation)?;
|
||||
if !ends_turn(messages) {
|
||||
return not_applicable();
|
||||
|
||||
@@ -10,13 +10,13 @@ use schemars::{schema_for, JsonSchema};
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::agents::state_machine::operation::{
|
||||
applied, messages_since_kickoff, not_applicable, yielded_with, Emitter, Operation,
|
||||
OperationResult, SlashCommand, StateEffect,
|
||||
};
|
||||
use crate::agents::state_machine::ops_toolcalling::{
|
||||
pending_tool_requests, tool_span, ToolDisposition,
|
||||
};
|
||||
use crate::agents::state_machine::{
|
||||
applied, messages_since_kickoff, not_applicable, yielded_with, ConversationEffect, Emitter,
|
||||
GooseEffect, Operation, OperationResult, SlashCommand,
|
||||
};
|
||||
use crate::agents::tool_execution::{CHAT_MODE_TOOL_SKIPPED_RESPONSE, DECLINED_RESPONSE};
|
||||
use crate::config::GooseMode;
|
||||
use crate::conversation::message::Message;
|
||||
@@ -206,7 +206,7 @@ impl SkillOperation {
|
||||
conversation: &Conversation,
|
||||
message: String,
|
||||
emit: &Emitter,
|
||||
) -> Result<OperationResult> {
|
||||
) -> Result<OperationResult<GooseEffect>> {
|
||||
let command = messages_since_kickoff(conversation)?
|
||||
.first()
|
||||
.cloned()
|
||||
@@ -222,18 +222,19 @@ impl SkillOperation {
|
||||
emit.message(command).await;
|
||||
let response = emit.message(response).await;
|
||||
yielded_with([
|
||||
StateEffect::SetMessageVisibility {
|
||||
ConversationEffect::SetMessageVisibility {
|
||||
message_id,
|
||||
user_visible: true,
|
||||
agent_visible: false,
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
response.into(),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Operation for SkillOperation {
|
||||
impl Operation<Session, GooseEffect> for SkillOperation {
|
||||
fn name(&self) -> &'static str {
|
||||
"skills"
|
||||
}
|
||||
@@ -244,7 +245,7 @@ impl Operation for SkillOperation {
|
||||
session: &Session,
|
||||
conversation: &Conversation,
|
||||
emit: &Emitter,
|
||||
) -> Result<OperationResult> {
|
||||
) -> Result<OperationResult<GooseEffect>> {
|
||||
if command.command == "skills" {
|
||||
return Self::command_response(
|
||||
conversation,
|
||||
@@ -273,11 +274,12 @@ impl Operation for SkillOperation {
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("Persisted slash command message has no id"))?;
|
||||
applied([
|
||||
StateEffect::SetMessageVisibility {
|
||||
ConversationEffect::SetMessageVisibility {
|
||||
message_id,
|
||||
user_visible: true,
|
||||
agent_visible: false,
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
Message::user()
|
||||
.with_text(prompt)
|
||||
.with_visibility(false, true)
|
||||
@@ -305,7 +307,7 @@ impl Operation for SkillOperation {
|
||||
session: &Session,
|
||||
conversation: &Conversation,
|
||||
emit: &Emitter,
|
||||
) -> Result<OperationResult> {
|
||||
) -> Result<OperationResult<GooseEffect>> {
|
||||
let pending: Vec<_> = pending_tool_requests(messages_since_kickoff(conversation)?)
|
||||
.into_iter()
|
||||
.filter(|(request, _)| {
|
||||
|
||||
@@ -5,14 +5,15 @@ use std::sync::Arc;
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::agents::state_machine::operation::{
|
||||
use crate::agents::state_machine::effects::GooseEffect;
|
||||
use crate::agents::state_machine::{
|
||||
messages_since_kickoff, not_applicable, Emitter, Operation, OperationResult, SlashCommand,
|
||||
};
|
||||
use crate::conversation::Conversation;
|
||||
use crate::session::Session;
|
||||
|
||||
pub struct SlashCommandOperation<'a> {
|
||||
operations: Vec<Arc<dyn Operation + 'a>>,
|
||||
operations: Vec<Arc<dyn Operation<Session, GooseEffect> + 'a>>,
|
||||
}
|
||||
|
||||
fn parse_slash_command(message: &str) -> Option<SlashCommand<'_>> {
|
||||
@@ -35,13 +36,13 @@ fn parse_slash_command(message: &str) -> Option<SlashCommand<'_>> {
|
||||
}
|
||||
|
||||
impl<'a> SlashCommandOperation<'a> {
|
||||
pub fn new(operations: Vec<Arc<dyn Operation + 'a>>) -> Self {
|
||||
pub fn new(operations: Vec<Arc<dyn Operation<Session, GooseEffect> + 'a>>) -> Self {
|
||||
Self { operations }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Operation for SlashCommandOperation<'_> {
|
||||
impl Operation<Session, GooseEffect> for SlashCommandOperation<'_> {
|
||||
fn name(&self) -> &'static str {
|
||||
"slash_command"
|
||||
}
|
||||
@@ -51,7 +52,7 @@ impl Operation for SlashCommandOperation<'_> {
|
||||
session: &Session,
|
||||
conversation: &Conversation,
|
||||
emit: &Emitter,
|
||||
) -> Result<OperationResult> {
|
||||
) -> Result<OperationResult<GooseEffect>> {
|
||||
let messages = messages_since_kickoff(conversation)?;
|
||||
let Some(user_message) = messages.first() else {
|
||||
return not_applicable();
|
||||
|
||||
@@ -7,7 +7,8 @@ use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::agents::state_machine::operation::{
|
||||
use crate::agents::state_machine::effects::GooseEffect;
|
||||
use crate::agents::state_machine::{
|
||||
applied, ends_turn, last_effective_role, messages_since_kickoff, not_applicable, Emitter,
|
||||
Operation, OperationResult,
|
||||
};
|
||||
@@ -33,7 +34,7 @@ impl SteerOperation {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Operation for SteerOperation {
|
||||
impl Operation<Session, GooseEffect> for SteerOperation {
|
||||
fn name(&self) -> &'static str {
|
||||
"steer"
|
||||
}
|
||||
@@ -43,7 +44,7 @@ impl Operation for SteerOperation {
|
||||
session: &Session,
|
||||
conversation: &Conversation,
|
||||
emit: &Emitter,
|
||||
) -> Result<OperationResult> {
|
||||
) -> Result<OperationResult<GooseEffect>> {
|
||||
let messages = messages_since_kickoff(conversation)?;
|
||||
let between_turns =
|
||||
ends_turn(messages) || last_effective_role(messages)? == EffectiveRole::Tool;
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::agents::state_machine::operation::{
|
||||
use crate::agents::state_machine::effects::GooseEffect;
|
||||
use crate::agents::state_machine::{
|
||||
applied, ends_turn, messages_since_kickoff, not_applicable, yielded, yielded_with, Emitter,
|
||||
Operation, OperationResult,
|
||||
};
|
||||
@@ -57,7 +58,7 @@ impl StopHookOperation {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Operation for StopHookOperation {
|
||||
impl Operation<Session, GooseEffect> for StopHookOperation {
|
||||
fn name(&self) -> &'static str {
|
||||
"stop_hook"
|
||||
}
|
||||
@@ -67,7 +68,7 @@ impl Operation for StopHookOperation {
|
||||
session: &Session,
|
||||
conversation: &Conversation,
|
||||
emit: &Emitter,
|
||||
) -> Result<OperationResult> {
|
||||
) -> Result<OperationResult<GooseEffect>> {
|
||||
let messages = messages_since_kickoff(conversation)?;
|
||||
if !ends_turn(messages) {
|
||||
return not_applicable();
|
||||
|
||||
@@ -5,9 +5,9 @@ use std::collections::{HashMap, HashSet};
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::agents::state_machine::operation::{
|
||||
applied, messages_since_kickoff, not_applicable, Emitter, Operation, OperationResult,
|
||||
StateEffect,
|
||||
use crate::agents::state_machine::{
|
||||
applied, messages_since_kickoff, not_applicable, ConversationEffect, Emitter, GooseEffect,
|
||||
Operation, OperationResult,
|
||||
};
|
||||
use crate::config::permission::PermissionLevel;
|
||||
use crate::config::GooseMode;
|
||||
@@ -40,7 +40,7 @@ impl<'a> ToolApprovalOperation<'a> {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Operation for ToolApprovalOperation<'_> {
|
||||
impl Operation<Session, GooseEffect> for ToolApprovalOperation<'_> {
|
||||
fn name(&self) -> &'static str {
|
||||
"tool_approval"
|
||||
}
|
||||
@@ -50,7 +50,7 @@ impl Operation for ToolApprovalOperation<'_> {
|
||||
session: &Session,
|
||||
conversation: &Conversation,
|
||||
emit: &Emitter,
|
||||
) -> Result<OperationResult> {
|
||||
) -> Result<OperationResult<GooseEffect>> {
|
||||
let goose_mode = *self.goose_mode.lock().await;
|
||||
if goose_mode == GooseMode::Chat {
|
||||
return not_applicable();
|
||||
@@ -259,9 +259,10 @@ fn permission_allows(permission: &Permission) -> bool {
|
||||
matches!(permission, Permission::AllowOnce | Permission::AlwaysAllow)
|
||||
}
|
||||
|
||||
fn mark_executable(tool_call_id: &str, executable: bool) -> StateEffect {
|
||||
StateEffect::PatchToolRequestMeta {
|
||||
fn mark_executable(tool_call_id: &str, executable: bool) -> GooseEffect {
|
||||
ConversationEffect::PatchToolRequestMeta {
|
||||
tool_call_id: tool_call_id.to_string(),
|
||||
patch: serde_json::json!({ TOOL_EXECUTABLE_KEY: executable }),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@ use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use tracing_futures::Instrument;
|
||||
|
||||
use crate::agents::state_machine::operation::{
|
||||
applied, messages_since_kickoff, not_applicable, Emitter, Operation, OperationResult,
|
||||
StateEffect,
|
||||
};
|
||||
use crate::agents::state_machine::ops_llm::chat_span;
|
||||
use crate::agents::state_machine::{
|
||||
applied, messages_since_kickoff, not_applicable, ConversationEffect, Emitter, GooseEffect,
|
||||
Operation, OperationResult,
|
||||
};
|
||||
use crate::context_mgmt::{summarize_tool_call, tool_ids_to_summarize};
|
||||
use crate::conversation::message::MessageContent;
|
||||
use crate::conversation::Conversation;
|
||||
@@ -42,7 +42,7 @@ impl ToolPairCompactionOperation {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Operation for ToolPairCompactionOperation {
|
||||
impl Operation<Session, GooseEffect> for ToolPairCompactionOperation {
|
||||
fn name(&self) -> &'static str {
|
||||
"tool_pair_compaction"
|
||||
}
|
||||
@@ -52,7 +52,7 @@ impl Operation for ToolPairCompactionOperation {
|
||||
session: &Session,
|
||||
conversation: &Conversation,
|
||||
_emit: &Emitter,
|
||||
) -> Result<OperationResult> {
|
||||
) -> Result<OperationResult<GooseEffect>> {
|
||||
if !self.enabled {
|
||||
return not_applicable();
|
||||
}
|
||||
@@ -66,7 +66,7 @@ impl Operation for ToolPairCompactionOperation {
|
||||
if tool_ids.is_empty() {
|
||||
return not_applicable();
|
||||
}
|
||||
let mut effects: Vec<StateEffect> = Vec::new();
|
||||
let mut effects: Vec<GooseEffect> = Vec::new();
|
||||
let mut hidden_messages: std::collections::HashSet<String> = Default::default();
|
||||
for tool_id in tool_ids {
|
||||
let pair: Vec<_> = conversation
|
||||
@@ -146,11 +146,14 @@ impl Operation for ToolPairCompactionOperation {
|
||||
continue;
|
||||
};
|
||||
hidden_messages.insert(message_id.clone());
|
||||
effects.push(StateEffect::SetMessageVisibility {
|
||||
message_id,
|
||||
user_visible: message.is_user_visible(),
|
||||
agent_visible: false,
|
||||
});
|
||||
effects.push(
|
||||
ConversationEffect::SetMessageVisibility {
|
||||
message_id,
|
||||
user_visible: message.is_user_visible(),
|
||||
agent_visible: false,
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
effects.push(summary.into());
|
||||
}
|
||||
|
||||
@@ -9,11 +9,11 @@ use rmcp::model::{CallToolRequestParams, CallToolResult, ContentBlock, ErrorData
|
||||
|
||||
use crate::agents::extension_manager::ExtensionManager;
|
||||
use crate::agents::platform_extensions::MANAGE_EXTENSIONS_TOOL_NAME_COMPLETE;
|
||||
use crate::agents::state_machine::operation::{
|
||||
applied, messages_since_kickoff, not_applicable, yielded_with, Emitter, Operation,
|
||||
OperationResult, SlashCommand, StateEffect,
|
||||
};
|
||||
use crate::agents::state_machine::ops_tool_approval::request_executable;
|
||||
use crate::agents::state_machine::{
|
||||
applied, messages_since_kickoff, not_applicable, yielded_with, ConversationEffect, Emitter,
|
||||
GooseEffect, Operation, OperationResult, SlashCommand,
|
||||
};
|
||||
use crate::agents::tool_execution::{
|
||||
tool_stream, ToolCallResult, ToolStreamItem, CHAT_MODE_TOOL_SKIPPED_RESPONSE, DECLINED_RESPONSE,
|
||||
};
|
||||
@@ -276,19 +276,19 @@ impl<'a> ToolExecutionOperation<'a> {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn extension_state_effect(&self, session: &Session) -> Result<StateEffect> {
|
||||
async fn extension_state_effect(&self, session: &Session) -> Result<GooseEffect> {
|
||||
let extension_configs = self.extension_manager.get_extension_configs().await;
|
||||
let extensions_state = EnabledExtensionsState::new(extension_configs);
|
||||
let mut extension_data = session.extension_data.clone();
|
||||
extensions_state.to_extension_data(&mut extension_data)?;
|
||||
Ok(StateEffect::SetExtensionData(extension_data))
|
||||
Ok(GooseEffect::SetExtensionData(extension_data))
|
||||
}
|
||||
|
||||
async fn command_response(
|
||||
conversation: &Conversation,
|
||||
message: String,
|
||||
emit: &Emitter,
|
||||
) -> Result<OperationResult> {
|
||||
) -> Result<OperationResult<GooseEffect>> {
|
||||
let command = messages_since_kickoff(conversation)?
|
||||
.first()
|
||||
.cloned()
|
||||
@@ -304,11 +304,12 @@ impl<'a> ToolExecutionOperation<'a> {
|
||||
emit.message(command).await;
|
||||
let response = emit.message(response).await;
|
||||
yielded_with([
|
||||
StateEffect::SetMessageVisibility {
|
||||
ConversationEffect::SetMessageVisibility {
|
||||
message_id,
|
||||
user_visible: true,
|
||||
agent_visible: false,
|
||||
},
|
||||
}
|
||||
.into(),
|
||||
response.into(),
|
||||
])
|
||||
}
|
||||
@@ -319,7 +320,7 @@ impl<'a> ToolExecutionOperation<'a> {
|
||||
session: &Session,
|
||||
conversation: &Conversation,
|
||||
emit: &Emitter,
|
||||
) -> Result<OperationResult> {
|
||||
) -> Result<OperationResult<GooseEffect>> {
|
||||
let prompts = match self
|
||||
.extension_manager
|
||||
.list_prompts(&session.id, emit.cancel_token().clone())
|
||||
@@ -373,7 +374,7 @@ impl<'a> ToolExecutionOperation<'a> {
|
||||
session: &Session,
|
||||
conversation: &Conversation,
|
||||
emit: &Emitter,
|
||||
) -> Result<OperationResult> {
|
||||
) -> Result<OperationResult<GooseEffect>> {
|
||||
let params: Vec<_> = command.params_str.split_whitespace().collect();
|
||||
let Some(prompt_name) = params.first() else {
|
||||
return Self::command_response(
|
||||
@@ -462,11 +463,12 @@ impl<'a> ToolExecutionOperation<'a> {
|
||||
.id
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("Persisted slash command message has no id"))?;
|
||||
let mut effects = vec![StateEffect::SetMessageVisibility {
|
||||
let mut effects = vec![ConversationEffect::SetMessageVisibility {
|
||||
message_id,
|
||||
user_visible: true,
|
||||
agent_visible: false,
|
||||
}];
|
||||
}
|
||||
.into()];
|
||||
for (index, prompt_message) in result.messages.into_iter().enumerate() {
|
||||
let message = Message::from(prompt_message);
|
||||
let expected_role = if index % 2 == 0 {
|
||||
@@ -573,7 +575,7 @@ fn approval_denied(permission: Option<&crate::permission::Permission>) -> bool {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Operation for ToolExecutionOperation<'_> {
|
||||
impl Operation<Session, GooseEffect> for ToolExecutionOperation<'_> {
|
||||
fn name(&self) -> &'static str {
|
||||
"tool_execution"
|
||||
}
|
||||
@@ -584,7 +586,7 @@ impl Operation for ToolExecutionOperation<'_> {
|
||||
session: &Session,
|
||||
conversation: &Conversation,
|
||||
emit: &Emitter,
|
||||
) -> Result<OperationResult> {
|
||||
) -> Result<OperationResult<GooseEffect>> {
|
||||
match command.command {
|
||||
"prompts" => {
|
||||
self.list_prompts(command, session, conversation, emit)
|
||||
@@ -676,7 +678,7 @@ impl Operation for ToolExecutionOperation<'_> {
|
||||
session: &Session,
|
||||
conversation: &Conversation,
|
||||
emit: &Emitter,
|
||||
) -> Result<OperationResult> {
|
||||
) -> Result<OperationResult<GooseEffect>> {
|
||||
let mut pending = pending_tool_requests(messages_since_kickoff(conversation)?);
|
||||
if pending.is_empty() {
|
||||
return not_applicable();
|
||||
|
||||
@@ -4,12 +4,13 @@ use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use rmcp::model::{CallToolResult, ContentBlock};
|
||||
|
||||
use crate::agents::state_machine::operation::{
|
||||
applied, messages_since_kickoff, not_applicable, Emitter, Operation, OperationResult,
|
||||
};
|
||||
use crate::agents::state_machine::effects::GooseEffect;
|
||||
use crate::agents::state_machine::ops_toolcalling::{
|
||||
pending_tool_requests, tool_span, ToolDisposition,
|
||||
};
|
||||
use crate::agents::state_machine::{
|
||||
applied, messages_since_kickoff, not_applicable, Emitter, Operation, OperationResult,
|
||||
};
|
||||
use crate::conversation::message::Message;
|
||||
use crate::conversation::Conversation;
|
||||
use crate::session::Session;
|
||||
@@ -19,7 +20,7 @@ pub(super) const UNCLAIMED_TOOL_ERROR: &str = "goose.unclaimed_tool";
|
||||
pub struct UnknownToolOperation;
|
||||
|
||||
#[async_trait]
|
||||
impl Operation for UnknownToolOperation {
|
||||
impl Operation<Session, GooseEffect> for UnknownToolOperation {
|
||||
fn name(&self) -> &'static str {
|
||||
"unknown_tool"
|
||||
}
|
||||
@@ -29,7 +30,7 @@ impl Operation for UnknownToolOperation {
|
||||
session: &Session,
|
||||
conversation: &Conversation,
|
||||
emit: &Emitter,
|
||||
) -> Result<OperationResult> {
|
||||
) -> Result<OperationResult<GooseEffect>> {
|
||||
let pending = pending_tool_requests(messages_since_kickoff(conversation)?);
|
||||
if pending.is_empty() {
|
||||
return not_applicable();
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::agents::state_machine::effects::GooseEffect;
|
||||
use crate::agents::state_machine::machine::{
|
||||
EffectHandler, EffectUsage, MachineSession, SessionLoader,
|
||||
};
|
||||
use crate::agents::state_machine::operation::{ConversationEffect, Emitter, MachineEffect};
|
||||
use crate::agents::state_machine::usage;
|
||||
use crate::agents::AgentEvent;
|
||||
use crate::conversation::Conversation;
|
||||
use crate::session::{Session, SessionManager};
|
||||
|
||||
impl MachineSession for Session {
|
||||
fn id(&self) -> &str {
|
||||
&self.id
|
||||
}
|
||||
fn conversation(&self) -> Option<&Conversation> {
|
||||
self.conversation.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl SessionLoader<Session> for SessionManager {
|
||||
async fn load(&self, session_id: &str) -> Result<Session> {
|
||||
self.get_session(session_id, true).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EffectHandler<Session, GooseEffect> for SessionManager {
|
||||
async fn apply_effects(
|
||||
&self,
|
||||
session: &Session,
|
||||
effects: &mut [GooseEffect],
|
||||
emit: &Emitter,
|
||||
) -> Result<()> {
|
||||
for effect in effects.iter_mut() {
|
||||
effect.ensure_message_ids();
|
||||
}
|
||||
usage::enrich(session, effects);
|
||||
|
||||
for effect in effects.iter_mut() {
|
||||
match effect {
|
||||
GooseEffect::Conversation(ConversationEffect::AppendMessage(message)) => {
|
||||
self.add_message(&session.id, message).await?;
|
||||
}
|
||||
GooseEffect::Conversation(ConversationEffect::ReplaceConversation(
|
||||
conversation,
|
||||
)) => {
|
||||
self.replace_conversation(&session.id, conversation).await?;
|
||||
self.update(&session.id)
|
||||
.usage(usage::estimate_context(conversation).await?)
|
||||
.apply()
|
||||
.await?;
|
||||
}
|
||||
GooseEffect::ReplaceConversation {
|
||||
conversation,
|
||||
usage: replacement_usage,
|
||||
} => {
|
||||
if let Some(provider_usage) = replacement_usage {
|
||||
usage::record(self, session, provider_usage, true).await?;
|
||||
}
|
||||
self.replace_conversation(&session.id, conversation).await?;
|
||||
self.update(&session.id)
|
||||
.usage(usage::estimate_context(conversation).await?)
|
||||
.apply()
|
||||
.await?;
|
||||
}
|
||||
GooseEffect::Conversation(ConversationEffect::PatchToolRequestMeta {
|
||||
tool_call_id,
|
||||
patch,
|
||||
}) => {
|
||||
self.update_tool_request_meta(&session.id, tool_call_id, patch.clone())
|
||||
.await?;
|
||||
}
|
||||
GooseEffect::Conversation(ConversationEffect::SetMessageVisibility {
|
||||
message_id,
|
||||
user_visible,
|
||||
agent_visible,
|
||||
}) => {
|
||||
self.update_message_metadata(&session.id, message_id, |mut metadata| {
|
||||
metadata.user_visible = *user_visible;
|
||||
metadata.agent_visible = *agent_visible;
|
||||
metadata
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
GooseEffect::SetRecipe(recipe) => {
|
||||
self.update(&session.id)
|
||||
.recipe(recipe.as_ref().clone())
|
||||
.apply()
|
||||
.await?;
|
||||
}
|
||||
GooseEffect::SetExtensionData(extension_data) => {
|
||||
self.update(&session.id)
|
||||
.extension_data(extension_data.clone())
|
||||
.apply()
|
||||
.await?;
|
||||
}
|
||||
GooseEffect::RecordUsage(provider_usage) => {
|
||||
usage::record(self, session, provider_usage, false).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for effect in effects {
|
||||
match effect {
|
||||
GooseEffect::Conversation(ConversationEffect::AppendMessage(message)) => {
|
||||
if let Some(usage) = message
|
||||
.metadata
|
||||
.usage
|
||||
.as_deref()
|
||||
.filter(|_| !message.user_visible_content().content.is_empty())
|
||||
.cloned()
|
||||
{
|
||||
emit.emit(AgentEvent::MessageUsage {
|
||||
message_id: message.id.clone(),
|
||||
usage,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
GooseEffect::Conversation(ConversationEffect::ReplaceConversation(
|
||||
conversation,
|
||||
))
|
||||
| GooseEffect::ReplaceConversation { conversation, .. } => {
|
||||
emit.emit(AgentEvent::HistoryReplaced(conversation.clone()))
|
||||
.await;
|
||||
}
|
||||
GooseEffect::RecordUsage(usage) => {
|
||||
emit.emit(AgentEvent::Usage(usage.clone())).await
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl EffectUsage<GooseEffect> for SessionManager {
|
||||
fn usage(
|
||||
&self,
|
||||
effect: &GooseEffect,
|
||||
) -> Option<goose_providers::conversation::token_usage::Usage> {
|
||||
match effect {
|
||||
GooseEffect::RecordUsage(usage)
|
||||
| GooseEffect::ReplaceConversation {
|
||||
usage: Some(usage), ..
|
||||
} => Some(usage.usage),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn run(
|
||||
machine: &crate::agents::state_machine::StateMachine<'_, Session, GooseEffect>,
|
||||
runtime: &SessionManager,
|
||||
session_id: &str,
|
||||
emit: &Emitter,
|
||||
) -> Result<Session> {
|
||||
let entry_session = runtime.load(session_id).await?;
|
||||
if let Some(input) = entry_session
|
||||
.conversation()
|
||||
.and_then(|conversation| {
|
||||
crate::agents::state_machine::messages_since_kickoff(conversation).ok()
|
||||
})
|
||||
.and_then(|messages| messages.first())
|
||||
.map(crate::conversation::message::Message::user_visible_content)
|
||||
.map(|message| message.as_concat_text())
|
||||
.filter(|text| !text.is_empty())
|
||||
{
|
||||
tracing::Span::current().record("trace_input", input.as_str());
|
||||
}
|
||||
|
||||
let mut turn_usage = goose_providers::conversation::token_usage::Usage::default();
|
||||
loop {
|
||||
let session = runtime.load(session_id).await?;
|
||||
let Some(mut result) = machine.step(&session, emit).await? else {
|
||||
break;
|
||||
};
|
||||
tracing::debug!(target: "goose::state_machine", step = result.applied_step, "applied step");
|
||||
for effect in &result.effects {
|
||||
if let Some(usage) = runtime.usage(effect) {
|
||||
turn_usage += usage;
|
||||
}
|
||||
}
|
||||
machine.apply(runtime, &session, &mut result, emit).await?;
|
||||
if result.yield_to_client {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let session = runtime.load(session_id).await?;
|
||||
let last_assistant_text = session
|
||||
.conversation()
|
||||
.and_then(|conversation| {
|
||||
crate::agents::state_machine::messages_since_kickoff(conversation).ok()
|
||||
})
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.rev()
|
||||
.filter(|message| message.role == rmcp::model::Role::Assistant)
|
||||
.map(crate::conversation::message::Message::user_visible_content)
|
||||
.map(|message| message.as_concat_text())
|
||||
.find(|text| !text.is_empty())
|
||||
.unwrap_or_default();
|
||||
if !last_assistant_text.is_empty() {
|
||||
let span = tracing::Span::current();
|
||||
span.record("trace_output", last_assistant_text.as_str());
|
||||
if crate::agents::gen_ai_telemetry::capture_message_content() {
|
||||
let output = crate::agents::gen_ai_telemetry::simple_output_json(&last_assistant_text);
|
||||
span.record("gen_ai.output.messages", output.as_str());
|
||||
}
|
||||
}
|
||||
crate::agents::gen_ai_telemetry::record_usage(&tracing::Span::current(), &turn_usage);
|
||||
Ok(session)
|
||||
}
|
||||
@@ -81,10 +81,11 @@ async fn proactive_and_manual_compaction_continue_with_replaced_usage() -> Resul
|
||||
state_machine::StateMachine::new(Vec::new(), tokio_util::sync::CancellationToken::new());
|
||||
let (tx, _rx) = tokio::sync::mpsc::channel(4);
|
||||
let emit = state_machine::Emitter::new(tx, tokio_util::sync::CancellationToken::new());
|
||||
let apply = async |effects: Vec<state_machine::StateEffect>| -> Result<()> {
|
||||
let apply = async |effects: Vec<state_machine::GooseEffect>| -> Result<()> {
|
||||
let session = pipeline.session().await?;
|
||||
let mut result = state_machine::StepResult {
|
||||
effects,
|
||||
applied_step: None,
|
||||
yield_to_client: false,
|
||||
};
|
||||
machine
|
||||
@@ -112,7 +113,7 @@ async fn proactive_and_manual_compaction_continue_with_replaced_usage() -> Resul
|
||||
);
|
||||
apply(vec![
|
||||
replacement.into(),
|
||||
state_machine::StateEffect::RecordUsage(usage),
|
||||
state_machine::GooseEffect::RecordUsage(usage),
|
||||
Message::assistant()
|
||||
.with_text("response after replacement")
|
||||
.into(),
|
||||
|
||||
@@ -15,11 +15,11 @@ use crate::agents::extension_manager::{ExtensionManager, ExtensionManagerCapabil
|
||||
use crate::agents::mcp_client::McpClientTrait;
|
||||
use crate::agents::prompt_manager::PromptManager;
|
||||
use crate::agents::state_machine::{
|
||||
BangShellOperation, CompactionOperation, DoctorOperation, Emitter, ExitOnErrorOperation,
|
||||
InferenceRunner, MaxTurnsOperation, Operation, ProjectOperation, RecipeOperation,
|
||||
RetryOperation, SkillOperation, SlashCommandOperation, StateMachine, SteerOperation,
|
||||
SteerQueue, Step, StopHookOperation, ToolApprovalOperation, ToolExecutionOperation,
|
||||
ToolPairCompactionOperation, UnknownToolOperation,
|
||||
BangShellOperation, CompactionOperation, DoctorOperation, Emitter, EntryHookOperation,
|
||||
ExitOnErrorOperation, GooseEffect, InferenceRunner, MaxTurnsOperation, Operation,
|
||||
ProjectOperation, RecipeOperation, RetryOperation, SkillOperation, SlashCommandOperation,
|
||||
StateMachine, SteerOperation, SteerQueue, Step, StopHookOperation, ToolApprovalOperation,
|
||||
ToolExecutionOperation, ToolPairCompactionOperation, UnknownToolOperation,
|
||||
};
|
||||
use crate::agents::AgentEvent;
|
||||
use crate::config::permission::{PermissionLevel, PermissionManager};
|
||||
@@ -105,13 +105,16 @@ pub(super) struct TestPipeline {
|
||||
}
|
||||
|
||||
impl TestPipeline {
|
||||
pub(super) fn machine(&self, cancel: CancellationToken) -> StateMachine<'_> {
|
||||
pub(super) fn machine(
|
||||
&self,
|
||||
cancel: CancellationToken,
|
||||
) -> StateMachine<'_, Session, GooseEffect> {
|
||||
let provider = self.provider.clone();
|
||||
let tool_call_cutoff = crate::context_mgmt::compute_tool_call_cutoff(
|
||||
self.model_config.context_limit(),
|
||||
COMPACTION_THRESHOLD,
|
||||
);
|
||||
let operations: Vec<Arc<dyn Operation + '_>> = vec![
|
||||
let operations: Vec<Arc<dyn Operation<Session, GooseEffect> + '_>> = vec![
|
||||
Arc::new(SteerOperation::new(
|
||||
self.steer_queue.clone(),
|
||||
self.hook_manager.clone(),
|
||||
@@ -167,15 +170,17 @@ impl TestPipeline {
|
||||
));
|
||||
let mut command_handlers = operations.clone();
|
||||
command_handlers.push(inference.clone());
|
||||
let command_operation: Arc<dyn Operation + '_> =
|
||||
let command_operation: Arc<dyn Operation<Session, GooseEffect> + '_> =
|
||||
Arc::new(SlashCommandOperation::new(command_handlers));
|
||||
let steps = std::iter::once(command_operation)
|
||||
.chain(operations)
|
||||
.map(Step::Operation)
|
||||
.chain(std::iter::once(Step::Inference(inference)))
|
||||
.collect();
|
||||
let steps = std::iter::once(Arc::new(EntryHookOperation::new(self.hook_manager.clone()))
|
||||
as Arc<dyn Operation<Session, GooseEffect> + '_>)
|
||||
.chain(std::iter::once(command_operation))
|
||||
.chain(operations)
|
||||
.map(Step::Operation)
|
||||
.chain(std::iter::once(Step::Inference(inference)))
|
||||
.collect();
|
||||
|
||||
StateMachine::new(steps, cancel).with_hook_manager(self.hook_manager.clone())
|
||||
StateMachine::new(steps, cancel)
|
||||
}
|
||||
|
||||
pub(super) async fn with_goose_mode(self, mode: GooseMode) -> Self {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use anyhow::Result;
|
||||
use goose_providers::conversation::token_usage::{CostSource, ProviderUsage, Usage as TokenUsage};
|
||||
|
||||
use crate::agents::state_machine::operation::StateEffect;
|
||||
use crate::agents::state_machine::{ConversationEffect, GooseEffect};
|
||||
use crate::conversation::message::MessageUsage;
|
||||
use crate::conversation::Conversation;
|
||||
use crate::session::{Session, SessionManager};
|
||||
|
||||
fn attach_to_last_assistant(effects: &mut [StateEffect], usage: &ProviderUsage) {
|
||||
fn attach_to_last_assistant(effects: &mut [GooseEffect], usage: &ProviderUsage) {
|
||||
let Some(message) = effects.iter_mut().rev().find_map(|effect| match effect {
|
||||
StateEffect::AppendMessage(message)
|
||||
GooseEffect::Conversation(ConversationEffect::AppendMessage(message))
|
||||
if message.role == rmcp::model::Role::Assistant && message.error_kind().is_none() =>
|
||||
{
|
||||
Some(message)
|
||||
@@ -20,11 +20,11 @@ fn attach_to_last_assistant(effects: &mut [StateEffect], usage: &ProviderUsage)
|
||||
message.metadata.usage = Some(Box::new(MessageUsage::from_provider_usage(usage, false)));
|
||||
}
|
||||
|
||||
pub(super) fn enrich(session: &Session, effects: &mut [StateEffect]) {
|
||||
pub(super) fn enrich(session: &Session, effects: &mut [GooseEffect]) {
|
||||
for index in 0..effects.len() {
|
||||
let (usage, replaces_conversation) = match &effects[index] {
|
||||
StateEffect::RecordUsage(usage) => (usage.clone(), false),
|
||||
StateEffect::ReplaceConversation {
|
||||
GooseEffect::RecordUsage(usage) => (usage.clone(), false),
|
||||
GooseEffect::ReplaceConversation {
|
||||
usage: Some(usage), ..
|
||||
} => (usage.clone(), true),
|
||||
_ => continue,
|
||||
@@ -53,8 +53,8 @@ pub(super) fn enrich(session: &Session, effects: &mut [StateEffect]) {
|
||||
attach_to_last_assistant(effects, &enriched);
|
||||
}
|
||||
match &mut effects[index] {
|
||||
StateEffect::RecordUsage(usage) => *usage = enriched,
|
||||
StateEffect::ReplaceConversation { usage, .. } => *usage = Some(enriched),
|
||||
GooseEffect::RecordUsage(usage) => *usage = enriched,
|
||||
GooseEffect::ReplaceConversation { usage, .. } => *usage = Some(enriched),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use goose::agents::state_machine::{
|
||||
yielded_with, Emitter, Inference, InferenceInput, Operation, OperationResult, StateEffect,
|
||||
yielded_with, Emitter, GooseEffect, Inference, InferenceInput, Operation, OperationResult,
|
||||
StateMachine, Step,
|
||||
};
|
||||
use goose::agents::AgentEvent;
|
||||
@@ -18,18 +17,11 @@ use goose_providers::conversation::token_usage::Usage;
|
||||
use goose_providers::model::ModelConfig;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::field::{Field, Visit};
|
||||
use tracing::span::{Attributes, Id, Record};
|
||||
use tracing::Subscriber;
|
||||
use tracing_futures::Instrument;
|
||||
use tracing_subscriber::layer::{Context, SubscriberExt};
|
||||
use tracing_subscriber::registry::LookupSpan;
|
||||
use tracing_subscriber::Layer;
|
||||
|
||||
struct PromptPart;
|
||||
|
||||
#[async_trait]
|
||||
impl Operation for PromptPart {
|
||||
impl Operation<Session, GooseEffect> for PromptPart {
|
||||
fn name(&self) -> &'static str {
|
||||
"prompt_part"
|
||||
}
|
||||
@@ -46,14 +38,14 @@ impl Operation for PromptPart {
|
||||
struct TestInference;
|
||||
|
||||
#[async_trait]
|
||||
impl Operation for TestInference {
|
||||
impl Operation<Session, GooseEffect> for TestInference {
|
||||
fn name(&self) -> &'static str {
|
||||
"test_inference"
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Inference for TestInference {
|
||||
impl Inference<Session, GooseEffect> for TestInference {
|
||||
fn applies(&self, _conversation: &Conversation) -> bool {
|
||||
true
|
||||
}
|
||||
@@ -64,7 +56,7 @@ impl Inference for TestInference {
|
||||
conversation: &Conversation,
|
||||
input: InferenceInput,
|
||||
emit: &Emitter,
|
||||
) -> Result<OperationResult> {
|
||||
) -> Result<OperationResult<GooseEffect>> {
|
||||
assert_eq!(
|
||||
input.prompt_parts,
|
||||
[("test".to_string(), "custom context".to_string())]
|
||||
@@ -80,8 +72,8 @@ impl Inference for TestInference {
|
||||
.message(Message::assistant().with_text(format!("{prompt} answered")))
|
||||
.await;
|
||||
yielded_with([
|
||||
StateEffect::from(message),
|
||||
StateEffect::RecordUsage(ProviderUsage::new(
|
||||
GooseEffect::from(message),
|
||||
GooseEffect::RecordUsage(ProviderUsage::new(
|
||||
"test-model".to_string(),
|
||||
Usage::new(Some(5), Some(7), Some(12)),
|
||||
)),
|
||||
@@ -89,52 +81,8 @@ impl Inference for TestInference {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct TraceFields(Arc<Mutex<HashMap<String, String>>>);
|
||||
|
||||
impl<S> Layer<S> for TraceFields
|
||||
where
|
||||
S: Subscriber + for<'lookup> LookupSpan<'lookup>,
|
||||
{
|
||||
fn on_new_span(&self, attributes: &Attributes<'_>, id: &Id, context: Context<'_, S>) {
|
||||
if context
|
||||
.span(id)
|
||||
.is_some_and(|span| span.metadata().name() == "state_machine_test")
|
||||
{
|
||||
attributes.record(&mut FieldVisitor(self.0.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
fn on_record(&self, id: &Id, values: &Record<'_>, context: Context<'_, S>) {
|
||||
if context
|
||||
.span(id)
|
||||
.is_some_and(|span| span.metadata().name() == "state_machine_test")
|
||||
{
|
||||
values.record(&mut FieldVisitor(self.0.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FieldVisitor(Arc<Mutex<HashMap<String, String>>>);
|
||||
|
||||
impl Visit for FieldVisitor {
|
||||
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
|
||||
self.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(field.name().to_string(), format!("{value:?}"));
|
||||
}
|
||||
|
||||
fn record_str(&mut self, field: &Field, value: &str) {
|
||||
self.0
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(field.name().to_string(), value.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn custom_pipeline_supports_step_apply_run_tracing_and_usage() -> Result<()> {
|
||||
async fn custom_pipeline_supports_step_apply_run_and_usage() -> Result<()> {
|
||||
let _env = goose_test_support::otel::clear_otel_env(&[(
|
||||
"OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT",
|
||||
"true",
|
||||
@@ -171,6 +119,12 @@ async fn custom_pipeline_supports_step_apply_run_tracing_and_usage() -> Result<(
|
||||
|
||||
let session = session_manager.get_session(&session.id, true).await?;
|
||||
let mut result = machine.step(&session, &emit).await?.unwrap();
|
||||
assert!(matches!(
|
||||
result.effects.first(),
|
||||
Some(GooseEffect::Conversation(
|
||||
goose::agents::state_machine::ConversationEffect::AppendMessage(message)
|
||||
)) if message.id.is_some()
|
||||
));
|
||||
machine
|
||||
.apply(&session_manager, &session, &mut result, &emit)
|
||||
.await?;
|
||||
@@ -191,21 +145,7 @@ async fn custom_pipeline_supports_step_apply_run_tracing_and_usage() -> Result<(
|
||||
session_manager
|
||||
.add_message(&session.id, &Message::user().with_text("second turn"))
|
||||
.await?;
|
||||
let fields = TraceFields::default();
|
||||
let subscriber = tracing_subscriber::registry().with(fields.clone());
|
||||
let _guard = tracing::subscriber::set_default(subscriber);
|
||||
let span = tracing::info_span!(
|
||||
"state_machine_test",
|
||||
trace_input = tracing::field::Empty,
|
||||
trace_output = tracing::field::Empty,
|
||||
gen_ai.output.messages = tracing::field::Empty,
|
||||
gen_ai.usage.input_tokens = tracing::field::Empty,
|
||||
gen_ai.usage.output_tokens = tracing::field::Empty,
|
||||
);
|
||||
let session = machine
|
||||
.run(&session_manager, &session.id, &emit)
|
||||
.instrument(span)
|
||||
.await?;
|
||||
let session = machine.run(&session_manager, &session.id, &emit).await?;
|
||||
|
||||
assert_eq!(
|
||||
session
|
||||
@@ -231,32 +171,5 @@ async fn custom_pipeline_supports_step_apply_run_tracing_and_usage() -> Result<(
|
||||
assert_eq!(terminal_usage.total_tokens, 12);
|
||||
assert_eq!(terminal_usage.accumulated_total_tokens, 24);
|
||||
|
||||
let fields = fields.0.lock().unwrap();
|
||||
assert_eq!(
|
||||
fields.get("trace_input").map(String::as_str),
|
||||
Some("second turn")
|
||||
);
|
||||
assert_eq!(
|
||||
fields.get("trace_output").map(String::as_str),
|
||||
Some("second turn answered")
|
||||
);
|
||||
assert_eq!(
|
||||
fields.get("gen_ai.usage.input_tokens").map(String::as_str),
|
||||
Some("5")
|
||||
);
|
||||
assert_eq!(
|
||||
fields.get("gen_ai.usage.output_tokens").map(String::as_str),
|
||||
Some("7")
|
||||
);
|
||||
let output: serde_json::Value = serde_json::from_str(&fields["gen_ai.output.messages"])?;
|
||||
assert_eq!(
|
||||
output,
|
||||
serde_json::json!([{
|
||||
"role": "assistant",
|
||||
"content": "second turn answered",
|
||||
"finish_reason": "stop",
|
||||
}])
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user