bxctl: chat --speaker NAME, the speaker on every turn of the chat
Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
@@ -73,12 +73,14 @@ pub fn new_session_id() -> SessionId {
|
||||
SessionId::new(&candidate).unwrap_or_else(|_| SessionId::new("chat-0-0").unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Sends one turn and reads the reply. `on_event` sees every event frame as it arrives.
|
||||
/// Sends one turn and reads the reply. `on_event` sees every event frame as it arrives. `speaker`
|
||||
/// is `--speaker`'s value, sent as it is; `None` sends none.
|
||||
pub fn run_turn(
|
||||
socket: &Path,
|
||||
session: &SessionId,
|
||||
content: &str,
|
||||
resume: bool,
|
||||
speaker: Option<&str>,
|
||||
on_event: &mut dyn FnMut(&TurnEvent),
|
||||
) -> Result<TurnDone, ChatError> {
|
||||
let mut stream = UnixStream::connect(socket).map_err(ChatError::Connect)?;
|
||||
@@ -87,8 +89,7 @@ pub fn run_turn(
|
||||
session: session.clone(),
|
||||
content: content.to_string(),
|
||||
resume,
|
||||
// Set in M5c-1 task 04, from `--speaker`. Until then no turn has a speaker.
|
||||
speaker: None,
|
||||
speaker: speaker.map(str::to_string),
|
||||
};
|
||||
write_frame(
|
||||
&mut stream,
|
||||
@@ -324,11 +325,12 @@ pub fn stream_turn(
|
||||
session: &SessionId,
|
||||
text: &str,
|
||||
resume: bool,
|
||||
speaker: Option<&str>,
|
||||
approvals: &Approvals<'_>,
|
||||
io: &mut TurnIo<'_>,
|
||||
) -> std::io::Result<Result<TurnDone, ChatError>> {
|
||||
let mut err = None;
|
||||
let outcome = run_turn(socket, session, text, resume, &mut |event| {
|
||||
let outcome = run_turn(socket, session, text, resume, speaker, &mut |event| {
|
||||
if err.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
+23
-1
@@ -9,8 +9,9 @@ use bxcore::SessionId;
|
||||
pub const USAGE: &str = "\
|
||||
usage: bxctl <command> [options]
|
||||
|
||||
bxctl chat [--socket PATH] [--admin-socket PATH] [--session ID] [--say TEXT] [--no-thinking] [--json]
|
||||
bxctl chat [--socket PATH] [--admin-socket PATH] [--session ID] [--say TEXT] [--no-thinking] [--json] [--speaker NAME]
|
||||
Talk to loopd.
|
||||
With --speaker NAME (no spaces), the turns are NAME's and are synced to Honcho as that person.
|
||||
|
||||
bxctl approvals [--admin-socket PATH]
|
||||
List the approvals waiting for a decision.
|
||||
@@ -50,6 +51,9 @@ pub struct ChatOptions {
|
||||
pub show_thinking: bool,
|
||||
pub say: Option<String>,
|
||||
pub json: bool,
|
||||
/// Who is typing, from `--speaker`, sent on every turn (M5c spec, section 11). There is no
|
||||
/// default: without the flag it is `None`, and a turn without a speaker is not synced.
|
||||
pub speaker: Option<String>,
|
||||
}
|
||||
|
||||
impl ChatOptions {
|
||||
@@ -61,6 +65,7 @@ impl ChatOptions {
|
||||
show_thinking: true,
|
||||
say: None,
|
||||
json: false,
|
||||
speaker: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,6 +148,14 @@ fn parse_chat(args: &[String], home: &Path) -> Result<Command, UsageError> {
|
||||
"--admin-socket" => opts.admin_socket = PathBuf::from(value_at(args, i)?),
|
||||
"--session" => opts.session = Some(parse_session(&value_at(args, i)?)?),
|
||||
"--say" => opts.say = Some(value_at(args, i)?),
|
||||
// Given twice, it is refused rather than one of them chosen: the turns would be synced
|
||||
// as the wrong person.
|
||||
"--speaker" => {
|
||||
if opts.speaker.is_some() {
|
||||
return Err(UsageError);
|
||||
}
|
||||
opts.speaker = Some(parse_speaker(&value_at(args, i)?)?);
|
||||
}
|
||||
"--no-thinking" => {
|
||||
opts.show_thinking = false;
|
||||
i += 1;
|
||||
@@ -305,6 +318,15 @@ fn parse_session(value: &str) -> Result<SessionId, UsageError> {
|
||||
}
|
||||
}
|
||||
|
||||
// A speaker must be a name: not empty, and no whitespace or control character anywhere. The rest,
|
||||
// Honcho's pattern, is `memoryd`'s to check (M5c spec, section 3); a `.` is kept as it is.
|
||||
fn parse_speaker(value: &str) -> Result<String, UsageError> {
|
||||
if value.is_empty() || value.chars().any(|c| c.is_whitespace() || c.is_control()) {
|
||||
return Err(UsageError);
|
||||
}
|
||||
Ok(value.to_string())
|
||||
}
|
||||
|
||||
// A refuse reason must be present; its content is unvalidated on the client.
|
||||
fn verify_reason(reason: &str) -> Result<String, UsageError> {
|
||||
if reason.is_empty() {
|
||||
|
||||
@@ -201,7 +201,15 @@ fn run_turn_twice(
|
||||
input,
|
||||
out: &mut handle,
|
||||
};
|
||||
match stream_turn(&opts.socket, session, text, resume, &approvals, &mut io) {
|
||||
match stream_turn(
|
||||
&opts.socket,
|
||||
session,
|
||||
text,
|
||||
resume,
|
||||
opts.speaker.as_deref(),
|
||||
&approvals,
|
||||
&mut io,
|
||||
) {
|
||||
Err(e) => Err(RunError::Io(e)),
|
||||
Ok(Ok(done)) => {
|
||||
write_answer(opts, &done).map_err(RunError::Io)?;
|
||||
@@ -214,7 +222,15 @@ fn run_turn_twice(
|
||||
input,
|
||||
out: &mut handle,
|
||||
};
|
||||
match stream_turn(&opts.socket, session, text, false, &approvals, &mut io) {
|
||||
match stream_turn(
|
||||
&opts.socket,
|
||||
session,
|
||||
text,
|
||||
false,
|
||||
opts.speaker.as_deref(),
|
||||
&approvals,
|
||||
&mut io,
|
||||
) {
|
||||
Err(e) => Err(RunError::Io(e)),
|
||||
Ok(Ok(done)) => {
|
||||
write_answer(opts, &done).map_err(RunError::Io)?;
|
||||
@@ -272,6 +288,7 @@ fn run_interactive(opts: &ChatOptions, reader: &mut dyn BufRead) -> ExitCode {
|
||||
this_session,
|
||||
text,
|
||||
resume,
|
||||
opts.speaker.as_deref(),
|
||||
&approvals,
|
||||
&mut io,
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user