1use crate::{MemorySystem, Narrator, NarratorConfig, OfficeError};
4use serde::{Deserialize, Serialize};
5use std::path::PathBuf;
6use std::time::{Duration, Instant};
7use tdln_brain::{GenerationConfig, Message, NeuralBackend};
8use tokio::sync::watch;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12pub enum OfficeState {
13 Opening,
15 Active,
17 Maintenance,
19 Closing,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
25pub enum SessionType {
26 #[default]
28 Work,
29 Assist,
31 Deliberate,
33 Research,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
39pub enum SessionMode {
40 #[default]
42 Commitment,
43 Deliberation,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct TokenBudget {
50 pub max_input_tokens: u32,
52 pub max_output_tokens: u32,
54 pub daily_token_quota: u64,
56 pub max_decisions_per_cycle: u32,
58}
59
60impl Default for TokenBudget {
61 fn default() -> Self {
62 Self {
63 max_input_tokens: 4000,
64 max_output_tokens: 1024,
65 daily_token_quota: 200_000,
66 max_decisions_per_cycle: 1,
67 }
68 }
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct DreamConfig {
74 pub dream_every_n_cycles: u64,
76 pub dream_min_interval_secs: u64,
78}
79
80impl Default for DreamConfig {
81 fn default() -> Self {
82 Self {
83 dream_every_n_cycles: 100,
84 dream_min_interval_secs: 900, }
86 }
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct OfficeConfig {
92 pub tenant_id: String,
94 pub constitution_path: Option<PathBuf>,
96 pub workspace_root: PathBuf,
98 pub ledger_path: Option<PathBuf>,
100 pub model_id: String,
102 pub session_type: SessionType,
104 pub session_mode: SessionMode,
106 pub budget: TokenBudget,
108 pub dream: DreamConfig,
110 pub step_pause_ms: u64,
112 pub max_consecutive_errors: u32,
114}
115
116impl Default for OfficeConfig {
117 fn default() -> Self {
118 Self {
119 tenant_id: "agent".into(),
120 constitution_path: None,
121 workspace_root: PathBuf::from("."),
122 ledger_path: None,
123 model_id: "mock".into(),
124 session_type: SessionType::default(),
125 session_mode: SessionMode::default(),
126 budget: TokenBudget::default(),
127 dream: DreamConfig::default(),
128 step_pause_ms: 1000,
129 max_consecutive_errors: 5,
130 }
131 }
132}
133
134#[derive(Debug, Clone, Default, Serialize, Deserialize)]
136pub struct OfficeMetrics {
137 pub steps_total: u64,
139 pub decisions_total: u64,
141 pub denials_total: u64,
143 pub challenges_total: u64,
145 pub tool_errors_total: u64,
147 pub consecutive_errors: u32,
149 pub input_tokens_today: u64,
151 pub output_tokens_today: u64,
153 pub dreams_total: u64,
155 pub decisions_since_dream: u64,
157}
158
159pub struct Office<B: NeuralBackend> {
161 config: OfficeConfig,
162 brain: B,
163 memory: MemorySystem,
164 narrator: Narrator,
165 metrics: OfficeMetrics,
166 state: OfficeState,
167 state_tx: watch::Sender<OfficeState>,
168 history: Vec<Message>,
169 last_dream_time: Option<Instant>,
170}
171
172impl<B: NeuralBackend> Office<B> {
173 pub fn new(config: OfficeConfig, brain: B) -> (Self, watch::Receiver<OfficeState>) {
177 let (state_tx, state_rx) = watch::channel(OfficeState::Opening);
178
179 let narrator_config = NarratorConfig {
180 system_directive: format!(
181 "IDENTITY: {}\nSESSION: {:?}/{:?}\nYou are an autonomous professional agent in LogLine OS.\nOutput MUST be a valid TDLN SemanticUnit JSON.",
182 config.tenant_id, config.session_type, config.session_mode
183 ),
184 constraints: Self::build_constraints(&config),
185 constitution: None,
186 session_type: config.session_type,
187 session_mode: config.session_mode,
188 };
189
190 let office = Self {
191 config,
192 brain,
193 memory: MemorySystem::new(),
194 narrator: Narrator::new(narrator_config),
195 metrics: OfficeMetrics::default(),
196 state: OfficeState::Opening,
197 state_tx,
198 history: Vec::new(),
199 last_dream_time: None,
200 };
201
202 (office, state_rx)
203 }
204
205 fn build_constraints(config: &OfficeConfig) -> Vec<String> {
206 let mut constraints = vec![
207 "Never execute write tools unless Gate:Permit".into(),
208 "Prefer simulate() for risk_score ≥ 0.7".into(),
209 ];
210
211 match config.session_type {
212 SessionType::Work => {
213 constraints.push("May sign & act; write receipts for all actions".into());
214 }
215 SessionType::Assist => {
216 constraints.push("Propose only, never act; include remediation steps".into());
217 }
218 SessionType::Deliberate => {
219 constraints.push("Think-only; do NOT call tools".into());
220 }
221 SessionType::Research => {
222 constraints.push("Read-only tools allowed; summarize sources with citations".into());
223 }
224 }
225
226 match config.session_mode {
227 SessionMode::Commitment => {
228 constraints.push("Actions are binding; all decisions produce receipts".into());
229 }
230 SessionMode::Deliberation => {
231 constraints.push("Proposals only; nothing is executed".into());
232 }
233 }
234
235 constraints
236 }
237
238 #[must_use]
240 pub fn state(&self) -> OfficeState {
241 self.state
242 }
243
244 #[must_use]
246 pub fn metrics(&self) -> &OfficeMetrics {
247 &self.metrics
248 }
249
250 pub fn metrics_mut(&mut self) -> &mut OfficeMetrics {
252 &mut self.metrics
253 }
254
255 #[must_use]
257 pub fn config(&self) -> &OfficeConfig {
258 &self.config
259 }
260
261 #[must_use]
263 pub fn memory(&self) -> &MemorySystem {
264 &self.memory
265 }
266
267 pub fn memory_mut(&mut self) -> &mut MemorySystem {
269 &mut self.memory
270 }
271
272 fn set_state(&mut self, state: OfficeState) {
273 self.state = state;
274 let _ = self.state_tx.send(state);
275 }
276
277 fn needs_dream(&self) -> bool {
279 if self.metrics.decisions_since_dream >= self.config.dream.dream_every_n_cycles {
281 if let Some(last) = self.last_dream_time {
283 let min_interval = Duration::from_secs(self.config.dream.dream_min_interval_secs);
284 return last.elapsed() >= min_interval;
285 }
286 return true;
287 }
288 false
289 }
290
291 fn check_budget(&self) -> Result<(), OfficeError> {
293 let total_today = self.metrics.input_tokens_today + self.metrics.output_tokens_today;
295 if total_today >= self.config.budget.daily_token_quota {
296 return Err(OfficeError::QuotaExceeded(format!(
297 "daily quota exceeded: {} >= {}",
298 total_today, self.config.budget.daily_token_quota
299 )));
300 }
301 Ok(())
302 }
303
304 pub async fn open(&mut self) -> Result<(), OfficeError> {
306 self.set_state(OfficeState::Opening);
307
308 if let Some(ref path) = self.config.constitution_path {
310 if path.exists() {
311 let constitution = tokio::fs::read_to_string(path)
312 .await
313 .map_err(|e| OfficeError::Config(format!("failed to load constitution: {e}")))?;
314 self.narrator.set_constitution(constitution);
315 }
316 }
317
318 self.set_state(OfficeState::Active);
319 Ok(())
320 }
321
322 pub async fn step(&mut self, input: Option<&str>) -> Result<Option<tdln_ast::SemanticUnit>, OfficeError> {
326 if self.state != OfficeState::Active {
327 return Ok(None);
328 }
329
330 self.metrics.steps_total += 1;
331
332 if self.needs_dream() {
334 self.dream().await?;
335 }
336
337 self.check_budget()?;
339
340 if let Some(input) = input {
342 self.history.push(Message::user(input));
343 self.memory.remember(format!("User: {input}"));
344 }
345
346 let ctx = self.narrator.orient(&self.memory, self.history.clone());
348
349 let gen_config = GenerationConfig {
351 max_tokens: Some(self.config.budget.max_output_tokens),
352 ..GenerationConfig::default()
353 };
354
355 let messages = ctx.render();
357 let raw = self.brain.generate(&messages, &gen_config).await?;
358
359 self.metrics.input_tokens_today += u64::from(raw.meta.input_tokens);
361 self.metrics.output_tokens_today += u64::from(raw.meta.output_tokens);
362
363 let decision = tdln_brain::parser::parse_decision(&raw.content, raw.meta)?;
365 self.metrics.decisions_total += 1;
366 self.metrics.decisions_since_dream += 1;
367 self.metrics.consecutive_errors = 0;
368
369 self.memory.remember(format!("Decision: {}", decision.intent.kind));
371 self.history.push(Message::assistant(&raw.content));
372
373 self.narrator.increment_maintenance_counter();
375
376 Ok(Some(decision.intent))
377 }
378
379 pub async fn dream(&mut self) -> Result<(), OfficeError> {
381 let prev_state = self.state;
382 self.set_state(OfficeState::Maintenance);
383
384 let events: Vec<String> = self.memory.recent(20);
386 self.memory.consolidate(&events);
387
388 if self.history.len() > 20 {
390 self.history = self.history.split_off(self.history.len() - 10);
391 }
392
393 self.metrics.decisions_since_dream = 0;
395 self.metrics.dreams_total += 1;
396 self.last_dream_time = Some(Instant::now());
397 self.narrator.reset_maintenance_counter();
398
399 self.set_state(prev_state);
400 Ok(())
401 }
402
403 pub async fn run(mut self) -> Result<(), OfficeError> {
405 self.open().await?;
406
407 loop {
408 tokio::time::sleep(Duration::from_millis(self.config.step_pause_ms)).await;
410
411 match self.step(None).await {
412 Ok(_) => {}
413 Err(OfficeError::Shutdown) => {
414 self.set_state(OfficeState::Closing);
415 break;
416 }
417 Err(OfficeError::QuotaExceeded(msg)) => {
418 tracing::warn!("quota exceeded: {msg}");
419 continue;
421 }
422 Err(e) => {
423 self.metrics.consecutive_errors += 1;
424 tracing::warn!("step error: {e}");
425
426 if self.metrics.consecutive_errors >= self.config.max_consecutive_errors {
427 self.set_state(OfficeState::Closing);
428 return Err(e);
429 }
430
431 let delay = Duration::from_millis(
433 100 * (2_u64.pow(self.metrics.consecutive_errors.min(6))),
434 );
435 tokio::time::sleep(delay).await;
436 }
437 }
438 }
439
440 Ok(())
441 }
442
443 pub fn shutdown(&mut self) {
445 self.set_state(OfficeState::Closing);
446 }
447
448 #[must_use]
450 pub fn can_execute_tools(&self) -> bool {
451 match self.config.session_type {
452 SessionType::Work => true,
453 SessionType::Research => true, SessionType::Assist | SessionType::Deliberate => false,
455 }
456 }
457
458 #[must_use]
460 pub fn can_write(&self) -> bool {
461 matches!(
462 (&self.config.session_type, &self.config.session_mode),
463 (SessionType::Work, SessionMode::Commitment)
464 )
465 }
466}
467
468#[cfg(test)]
469mod tests {
470 use super::*;
471 use serde_json::json;
472 use tdln_brain::MockBackend;
473
474 #[tokio::test]
475 async fn state_transitions() {
476 let backend = MockBackend::with_intent("noop", json!({}));
477 let (mut office, rx) = Office::new(OfficeConfig::default(), backend);
478
479 assert_eq!(office.state(), OfficeState::Opening);
480 assert_eq!(*rx.borrow(), OfficeState::Opening);
481
482 office.open().await.unwrap();
483 assert_eq!(office.state(), OfficeState::Active);
484 }
485
486 #[tokio::test]
487 async fn step_produces_intent() {
488 let backend = MockBackend::with_intent("greet", json!({"name": "alice"}));
489 let (mut office, _) = Office::new(OfficeConfig::default(), backend);
490 office.open().await.unwrap();
491
492 let intent = office.step(Some("say hello")).await.unwrap();
493 assert!(intent.is_some());
494 assert_eq!(intent.unwrap().kind, "greet");
495 }
496
497 #[tokio::test]
498 async fn dream_consolidates() {
499 let backend = MockBackend::with_intent("noop", json!({}));
500 let (mut office, _) = Office::new(OfficeConfig::default(), backend);
501 office.open().await.unwrap();
502
503 for i in 0..80 {
505 office.memory.remember(format!("event {i}"));
506 }
507 let before = office.memory.short_term_len();
508
509 office.dream().await.unwrap();
510
511 assert!(office.memory.short_term_len() < before);
513 }
514
515 #[tokio::test]
516 async fn metrics_increment() {
517 let backend = MockBackend::with_intent("test", json!({}));
518 let (mut office, _) = Office::new(OfficeConfig::default(), backend);
519 office.open().await.unwrap();
520
521 office.step(Some("test")).await.unwrap();
522 assert_eq!(office.metrics().steps_total, 1);
523 assert_eq!(office.metrics().decisions_total, 1);
524 }
525}