Skip to content

Commit fd6753e

Browse files
committed
feat(bob-runtime): enhance session management and tower service functionality
1 parent 59e4aad commit fd6753e

21 files changed

Lines changed: 1910 additions & 74 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ utoipa = "5.4.0"
5454
utoipa-swagger-ui = "9.0.2"
5555
arc-swap = "1.9.0"
5656
hpx = "2.4.3"
57-
scc = "3.6.11"
57+
scc = "3.6.12"
5858
winnow = "1.0.0"
5959
shadow-rs = "1.7.1"
6060
ecdysis = "1.0.1"

Cargo.toml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,21 +34,26 @@ flume = "0.12.0"
3434
futures-core = "0.3.32"
3535
futures-util = "0.3.32"
3636
genai = "=0.6.0-beta.10"
37+
jsonschema = "0.45.0"
3738
opentelemetry = "0.31.0"
38-
opentelemetry-otlp = "0.31.0"
39+
opentelemetry-otlp = { version = "0.31.1", features = ["grpc-tonic"] }
3940
opentelemetry_sdk = "0.31.0"
4041
rmcp = { version = "1.2.0" }
4142
scc = "3.6.12"
4243
serde = "1.0.228"
4344
serde_json = "1.0.149"
45+
serde_yml = "0.0.12"
4446
tempfile = "3.27.0"
4547
thiserror = "2.0.18"
4648
tokio = "1.50.0"
4749
tokio-stream = "0.1.18"
4850
tokio-util = "0.7.18"
51+
toml = "1.1.0"
52+
tower = "0.5"
4953
tracing = "0.1.44"
50-
tracing-opentelemetry = "0.32.0"
54+
tracing-opentelemetry = "0.32.1"
5155
tracing-subscriber = "0.3.23"
56+
uuid = { version = "1.22.0", features = ["v4"] }
5257

5358
# Enable pedantic lints for stricter code quality
5459
# Priority -1 so individual lint settings override

bin/bob-cli/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ eyre = { workspace = true }
2626
genai = { workspace = true }
2727
serde = { workspace = true, features = ["derive"] }
2828
serde_json.workspace = true
29-
serde_yml = "0.0.12"
29+
serde_yml = { workspace = true }
3030
tokio = { workspace = true, features = ["io-std", "io-util"] }
31-
toml = "0.9.11"
31+
toml = { workspace = true }
3232
tracing-subscriber = { workspace = true, features = ["env-filter"] }
3333

3434
[lints]

bin/bob-cli/src/bootstrap.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ pub(crate) struct CliRuntimeHandles {
4040
pub tools: Arc<dyn bob_adapters::core::ports::ToolPort>,
4141
pub store: Arc<dyn bob_adapters::core::ports::SessionStore>,
4242
pub tape: Arc<dyn bob_adapters::core::ports::TapeStorePort>,
43+
#[expect(dead_code)]
4344
pub skills_context: Option<SkillsRuntimeContext>,
4445
}
4546

bin/bob-cli/src/main.rs

Lines changed: 62 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ mod request_context;
1616

1717
use std::path::{Path, PathBuf};
1818

19-
use bob_runtime::agent_loop::{AgentLoop, AgentLoopOutput, help_text};
19+
use bob_runtime::{Agent, Session};
2020
use clap::{Parser, Subcommand};
2121
use eyre::WrapErr;
2222

2323
use crate::{
24-
bootstrap::{CliRuntimeHandles, SkillsRuntimeContext, build_runtime},
25-
request_context::build_request_context,
24+
bootstrap::{CliRuntimeHandles, build_runtime},
25+
config::AgentConfig,
2626
};
2727

2828
/// Bob CLI — a general-purpose AI agent framework CLI.
@@ -140,34 +140,51 @@ fn print_help() {
140140
CLI session commands:
141141
/help, /h Show this help message
142142
/new, /reset Start a new session context
143-
144-
{}
145-
",
146-
help_text()
143+
/quit, /q Exit the REPL
144+
145+
Available slash commands:
146+
/tools List available tools
147+
/tool <name> Describe a specific tool
148+
/tape search <query> Search tape history
149+
/tape info Show tape statistics
150+
/anchors List tape anchors
151+
/handoff [name] Create handoff checkpoint
152+
/usage Show session token usage
153+
"
147154
);
148155
}
149156

157+
/// Build an Agent from configuration.
158+
async fn build_agent(cfg: &AgentConfig) -> eyre::Result<Agent> {
159+
let CliRuntimeHandles { runtime, tools, store, tape, skills_context: _ } =
160+
build_runtime(cfg).await?;
161+
162+
let mut builder = Agent::from_runtime(runtime, tools).with_store(store).with_tape(tape);
163+
164+
// Load system prompt from workspace file if it exists
165+
if let Ok(prompt) = std::fs::read_to_string(".agent/system-prompt.md") {
166+
builder = builder.with_system_prompt(prompt);
167+
}
168+
169+
Ok(builder.build())
170+
}
171+
150172
/// Run the interactive REPL loop.
151173
#[expect(
152174
clippy::print_stdout,
153175
clippy::print_stderr,
154176
reason = "CLI REPL must use stdout/stderr for user interaction"
155177
)]
156-
async fn repl(
157-
agent_loop: AgentLoop,
158-
model: &str,
159-
skills_context: Option<&SkillsRuntimeContext>,
160-
policy: Option<&config::PolicyConfig>,
161-
) {
178+
async fn repl(mut session: Session, model: &str, cfg: &AgentConfig) {
162179
use tokio::io::{AsyncBufReadExt, BufReader};
163180

164181
let stdin = BufReader::new(tokio::io::stdin());
165182
let mut lines = stdin.lines();
166183

167-
let mut session_seq: u64 = 1;
168-
let mut session_id = format!("cli-session-{session_seq}");
184+
let skills_context = bootstrap::build_skills_composer(cfg).ok().flatten();
169185

170186
eprintln!("Bob agent ready (model: {model})");
187+
eprintln!("Session: {}", session.session_id());
171188
eprintln!("Type a message and press Enter. /help for commands.\n");
172189

173190
loop {
@@ -195,31 +212,37 @@ async fn repl(
195212
continue;
196213
}
197214
ReplCommand::NewSession => {
198-
session_seq = session_seq.saturating_add(1);
199-
session_id = format!("cli-session-{session_seq}");
200-
eprintln!("Started new session: {session_id}");
215+
session = session.new_session();
216+
eprintln!("Started new session: {}", session.session_id());
201217
continue;
202218
}
203219
}
204220
}
205221

206-
let context = build_request_context(&input, skills_context, policy);
207-
match agent_loop.handle_input_with_context(&input, &session_id, context).await {
208-
Ok(AgentLoopOutput::Response(bob_runtime::core::types::AgentRunResult::Finished(
209-
resp,
210-
))) => {
211-
println!("{}", resp.content);
212-
println!(
213-
"\n[usage] prompt={} completion={} total={}",
214-
resp.usage.prompt_tokens,
215-
resp.usage.completion_tokens,
216-
resp.usage.total()
217-
);
218-
}
219-
Ok(AgentLoopOutput::CommandOutput(output)) => {
220-
println!("{output}");
222+
// Build request context with skills if available
223+
let context = request_context::build_request_context(
224+
&input,
225+
skills_context.as_ref(),
226+
cfg.policy.as_ref(),
227+
);
228+
229+
match session.chat_with_context(&input, context).await {
230+
Ok(response) => {
231+
if response.is_quit {
232+
break;
233+
}
234+
if !response.content.is_empty() {
235+
println!("{}", response.content);
236+
}
237+
if response.usage.total() > 0 {
238+
println!(
239+
"\n[usage] prompt={} completion={} total={}",
240+
response.usage.prompt_tokens,
241+
response.usage.completion_tokens,
242+
response.usage.total()
243+
);
244+
}
221245
}
222-
Ok(AgentLoopOutput::Quit) => break,
223246
Err(err) => {
224247
eprintln!("Error: {err}");
225248
}
@@ -584,17 +607,11 @@ async fn main() -> eyre::Result<()> {
584607
let cfg = config::load_config(&cli.config)
585608
.wrap_err_with(|| format!("failed to load config from '{}'", cli.config))?;
586609

587-
let CliRuntimeHandles { runtime, tools, store, tape, skills_context } =
588-
build_runtime(&cfg).await?;
589-
let agent_loop =
590-
AgentLoop::new(runtime.clone(), tools.clone()).with_store(store).with_tape(tape);
591-
repl(
592-
agent_loop,
593-
&cfg.runtime.default_model,
594-
skills_context.as_ref(),
595-
cfg.policy.as_ref(),
596-
)
597-
.await;
610+
// Build agent using the new simplified API
611+
let agent = build_agent(&cfg).await?;
612+
let session = agent.start_session();
613+
614+
repl(session, &cfg.runtime.default_model, &cfg).await;
598615
}
599616
Commands::Skills(skills_cmd) => match skills_cmd {
600617
SkillsCommands::List {

crates/bob-adapters/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@ mcp-rmcp = [
2424
observe-otel = [
2525
"dep:tracing",
2626
"dep:tracing-opentelemetry",
27+
"dep:tracing-subscriber",
2728
"dep:opentelemetry",
2829
"dep:opentelemetry-otlp",
2930
"dep:opentelemetry_sdk",
3031
]
3132
observe-tracing = ["dep:tracing"]
3233
skills-agent = ["dep:bob-skills"]
3334
store-memory = []
35+
tracing-subscriber = ["dep:tracing-subscriber"]
3436

3537
[dependencies]
3638
async-trait = { workspace = true }
@@ -50,6 +52,7 @@ opentelemetry_sdk = { workspace = true, optional = true }
5052
rmcp = { workspace = true, optional = true }
5153
tracing = { workspace = true, optional = true }
5254
tracing-opentelemetry = { workspace = true, optional = true }
55+
tracing-subscriber = { workspace = true, optional = true }
5356

5457
[dev-dependencies]
5558
tempfile = { workspace = true }

crates/bob-adapters/src/observe_otel.rs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@
2727
use std::sync::OnceLock;
2828

2929
use bob_core::{ports::EventSink, types::AgentEvent};
30-
use opentelemetry::trace::SpanKind;
31-
use opentelemetry_otlp::SpanExporter;
30+
use opentelemetry::trace::{SpanKind, TracerProvider as _};
31+
use opentelemetry_otlp::{SpanExporter, WithExportConfig};
3232
use opentelemetry_sdk::{Resource, trace as sdktrace};
33-
use tracing_opentelemetry::OpenTelemetrySpanExt;
3433

3534
/// Keeps the OTel pipeline alive. Drop this to shut down export.
35+
#[derive(Debug)]
3636
pub struct OtlpGuard {
3737
_tracer_provider: sdktrace::SdkTracerProvider,
3838
}
@@ -70,7 +70,8 @@ pub async fn init_otel(
7070
INSTALL.get_or_init(|| {
7171
// The subscriber must already be initialised; we just add the OTel layer.
7272
// If no subscriber is installed, this is a no-op.
73-
let _otel_layer = tracing_opentelemetry::layer().with_tracer(tracer);
73+
let _otel_layer =
74+
tracing_opentelemetry::layer::<tracing_subscriber::Registry>().with_tracer(tracer);
7475
// NOTE: The caller is responsible for installing the tracing subscriber
7576
// and adding this layer. We expose the tracer provider so advanced
7677
// users can add the layer themselves.
@@ -197,6 +198,30 @@ impl EventSink for OtlpEventSink {
197198
"agent error",
198199
);
199200
}
201+
AgentEvent::SubagentSpawned { parent_session_id, subagent_id, task } => {
202+
tracing::info!(
203+
parent: &tracing::Span::current(),
204+
parent_session_id = %parent_session_id,
205+
subagent_id = %subagent_id,
206+
task = %task,
207+
"subagent spawned",
208+
);
209+
}
210+
AgentEvent::SubagentCompleted { subagent_id, is_error } => {
211+
if is_error {
212+
tracing::warn!(
213+
parent: &tracing::Span::current(),
214+
subagent_id = %subagent_id,
215+
"subagent completed with error",
216+
);
217+
} else {
218+
tracing::info!(
219+
parent: &tracing::Span::current(),
220+
subagent_id = %subagent_id,
221+
"subagent completed",
222+
);
223+
}
224+
}
200225
}
201226
}
202227
}

crates/bob-adapters/src/provider_router.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -269,14 +269,14 @@ mod tests {
269269
use super::*;
270270

271271
struct MockLlm {
272-
name: &'static str,
272+
_name: &'static str,
273273
responses: Mutex<Vec<Result<LlmResponse, LlmError>>>,
274274
}
275275

276276
impl MockLlm {
277277
fn succeeds(name: &'static str, content: &'static str) -> Self {
278278
Self {
279-
name,
279+
_name: name,
280280
responses: Mutex::new(vec![Ok(LlmResponse {
281281
content: content.into(),
282282
usage: bob_core::types::TokenUsage::default(),
@@ -288,7 +288,7 @@ mod tests {
288288

289289
fn always_fails(name: &'static str) -> Self {
290290
Self {
291-
name,
291+
_name: name,
292292
responses: Mutex::new(vec![Err(LlmError::Provider(format!(
293293
"{name}: simulated failure"
294294
)))]),

crates/bob-chat/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ pub mod event;
7676
pub mod file;
7777
pub mod message;
7878
pub mod modal;
79+
pub mod state;
7980
pub mod stream;
8081
pub mod thread;
8182

@@ -103,5 +104,6 @@ pub use message::{
103104
pub use modal::{
104105
ModalChild, ModalElement, RadioSelectElement, SelectElement, SelectOption, TextInputElement,
105106
};
107+
pub use state::AppState;
106108
pub use stream::{StreamOptions, TextStream, fallback_stream};
107109
pub use thread::ThreadHandle;

0 commit comments

Comments
 (0)