make Emitter sync
Some checks failed
Unused Dependencies / machete (push) Has been cancelled

This commit is contained in:
Lifei Zhou 2026-10-09 13:34:02 +11:00
parent e110fbab96
commit ec52906b67
20 changed files with 100 additions and 134 deletions

View file

@ -335,21 +335,16 @@ impl<'a, S: MaybeSync, E: InferenceEffect> InferenceRunner<'a, S, E> {
effects
}
async fn emit_message(&self, message: Message, emit: &Emitter) {
fn emit_message(&self, message: Message, emit: &Emitter) {
let message = message.with_generated_id_if_missing();
self.output().accumulator.push(message.clone());
emit.message(message).await;
emit.message(message);
}
async fn error_outcome(
&self,
err: &ProviderError,
emit: &Emitter,
) -> Result<OperationResult<E>> {
fn error_outcome(&self, err: &ProviderError, emit: &Emitter) -> Result<OperationResult<E>> {
tracing::Span::current().record("error.type", err.telemetry_type());
tracing::error!("LLM provider error: {err}");
self.emit_message(Message::from_provider_error(err), emit)
.await;
self.emit_message(Message::from_provider_error(err), emit);
applied(self.take_output())
}
}
@ -453,7 +448,7 @@ impl<S: MaybeSync, E: InferenceEffect> Inference<S, E> for InferenceRunner<'_, S
let mut stream = match stream {
Ok(stream) => stream,
Err(err) => return self.error_outcome(&err, emit).await,
Err(err) => return self.error_outcome(&err, emit),
};
let requested_model = self.model_config.model_name.clone();
@ -475,7 +470,7 @@ impl<S: MaybeSync, E: InferenceEffect> Inference<S, E> for InferenceRunner<'_, S
while let Some(result) = stream.next().await {
let (msg_opt, usage_opt) = match result {
Ok(chunk) => chunk,
Err(err) => return self.error_outcome(&err, emit).await,
Err(err) => return self.error_outcome(&err, emit),
};
if let Some(usage) = usage_opt {
record_chat_usage(&tracing::Span::current(), &usage);
@ -497,7 +492,7 @@ impl<S: MaybeSync, E: InferenceEffect> Inference<S, E> for InferenceRunner<'_, S
if chunk.content.is_empty() && !chunk.metadata.output_token_limit_reached {
self.output().accumulator.push(chunk);
} else {
self.emit_message(chunk, emit).await;
self.emit_message(chunk, emit);
}
}
}
@ -547,7 +542,7 @@ impl<S: MaybeSync, E: InferenceEffect> Inference<S, E> for InferenceRunner<'_, S
output.accumulator.clear();
message
};
self.emit_message(message, emit).await;
self.emit_message(message, emit);
}
applied(self.take_output())
}

View file

@ -236,7 +236,7 @@ where
.and_then(|conversation| messages_since_kickoff(conversation).ok())
.and_then(interrupted_response);
if let Some(response) = unanswered {
let effects = vec![E::from(emit.message(response).await)];
let effects = vec![E::from(emit.message(response))];
session = self
.save(runtime, session_id, session, effects, emit)
.await?;

View file

@ -260,13 +260,13 @@ impl Emitter {
Self { tx, cancel }
}
pub async fn emit(&self, event: AgentEvent) {
pub fn emit(&self, event: AgentEvent) {
let _ = self.tx.send(event);
}
pub async fn message(&self, message: Message) -> Message {
pub fn message(&self, message: Message) -> Message {
let message = message.with_generated_id_if_missing();
self.emit(AgentEvent::Message(message.clone())).await;
self.emit(AgentEvent::Message(message.clone()));
message
}

View file

@ -311,7 +311,7 @@ where
) -> Vec<E> {
let response = self.response.lock().unwrap().take();
match response {
Some(response) => vec![E::from(emit.message(response).await)],
Some(response) => vec![E::from(emit.message(response))],
None => Vec::new(),
}
}
@ -391,7 +391,7 @@ where
.unwrap()
.take()
.expect("tool responses retained");
let message = emit.message(message).await;
let message = emit.message(message);
applied([E::from(message)])
}
}

View file

@ -68,7 +68,7 @@ impl Operation<Session, GooseEffect> for BangShellOperation {
);
let request = Message::assistant()
.with_tool_request(format!("bang_shell_{}", uuid::Uuid::now_v7()), Ok(call));
let request = emit.message(request).await;
let request = emit.message(request);
applied([request.into()])
}

View file

@ -98,7 +98,7 @@ impl CompactionOperation {
}
}
async fn command_error(
fn command_error(
conversation: &Conversation,
message: String,
emit: &Emitter,
@ -115,8 +115,8 @@ impl CompactionOperation {
let response = Message::assistant()
.with_text(message)
.with_visibility(true, false);
emit.message(command).await;
let response = emit.message(response).await;
emit.message(command);
let response = emit.message(response);
yielded_with([
ConversationEffect::SetMessageVisibility {
message_id,
@ -128,10 +128,7 @@ impl CompactionOperation {
])
}
async fn clear(
conversation: &Conversation,
emit: &Emitter,
) -> Result<OperationResult<GooseEffect>> {
fn clear(conversation: &Conversation, emit: &Emitter) -> Result<OperationResult<GooseEffect>> {
let command = messages_since_kickoff(conversation)?
.first()
.cloned()
@ -140,8 +137,8 @@ impl CompactionOperation {
let response = Message::assistant()
.with_text("Conversation cleared")
.with_visibility(true, false);
let command = emit.message(command).await;
let response = emit.message(response).await;
let command = emit.message(command);
let response = emit.message(response);
yielded_with([
Conversation::default().into(),
command.into(),
@ -164,7 +161,7 @@ impl Operation<Session, GooseEffect> for CompactionOperation {
emit: &Emitter,
) -> Result<OperationResult<GooseEffect>> {
match command.command {
"clear" => return Self::clear(conversation, emit).await,
"clear" => return Self::clear(conversation, emit),
"compact" => {}
_ => return not_applicable(),
}
@ -188,7 +185,7 @@ impl Operation<Session, GooseEffect> for CompactionOperation {
Ok(result) => result,
Err(error) => {
span.record("error.type", "compaction_error");
return Self::command_error(conversation, error.to_string(), emit).await;
return Self::command_error(conversation, error.to_string(), emit);
}
};
let compacted = result.conversation;
@ -203,8 +200,8 @@ impl Operation<Session, GooseEffect> for CompactionOperation {
let response = Message::assistant()
.with_text("Compaction complete")
.with_visibility(true, false);
emit.message(command).await;
let response = emit.message(response).await;
emit.message(command);
let response = emit.message(response);
yielded_with([
GooseEffect::CompactConversation {
conversation: compacted,
@ -296,13 +293,11 @@ impl Operation<Session, GooseEffect> for CompactionOperation {
"Exceeded auto-compact threshold of {threshold_percentage}%. \
Performing auto-compaction..."
),
))
.await;
));
emit.message(Message::assistant().with_system_notification(
SystemNotificationType::ThinkingMessage,
COMPACTION_THINKING_TEXT,
))
.await;
));
let span = chat_span(
self.provider.as_ref(),
@ -327,8 +322,7 @@ impl Operation<Session, GooseEffect> for CompactionOperation {
emit.message(Message::assistant().with_system_notification(
SystemNotificationType::InlineMessage,
"Compaction complete",
))
.await;
));
applied([GooseEffect::CompactConversation {
conversation: compacted,
usage: Some(usage),
@ -339,8 +333,7 @@ impl Operation<Session, GooseEffect> for CompactionOperation {
emit.message(Message::assistant().with_text(format!(
"Ran into this error trying to compact: {e}.\n\n\
Please try again or create a new session"
)))
.await;
)));
yielded()
}
}

View file

@ -57,8 +57,8 @@ impl Operation<Session, GooseEffect> for DoctorOperation {
if result.role == Role::Assistant {
let command_message = command_message.with_visibility(true, false);
let result = result.with_visibility(true, false);
emit.message(command_message).await;
let result = emit.message(result).await;
emit.message(command_message);
let result = emit.message(result);
return yielded_with([
ConversationEffect::SetMessageVisibility {
message_id,

View file

@ -44,8 +44,7 @@ impl Operation<Session, GooseEffect> for EmptyResponseOperation {
.id
.clone()
.ok_or_else(|| anyhow!("Persisted empty-response fallback has no id"))?;
emit.message(fallback.clone().with_visibility(true, true))
.await;
emit.message(fallback.clone().with_visibility(true, true));
applied([ConversationEffect::SetMessageVisibility {
message_id,
user_visible: true,

View file

@ -98,7 +98,7 @@ fn readable_output(output: &str) -> String {
}
}
async fn subagent_result_message(
fn subagent_result_message(
subagent_id: &str,
outcome: SubagentOutcome,
others_waiting: bool,
@ -123,7 +123,7 @@ async fn subagent_result_message(
),
SubagentOutcome::Cancelled => return None,
};
emit.message(inline_notice(notice)).await;
emit.message(inline_notice(notice));
let mut message = Message::user().with_text(text).with_visibility(false, true);
message
.metadata
@ -207,8 +207,7 @@ impl Operation<Session, GooseEffect> for ForegroundSubagentOperation {
let Some(subagent_id) = stopped.subagent_ids.remove(&task_id) else {
continue;
};
if let Some(message) = subagent_result_message(&subagent_id, outcome, true, emit).await
{
if let Some(message) = subagent_result_message(&subagent_id, outcome, true, emit) {
effects.push(message);
delivered.insert(subagent_id);
}
@ -252,14 +251,13 @@ impl Operation<Session, GooseEffect> for ForegroundSubagentOperation {
{
SubagentStart::HasOutcome(outcome) => {
if let Some(message) =
subagent_result_message(subagent_id, outcome, pending.len() > 1, emit).await
subagent_result_message(subagent_id, outcome, pending.len() > 1, emit)
{
return applied([message]);
}
}
SubagentStart::Started { task, run } => {
emit.message(inline_notice(start_notice(subagent_id, task.as_deref())))
.await;
emit.message(inline_notice(start_notice(subagent_id, task.as_deref())));
let handle = running.tasks.spawn(run.in_current_span());
running
.subagent_ids
@ -281,7 +279,7 @@ impl Operation<Session, GooseEffect> for ForegroundSubagentOperation {
.subagent_ids
.remove(&task_id)
.ok_or_else(|| anyhow!("Unknown foreground subagent task {task_id}"))?;
match subagent_result_message(&subagent_id, outcome, pending.len() > 1, emit).await {
match subagent_result_message(&subagent_id, outcome, pending.len() > 1, emit) {
Some(message) => applied([message]),
None => not_applicable(),
}

View file

@ -62,7 +62,7 @@ impl Operation<Session, GooseEffect> for MaxTurnsOperation {
}
let message = Message::assistant().with_text(MAX_TURNS_MESSAGE);
let message = emit.message(message).await;
let message = emit.message(message);
yielded_with([message.into()])
}
}

View file

@ -67,7 +67,7 @@ impl RecipeOperation {
self.response.lock().unwrap().take()
}
async fn command_error(
fn command_error(
&self,
conversation: &Conversation,
message: String,
@ -85,8 +85,8 @@ impl RecipeOperation {
let response = Message::assistant()
.with_text(message)
.with_visibility(true, false);
emit.message(command).await;
let response = emit.message(response).await;
emit.message(command);
let response = emit.message(response);
yielded_with([
ConversationEffect::SetMessageVisibility {
message_id,
@ -112,7 +112,7 @@ impl Operation<Session, GooseEffect> for RecipeOperation {
emit: &Emitter,
) -> Vec<GooseEffect> {
match self.take_response() {
Some(response) => vec![emit.message(response).await.into()],
Some(response) => vec![emit.message(response).into()],
None => Vec::new(),
}
}
@ -130,14 +130,16 @@ impl Operation<Session, GooseEffect> for RecipeOperation {
) {
Ok(Some(recipe)) => recipe,
Ok(None) => return not_applicable(),
Err(error) => return self.command_error(conversation, error, emit).await,
Err(error) => return self.command_error(conversation, error, emit),
};
if let Some(response) = recipe.response.clone() {
if let Err(error) = FinalOutputTool::try_new(response) {
return self
.command_error(conversation, format!("Recipe is not valid: {error}"), emit)
.await;
return self.command_error(
conversation,
format!("Recipe is not valid: {error}"),
emit,
);
}
}
@ -193,13 +195,11 @@ impl Operation<Session, GooseEffect> for RecipeOperation {
};
if !self.provider.supports_builtin_tools() {
return self
.command_error(
conversation,
structured_output_unsupported_message(self.provider.get_name()),
emit,
)
.await;
return self.command_error(
conversation,
structured_output_unsupported_message(self.provider.get_name()),
emit,
);
}
let messages = messages_since_kickoff(conversation)?;
@ -222,7 +222,7 @@ impl Operation<Session, GooseEffect> for RecipeOperation {
)])),
request.metadata.as_ref(),
);
let response = emit.message(response).await;
let response = emit.message(response);
return applied([response.into()]);
}
if FinalOutputTool::has_unanswered_siblings(messages, &request.id) {
@ -297,14 +297,14 @@ impl Operation<Session, GooseEffect> for RecipeOperation {
.await;
}
let response = self.take_response().expect("recipe response held");
let response = emit.message(response).await;
let response = emit.message(response);
return applied([response.into()]);
}
if let Some(output) = FinalOutputTool::successful_output(messages) {
if last_effective_role(messages)? == EffectiveRole::Tool {
let message = Message::assistant().with_text(output);
let message = emit.message(message).await;
let message = emit.message(message);
return applied([message.into()]);
}
return not_applicable();
@ -314,7 +314,7 @@ impl Operation<Session, GooseEffect> for RecipeOperation {
let message = Message::user()
.with_text(FINAL_OUTPUT_CONTINUATION_MESSAGE)
.agent_only();
let message = emit.message(message).await;
let message = emit.message(message);
return applied([message.into()]);
}

View file

@ -139,8 +139,8 @@ impl Operation<Session, GooseEffect> for RetryOperation {
.ok_or_else(|| anyhow!("Persisted slash command message has no id"))?;
let command_message = command_message.with_visibility(true, false);
let response = response.with_visibility(true, false);
emit.message(command_message).await;
let response = emit.message(response).await;
emit.message(command_message);
let response = emit.message(response);
let mut effects = vec![
ConversationEffect::SetMessageVisibility {
@ -195,8 +195,7 @@ impl Operation<Session, GooseEffect> for RetryOperation {
emit.message(Message::assistant().with_system_notification(
SystemNotificationType::InlineMessage,
format!("Goal: {goal}"),
))
.await;
));
return applied([message.into()]);
}
}
@ -213,8 +212,7 @@ impl Operation<Session, GooseEffect> for RetryOperation {
emit.message(Message::assistant().with_system_notification(
SystemNotificationType::InlineMessage,
format!("Grind: {grind}"),
))
.await;
));
return applied([message.into()]);
}
@ -235,7 +233,7 @@ impl Operation<Session, GooseEffect> for RetryOperation {
let success = match success {
Ok(success) => success,
Err(error) => {
let message = emit.message(retry_error(&error.to_string())).await;
let message = emit.message(retry_error(&error.to_string()));
return applied([message.into()]);
}
};
@ -257,7 +255,7 @@ impl Operation<Session, GooseEffect> for RetryOperation {
"retry_max_exceeded",
&format!("Max retries ({}) exceeded", retry_config.max_retries),
);
let message = emit.message(message).await;
let message = emit.message(message);
return applied([message.into()]);
}
@ -267,7 +265,7 @@ impl Operation<Session, GooseEffect> for RetryOperation {
.map(Duration::from_secs)
.unwrap_or(self.on_failure_timeout);
if let Err(error) = execute_on_failure_command_with_timeout(command, timeout).await {
let message = emit.message(retry_error(&error.to_string())).await;
let message = emit.message(retry_error(&error.to_string()));
return applied([message.into()]);
}
}

View file

@ -219,7 +219,7 @@ impl SkillOperation {
Some(session)
}
async fn command_response(
fn command_response(
conversation: &Conversation,
message: String,
emit: &Emitter,
@ -236,8 +236,8 @@ impl SkillOperation {
let response = Message::assistant()
.with_text(message)
.with_visibility(true, false);
emit.message(command).await;
let response = emit.message(response).await;
emit.message(command);
let response = emit.message(response);
yielded_with([
ConversationEffect::SetMessageVisibility {
message_id,
@ -270,8 +270,7 @@ impl Operation<Session, GooseEffect> for SkillOperation {
&session.working_dir,
)),
emit,
)
.await;
);
}
let prompt = match crate::slash_commands::skill_slash_command::resolve_command(
@ -281,7 +280,7 @@ impl Operation<Session, GooseEffect> for SkillOperation {
) {
Ok(Some(prompt)) => prompt,
Ok(None) => return not_applicable(),
Err(error) => return Self::command_response(conversation, error, emit).await,
Err(error) => return Self::command_response(conversation, error, emit),
};
let command_message = messages_since_kickoff(conversation)?
.first()
@ -426,7 +425,7 @@ impl Operation<Session, GooseEffect> for SkillOperation {
};
response.add_tool_response_with_metadata(request.id, result, request.metadata.as_ref());
}
let response = emit.message(response).await;
let response = emit.message(response);
applied([response.into()])
}
}

View file

@ -69,9 +69,8 @@ impl Operation<Session, GooseEffect> for StatusOperation {
.id
.clone()
.ok_or_else(|| anyhow!("Persisted slash command message has no id"))?;
emit.message(command_message.with_visibility(true, false))
.await;
let response = emit.message(response).await;
emit.message(command_message.with_visibility(true, false));
let response = emit.message(response);
yielded_with([
ConversationEffect::SetMessageVisibility {
message_id,

View file

@ -83,7 +83,7 @@ impl Operation<Session, GooseEffect> for SteerOperation {
let mut drained = Vec::with_capacity(pending.len());
for message in pending {
drained.push(emit.message(message).await);
drained.push(emit.message(message));
}
*self.drained.lock().unwrap() = drained.clone();
for message in drained {

View file

@ -106,12 +106,12 @@ impl Operation<Session, GooseEffect> for StopHookOperation {
+ 1;
if blocks > self.block_cap {
let warning = block_cap_warning(&plugin, self.block_cap);
let warning = emit.message(warning).await;
let warning = emit.message(warning);
yielded_with([warning.into()])
} else {
let mut denial = denial_context_message(&plugin, &reason);
self.set_message_meta(&mut denial, DENIED, serde_json::json!(true));
emit.message(denial_notification(&plugin)).await;
emit.message(denial_notification(&plugin));
applied([denial.into()])
}
}

View file

@ -475,7 +475,7 @@ impl ToolExecutionOperation {
.await
}
async fn command_response(
fn command_response(
conversation: &Conversation,
message: String,
emit: &Emitter,
@ -492,8 +492,8 @@ impl ToolExecutionOperation {
let response = Message::assistant()
.with_text(message)
.with_visibility(true, false);
emit.message(command).await;
let response = emit.message(response).await;
emit.message(command);
let response = emit.message(response);
yielded_with([
ConversationEffect::SetMessageVisibility {
message_id,
@ -524,8 +524,7 @@ impl ToolExecutionOperation {
conversation,
format!("Extension '{filter}' not found"),
emit,
)
.await;
);
}
}
@ -546,7 +545,7 @@ impl ToolExecutionOperation {
output.push('\n');
}
}
Self::command_response(conversation, output, emit).await
Self::command_response(conversation, output, emit)
}
async fn run_prompt(
@ -562,8 +561,7 @@ impl ToolExecutionOperation {
conversation,
"Prompt name argument is required".to_string(),
emit,
)
.await;
);
};
let prompts = self
.lease(session)
@ -581,8 +579,7 @@ impl ToolExecutionOperation {
conversation,
format!("Prompt '{prompt_name}' not found"),
emit,
)
.await;
);
};
if params.get(1) == Some(&"--info") {
@ -601,7 +598,7 @@ impl ToolExecutionOperation {
output.push('\n');
}
}
return Self::command_response(conversation, output, emit).await;
return Self::command_response(conversation, output, emit);
}
let arguments: HashMap<_, _> = params
@ -623,7 +620,7 @@ impl ToolExecutionOperation {
{
Ok(result) => result,
Err(error) => {
return Self::command_response(conversation, error.to_string(), emit).await;
return Self::command_response(conversation, error.to_string(), emit);
}
};
@ -655,8 +652,7 @@ impl ToolExecutionOperation {
message.role
),
emit,
)
.await;
);
}
effects.push(message.with_visibility(false, true).into());
}
@ -665,8 +661,7 @@ impl ToolExecutionOperation {
conversation,
format!("Prompt '{prompt_name}' returned no messages"),
emit,
)
.await;
);
}
applied(effects)
}
@ -823,8 +818,7 @@ impl Operation<Session, GooseEffect> for ToolExecutionOperation {
) -> Vec<GooseEffect> {
let (actions, response) = self.take_batch();
if let Some(response) = &response {
emit.emit(AgentEvent::Message(response.user_visible_content()))
.await;
emit.emit(AgentEvent::Message(response.user_visible_content()));
}
actions
.into_iter()
@ -975,7 +969,7 @@ impl Operation<Session, GooseEffect> for ToolExecutionOperation {
request.metadata.as_ref(),
);
}
let response = emit.message(response).await;
let response = emit.message(response);
return applied([response.into()]);
};
@ -1015,7 +1009,7 @@ impl Operation<Session, GooseEffect> for ToolExecutionOperation {
request.metadata.as_ref(),
);
}
let response = emit.message(response).await;
let response = emit.message(response);
return applied([response.into()]);
}
@ -1088,8 +1082,7 @@ impl Operation<Session, GooseEffect> for ToolExecutionOperation {
emit.emit(AgentEvent::McpNotification((
request_id.clone(),
notification,
)))
.await;
)));
}
}
let metadata = requests
@ -1102,21 +1095,19 @@ impl Operation<Session, GooseEffect> for ToolExecutionOperation {
.record(&request_id, output, metadata);
}
ToolStreamItem::Message(msg) => {
emit.emit(AgentEvent::McpNotification((request_id, msg)))
.await;
emit.emit(AgentEvent::McpNotification((request_id, msg)));
}
ToolStreamItem::ActionRequired(msg) => {
let msg = msg.with_generated_id_if_missing();
self.batch.lock().unwrap().actions.push(msg.clone());
emit.message(msg).await;
emit.message(msg);
}
}
}
let (actions, response) = self.take_batch();
let response = response.ok_or_else(|| anyhow!("tool batch produced no responses"))?;
emit.emit(AgentEvent::Message(response.user_visible_content()))
.await;
emit.emit(AgentEvent::Message(response.user_visible_content()));
applied(actions.into_iter().chain([response]).map(GooseEffect::from))
}
}

View file

@ -192,7 +192,7 @@ impl Operation<Session, GooseEffect> for UnknownToolOperation {
response.add_tool_response_with_metadata(request.id, result, metadata.as_ref());
}
let response = emit.message(response).await;
let response = emit.message(response);
applied([response.into()])
}
}

View file

@ -121,7 +121,7 @@ impl EffectHandler<Session, GooseEffect> for SessionManager {
GooseEffect::Conversation(ConversationEffect::AppendMessage(message)) => {
if contains_tool_confirmation_request(message) {
// Responses can arrive immediately, so publish only after the persistence pass.
emit.emit(AgentEvent::Message(message.clone())).await;
emit.emit(AgentEvent::Message(message.clone()));
}
if let Some(usage) = message
.metadata
@ -133,8 +133,7 @@ impl EffectHandler<Session, GooseEffect> for SessionManager {
emit.emit(AgentEvent::MessageUsage {
message_id: message.id.clone(),
usage,
})
.await;
});
}
}
GooseEffect::Conversation(ConversationEffect::ReplaceConversation(
@ -144,12 +143,9 @@ impl EffectHandler<Session, GooseEffect> for SessionManager {
emit.emit(AgentEvent::HistoryReplaced(replaced_history(
conversation,
&effects[index + 1..],
)))
.await;
}
GooseEffect::RecordUsage(usage) => {
emit.emit(AgentEvent::Usage(usage.clone())).await
)));
}
GooseEffect::RecordUsage(usage) => emit.emit(AgentEvent::Usage(usage.clone())),
_ => {}
}
}

View file

@ -68,9 +68,7 @@ impl Inference<Session, GooseEffect> for TestInference {
.find(|message| message.role == rmcp::model::Role::User)
.map(Message::as_concat_text)
.unwrap();
let message = emit
.message(Message::assistant().with_text(format!("{prompt} answered")))
.await;
let message = emit.message(Message::assistant().with_text(format!("{prompt} answered")));
yielded_with([
GooseEffect::from(message),
GooseEffect::RecordUsage(ProviderUsage::new(