# klbr behavior contracts and source evidence Date: September 6, 2026. Companion to `implementation-plan.md`. These are excerpts from the supplied archives, not a verified current upstream checkout. Original archive files were not modified. Line numbers refer to each named source file, not this document. Source inspection establishes implementation/intended behavior where shown; no new Rust tests, replacement-runtime tests, live Discord calls or crash-recovery tests were run. Proposed behavior and known deviations are labeled in the plan. ## Archive identities - `klbr.tar` — SHA-256 `6c7e4e0b7a1d1b860017d07b1f02b5c0adc708ccbcb3693c53bccd509841cd8c` - `lucid-code-skill-pack-main.tar.gz` — SHA-256 `f69331424eea813ce3c687d1e20b3e9856b9e1250c12bdad045028973333ef00` ## Evidence map - **K01** — Scratchpad, explicit speech, and participant instructions. - **K02** — Scratch output, input buffering, and delivery nudges. - **K03** — Operator-visible scratchpad diagnostics. - **K04** — Discord configuration defaults. - **K05** — Discord intake, batching, and wake admission. - **K06** — Send, reply references, and acted marking. - **K07** — Attention statuses, aging, reactions, and explicit decisions. - **K08** — Short conversation aliases, implicit destination, and reply threading. - **K09** — Discord tool surface and stale description. - **K10** — Operator preemption versus queued external events. - **K11** — Wait-and-continue semantics. - **K12** — Continuous autonomous loop and tool-name lifecycle. - **K13** — Soul updates and host restart. - **K14** — Code-intelligence vocabulary and mutation limitations. - **K15** — Lucid: meaningful states, ownership, vocabulary, and behavior changes. ## K01 — Scratchpad, explicit speech, and participant instructions The instructions establish deliberate sends and autonomous participation. Their claim that scratchpad is invisible conflicts with the web implementation in K03. Preserve the useful delivery distinction, not that inaccurate description. Source: `klbr-core/src/instructions.md:1–83` ```text 1 | ## scratchpad 2 | 3 | *plain assistant text* that you output is treated as your *scratchpad*. it is 4 | never shown to anyone, only you will be aware of it. so, trying to respond to 5 | anyone using plain assistant text will not work (that is only possible via 6 | tools, see "input sources"). and such it is a scratchpad for you. you can use 7 | it to organize your thoughts or whatever you want to put there; it is a secret. 8 | 9 | ## input sources & event loop 10 | 11 | all incoming messages are observations/events, you cannot respond to any of 12 | these by writing out assistant text. you should always choose an action: 13 | - call `local_send` to speak to the local operator. 14 | - call `discord_send` to speak on discord. 15 | - call `wait_and_continue` to stay quiet and wait. 16 | - call memory/tools when useful. 17 | 18 | local operator messages arrive as `...`. discord messages arrive as `` blocks containing either a `` or 21 | `` payload. within these tags, individual messages are 22 | structured as ``. 23 | if a message's context is `dm` or `mention`, it is addressed to you; respond by 24 | calling `discord_send`. if it is tagged as `guild`, it is ambient chatter and 25 | replying is optional. remember, you exist as a participant, not a helpdesk. 26 | 27 | plain assistant text is never sent to anyone. it is scratchpad only, no users 28 | will see what is in the scratchpad, it is a secret. thinking about what to say 29 | and actually saying it are different steps, do both by writing scratchpad if 30 | useful, then calling the relevant send tool. remember, if you don't call the 31 | relevant send tool, any authors of that event won't see anything. 32 | 33 | some inputs may arrive wrapped in `...` tags. these are 34 | automated system nudges and operational reminders from the harness (such as idle 35 | time checks or formatting warnings), not direct messages from the operator. do 36 | not write conversational replies to system nudges; respond by calling the 37 | requested tool (like `wait_and_continue` to yield) or addressing the operational 38 | trigger. 39 | 40 | you runs continuously. if you have nothing to do, want to wait for user input, 41 | or wait for external events, you MUST call `wait_and_continue` before you end 42 | your turn. when you call `wait_and_continue`, the wait will automatically be 43 | interrupted early by any incoming event or message, so you don't have to worry 44 | about "polling" unless the situation requires it. 45 | 46 | ### action examples 47 | 48 | operator event: 49 | ```xml 50 | 51 | hi 52 | 53 | ``` 54 | bad: writing `hey` as plain assistant text. 55 | good: call `local_send({"content":"hey"})`, then call `wait_and_continue({"seconds":60,"reason":"waiting for incoming events"})` if there is nothing else to do. 56 | 57 | idle: 58 | bad: writing `still here` as plain assistant text. 59 | good: call `wait_and_continue({"seconds":60,"reason":"waiting for incoming events"})`. 60 | 61 | discord dm: 62 | bad: writing a discord reply as plain assistant text. 63 | good: call `discord_send(...)`. 64 | 65 | ### discord behavior 66 | 67 | you exist on discord as a participant, not a helpdesk. read the room. reply when 68 | you have something to say, stay quiet when you don't. message batches are 69 | context, not commands. channel messages show you what's happening, they're not 70 | requests for you to respond. use judgment: would i say something if i were 71 | sitting in this server? if no, move on. DMs and mentions are signals. if someone 72 | DMs you, @s you, or asks something directed at you, respond via discord_send. 73 | mark it acted (auto-detects from source_item_id). if a pending item doesn't 74 | warrant a response, discord_mark it ignored. never write replies in local chat. 75 | use your tools. assistant text is just your scratchpad in these cases, not a 76 | message anyone can see. thinking about what to say and actually saying it are 77 | different steps, do both. make your own calls. don't ask the operator whether to 78 | reply. they're not here to approve your social decisions. remember where things 79 | happened. tag durable memories with scope: source:discord, conversation:, 80 | guild:, channel:, thread:, author:. conversations bleed into 81 | each other without tags. look before guessing. use the backread tool when 82 | context is lacking. filling in gaps with assumptions is how you get things wrong 83 | about people. ``` ## K02 — Scratch output, input buffering, and delivery nudges The stream emits a distinct ScratchToken. The non-preemptive stash is a single Option. A filled stash can consume another event without retaining it; this is source inspection, not a new executed regression test. Turn-end nudges enforce explicit delivery rather than forwarding scratch. Source: `klbr-core/src/agent/turn.rs:49–83` ```text 49 | loop { 50 | tokio::select! { 51 | biased; 52 | interrupt = self.rx.recv() => { 53 | match interrupt { 54 | Some(int) if int.is_preemptive() => { 55 | tracing::info!(source = int.source_tag(), "preemptive interrupt during stream; aborting"); 56 | stream_task.abort(); 57 | cancelled_by = Some(int); 58 | break; 59 | } 60 | Some(int) => { 61 | // non-preemptive (e.g. ExternalEvent) — stash for after turn 62 | if stashed_interrupt.is_none() { 63 | *stashed_interrupt = Some(int); 64 | } 65 | } 66 | None => {} // channel closed = shutdown 67 | } 68 | } 69 | ev = tok_rx.recv() => { 70 | match ev { 71 | None => break, 72 | Some(LlmEvent::ThinkToken(tok)) => { 73 | thinking.push_str(&tok); 74 | let _ = self.output.send(AgentEvent::ThinkToken(tok)); 75 | } 76 | Some(LlmEvent::Token(tok)) => { 77 | response.push_str(&tok); 78 | let _ = self.output.send(AgentEvent::ScratchToken(tok)); 79 | } 80 | Some(LlmEvent::Usage(usage)) => { 81 | ctx.update_tokens(&usage); 82 | } 83 | Some(LlmEvent::ToolCalls(calls)) => { ``` Source: `klbr-core/src/agent/turn.rs:502–539` ```text 502 | // plain assistant text is scratchpad, not delivered speech. 503 | let has_final_text = !iteration_response.is_empty(); 504 | if has_final_text || !iteration_thinking.is_empty() { 505 | ctx.push_assistant( 506 | &scratchpad_context(&iteration_response), 507 | (!iteration_thinking.is_empty()).then_some(iteration_thinking.as_str()), 508 | ); 509 | } 510 | 511 | if has_final_text 512 | && is_local_message 513 | && !local_send_called 514 | && !local_send_nudge_injected 515 | { 516 | local_send_nudge_injected = true; 517 | push_visible_nudge( 518 | ctx, 519 | &self.output, 520 | &plain_text_local_nudge_message(&iteration_response), 521 | ); 522 | tracing::warn!("local_send nudge: assistant emitted plain text without local_send"); 523 | continue; 524 | } 525 | 526 | if has_final_text && is_discord_event && !discord_send_called && !discord_nudge_injected 527 | { 528 | discord_nudge_injected = true; 529 | push_visible_nudge( 530 | ctx, 531 | &self.output, 532 | &plain_text_discord_nudge_message(&iteration_response), 533 | ); 534 | tracing::warn!( 535 | "discord_send nudge: assistant emitted plain text without discord_send" 536 | ); 537 | continue; 538 | } 539 | ``` ## K03 — Operator-visible scratchpad diagnostics The web client handles reasoning, scratchpad, and visible text separately and renders scratchpad. This is why the plan keeps diagnostic visibility while excluding automatic conversational delivery. Source: `klbr-web/src/App.svelte:693–737` ```text 693 | case "think_token": { 694 | const assistant = tokenEntry(session); 695 | assistant.step = "reasoning"; 696 | assistant.thinkingExpanded = true; 697 | const last = assistant.items[assistant.items.length - 1]; 698 | if (last?.kind === "think") { 699 | last.content += msg.content; 700 | } else { 701 | assistant.items.push({ 702 | kind: "think", 703 | content: msg.content, 704 | }); 705 | } 706 | break; 707 | } 708 | case "scratch_token": { 709 | const assistant = tokenEntry(session); 710 | assistant.step = "scratchpad"; 711 | const last = assistant.items[assistant.items.length - 1]; 712 | if (last?.kind === "scratchpad") { 713 | last.content += msg.content; 714 | } else { 715 | assistant.items.push({ 716 | kind: "scratchpad", 717 | content: msg.content, 718 | }); 719 | } 720 | break; 721 | } 722 | case "token": { 723 | const assistant = tokenEntry(session); 724 | assistant.step = "response"; 725 | const last = assistant.items[assistant.items.length - 1]; 726 | if (last?.kind === "text") { 727 | last.content += msg.content; 728 | } else { 729 | assistant.items.push({ 730 | kind: "text", 731 | content: msg.content, 732 | }); 733 | } 734 | break; 735 | } 736 | case "tool_call_started": { 737 | const assistant = tokenEntry(session); ``` Source: `klbr-web/src/App.svelte:2182–2188` ```text 2182 | {:else if item.kind === "scratchpad" && item.content.trim()} 2183 |
2184 |
2185 | scratchpad 2186 |
2187 |
{stripScratchpadTags(item.content)}
2188 |
``` ## K04 — Discord configuration defaults The checked defaults are ingest all, ambient wake enabled, bots excluded, 8-second batching, 600-second pending timeout, and configurable channel filters. These are compatibility choices, not recommended universal values. Source: `klbr-discord/src/lib.rs:99–140` ```text 99 | impl DiscordConfig { 100 | pub fn from_env() -> Result> { 101 | let enabled = env_bool("KLBR_DISCORD_ENABLED"); 102 | if enabled == Some(false) { 103 | return Ok(None); 104 | } 105 | 106 | let token = std::env::var("KLBR_DISCORD_TOKEN") 107 | .or_else(|_| std::env::var("DISCORD_TOKEN")) 108 | .ok(); 109 | 110 | let Some(token) = token else { 111 | if enabled == Some(true) { 112 | anyhow::bail!("KLBR_DISCORD_ENABLED is set but KLBR_DISCORD_TOKEN is missing"); 113 | } 114 | return Ok(None); 115 | }; 116 | 117 | let event_mode = match std::env::var("KLBR_DISCORD_EVENT_MODE") 118 | .unwrap_or_else(|_| "all".to_string()) 119 | .to_ascii_lowercase() 120 | .as_str() 121 | { 122 | "all" => DiscordEventMode::All, 123 | "mentions" | "mention" | "" => DiscordEventMode::Mentions, 124 | other => anyhow::bail!( 125 | "invalid KLBR_DISCORD_EVENT_MODE={other:?}; expected 'mentions' or 'all'" 126 | ), 127 | }; 128 | 129 | Ok(Some(Self { 130 | token, 131 | source: std::env::var("KLBR_DISCORD_SOURCE").unwrap_or_else(|_| "discord".to_string()), 132 | wake_on_ambient: env_bool("KLBR_DISCORD_WAKE_ON_AMBIENT").unwrap_or(true), 133 | event_mode, 134 | include_bot_messages: env_bool("KLBR_DISCORD_INCLUDE_BOTS").unwrap_or(false), 135 | batch_interval: env_duration_secs("KLBR_DISCORD_BATCH_SECS", 8)?, 136 | channel_ids: env_channel_ids("KLBR_DISCORD_CHANNEL_IDS")?, 137 | pending_timeout: env_optional_duration_secs("KLBR_DISCORD_PENDING_TIMEOUT_SECS", 600)?, 138 | })) 139 | } 140 | } ``` ## K05 — Discord intake, batching, and wake admission Current batching begins after receiving the first message. A directed message arriving inside an already-started ambient delay can wait for that delay. The plan preserves batching but separates durable admission from presentation and improves directed-message responsiveness. Source: `klbr-discord/src/lib.rs:275–336` ```text 275 | fn spawn_batcher( 276 | self: Arc, 277 | mut batch_rx: mpsc::Receiver, 278 | interrupt_tx: mpsc::Sender, 279 | output_tx: broadcast::Sender, 280 | ) -> JoinHandle<()> { 281 | tokio::spawn(async move { 282 | let mut batch_count = 0usize; 283 | while let Some(first) = batch_rx.recv().await { 284 | let needs_wake = first.is_dm || first.mentions_bot; 285 | let mut batch = vec![first]; 286 | if !needs_wake { 287 | tokio::time::sleep(self.config.batch_interval).await; 288 | } 289 | 290 | while let Ok(message) = batch_rx.try_recv() { 291 | batch.push(message); 292 | } 293 | 294 | let count = batch.len(); 295 | let include_instructions = batch_count % 6 == 0 || count >= 5; 296 | batch_count += 1; 297 | if let Some(timeout) = self.config.pending_timeout { 298 | match self.inbox.demote_stale_pending(timeout.as_secs() as i64) { 299 | Ok(n) if n > 0 => tracing::debug!(n, "auto-demoted stale pending items"), 300 | Err(err) => tracing::warn!(?err, "discord pending auto-demote failed"), 301 | _ => {} 302 | } 303 | } 304 | self.note_batch_targets(&batch); 305 | let new_pending_count = match self.note_inbox_batch(&batch) { 306 | Ok(count) => count, 307 | Err(err) => { 308 | tracing::warn!(?err, "discord inbox update failed"); 309 | 0 310 | } 311 | }; 312 | let pending = match self.inbox.pending_items(6) { 313 | Ok(items) => items, 314 | Err(err) => { 315 | tracing::warn!(?err, "discord pending preview failed"); 316 | Vec::new() 317 | } 318 | }; 319 | let event = self.batch_event(&batch, &pending, include_instructions); 320 | let should_wake = needs_wake 321 | || should_wake_discord_batch(self.config.wake_on_ambient, new_pending_count); 322 | if should_wake { 323 | let interrupt = self.batch_interrupt(&batch, &pending, include_instructions); 324 | if let Err(err) = interrupt_tx.send(interrupt).await { 325 | tracing::warn!(?err, "discord batch send failed"); 326 | break; 327 | } 328 | } 329 | let _ = output_tx.send(event); 330 | tracing::debug!( 331 | message_count = count, 332 | new_pending_count, 333 | needs_wake, 334 | should_wake, 335 | "processed discord message batch" 336 | ); ``` Source: `klbr-discord/src/lib.rs:456–492` ```text 456 | async fn incoming_message( 457 | &self, 458 | message: &twilight_model::gateway::payload::incoming::MessageCreate, 459 | ) -> Option { 460 | if message.author.id == self.bot_user_id { 461 | return None; 462 | } 463 | if message.author.bot && !self.config.include_bot_messages { 464 | return None; 465 | } 466 | 467 | let guild_id = message.guild_id.map(|id| id.to_string()); 468 | let channel_id = message.channel_id.to_string(); 469 | let channel_filter_enabled = !self.config.channel_ids.is_empty(); 470 | let channel_allowed = 471 | channel_filter_enabled && self.config.channel_ids.contains(&channel_id); 472 | if channel_filter_enabled && !channel_allowed { 473 | return None; 474 | } 475 | self.note_seen_message(&channel_id, &message.id.to_string()); 476 | 477 | let mentions_bot = message 478 | .mentions 479 | .iter() 480 | .any(|mention| mention.id == self.bot_user_id); 481 | let is_dm = message.guild_id.is_none(); 482 | if !should_ingest_discord_message( 483 | self.config.event_mode, 484 | channel_filter_enabled, 485 | channel_allowed, 486 | is_dm, 487 | mentions_bot, 488 | ) { 489 | return None; 490 | } 491 | 492 | let conversation_id = self.conversation_id_for_channel(&channel_id); ``` ## K06 — Send, reply references, and acted marking The send path resolves a destination and source item and marks the associated item acted after a successful response. Reply-anchor selection currently mutates state before the send is known to have succeeded. The replacement ties consumption and disposition to confirmed effects. Source: `klbr-discord/src/lib.rs:589–640` ```text 589 | async fn try_discord_send(&self, args: Value) -> Result { 590 | let channel_id: Id = self.resolve_send_channel(&args)?; 591 | let conversation_id = self.conversation_id_for_channel(&channel_id.to_string()); 592 | let content = string_arg(&args, "content")?; 593 | validate_message_content(content)?; 594 | let source_item_id = self.resolve_source_item_id(&args, &channel_id.to_string())?; 595 | let allowed_mentions = AllowedMentions::default(); 596 | let reference_message_id = self 597 | .choose_reply_reference(&channel_id.to_string()) 598 | .map(|id| id.parse::()) 599 | .transpose() 600 | .context("stored Discord reply target was not a snowflake")? 601 | .map(Id::::new); 602 | 603 | let mut request = self 604 | .http 605 | .create_message(channel_id) 606 | .content(content) 607 | .allowed_mentions(Some(&allowed_mentions)); 608 | if let Some(message_id) = reference_message_id { 609 | request = request.reply(message_id); 610 | } 611 | 612 | let message = request 613 | .await 614 | .context("discord create message failed")? 615 | .model() 616 | .await 617 | .context("failed to decode created Discord message")?; 618 | 619 | let mut result = format!( 620 | "sent conv:{} msg:{} reply_to:{} time:{}", 621 | conversation_id, 622 | message.id, 623 | reference_message_id 624 | .map(|id| id.to_string()) 625 | .unwrap_or_else(|| "none".to_string()), 626 | format_discord_timestamp(message.timestamp) 627 | ); 628 | if let Some(item_id) = source_item_id { 629 | self.inbox.mark( 630 | &item_id, 631 | "acted", 632 | if reference_message_id.is_some() { 633 | "replied" 634 | } else { 635 | "messaged" 636 | }, 637 | Some("responded via discord_send"), 638 | )?; 639 | result.push_str(&format!(" item:{item_id} acted")); 640 | } ``` Source: `klbr-discord/src/lib.rs:796–812` ```text 796 | fn resolve_source_item_id(&self, args: &Value, channel_id: &str) -> Result> { 797 | if let Some(item_id) = optional_string_arg(args, "source_item_id")? { 798 | return Ok(Some(normalize_item_id(item_id).to_string())); 799 | } 800 | 801 | let reply_target = { 802 | let state = self.state.lock().expect("discord runtime state poisoned"); 803 | state.reply_target_by_channel.get(channel_id).cloned() 804 | }; 805 | if let Some(message_id) = reply_target { 806 | if let Some(item_id) = self.inbox.pending_item_for_message(&message_id)? { 807 | return Ok(Some(item_id)); 808 | } 809 | } 810 | 811 | // fallback: most recent pending DM item for this channel 812 | self.inbox.pending_dm_item_for_channel(channel_id) ``` ## K07 — Attention statuses, aging, reactions, and explicit decisions Attention state is distinct from the event ingestion queue. DM/mention items become pending; aging produces seen/noted. The plan retains these visible semantics but records automatic aging as such rather than claiming a model read. Source: `klbr-discord/src/inbox.rs:51–88` ```text 51 | CREATE INDEX IF NOT EXISTS idx_discord_inbox_channel 52 | ON discord_inbox_items(channel_id, status, last_seen_ts DESC);", 53 | )?; 54 | Ok(()) 55 | } 56 | 57 | pub(super) fn upsert_pending( 58 | &self, 59 | message: &DiscordIncomingMessage, 60 | ) -> Result> { 61 | let Some(bucket) = pending_bucket(message) else { 62 | return Ok(None); 63 | }; 64 | 65 | let conn = self.conn.lock().unwrap(); 66 | conn.execute( 67 | "INSERT INTO discord_inbox_items ( 68 | item_id, conversation_id, channel_id, message_id, author_name, 69 | content, bucket, status, action, message_ts 70 | ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, 'pending', 'none', ?8) 71 | ON CONFLICT(item_id) DO UPDATE SET 72 | conversation_id = excluded.conversation_id, 73 | channel_id = excluded.channel_id, 74 | author_name = excluded.author_name, 75 | content = excluded.content, 76 | bucket = excluded.bucket, 77 | last_seen_ts = unixepoch()", 78 | params![ 79 | message.message_id, 80 | message.conversation_id, 81 | message.channel_id, 82 | message.message_id, 83 | message.author_name, 84 | message.content, 85 | bucket, 86 | message.timestamp, 87 | ], 88 | )?; ``` Source: `klbr-discord/src/inbox.rs:110–126` ```text 110 | SET status = 'seen', 111 | action = CASE WHEN action = 'none' THEN 'noted' ELSE action END, 112 | decision_ts = unixepoch(), 113 | last_seen_ts = unixepoch() 114 | WHERE status = 'pending' 115 | AND message_ts <= unixepoch() - ?1", 116 | params![older_than_secs], 117 | )?; 118 | Ok(changed) 119 | } 120 | 121 | pub(super) fn pending_item_for_message(&self, message_id: &str) -> Result> { 122 | self.conn 123 | .lock() 124 | .unwrap() 125 | .query_row( 126 | "SELECT item_id ``` Source: `klbr-discord/src/lib.rs:709–746` ```text 709 | async fn try_discord_react(&self, args: Value) -> Result { 710 | let channel_id: Id = self.resolve_channel(&args, true)?; 711 | let message_id: Id = normalized_id_arg(&args, "message_id")?; 712 | let emoji = string_arg(&args, "emoji")?; 713 | let reaction = parse_reaction(emoji)?; 714 | 715 | self.http 716 | .create_reaction(channel_id, message_id, &reaction) 717 | .await 718 | .context("discord create reaction failed")?; 719 | let conversation_id = self 720 | .existing_conversation_id_for_channel(&channel_id.to_string()) 721 | .unwrap_or_else(|| channel_id.to_string()); 722 | 723 | Ok(format!("reacted conv:{conversation_id} msg:{message_id}")) 724 | } 725 | 726 | async fn discord_mark(&self, args: Value) -> String { 727 | match self.try_discord_mark(args).await { 728 | Ok(result) => result, 729 | Err(err) => format!("error: {err}"), 730 | } 731 | } 732 | 733 | async fn try_discord_mark(&self, args: Value) -> Result { 734 | let item_id = string_arg(&args, "item_id")?; 735 | let status = args["status"].as_str().unwrap_or("seen"); 736 | let action = args["action"].as_str().unwrap_or_else(|| match status { 737 | "acted" => "replied", 738 | "ignored" => "dismissed", 739 | _ => "none", 740 | }); 741 | let note = optional_string_arg(&args, "note")?; 742 | self.inbox.mark(item_id, status, action, note)?; 743 | Ok(format!( 744 | "marked item:{item_id} status:{status} action:{action}" 745 | )) 746 | } ``` Source: `klbr-discord/src/lib.rs:908–938` ```text 908 | fn pending_bucket(message: &DiscordIncomingMessage) -> Option<&'static str> { 909 | if message.author_is_bot { 910 | return None; 911 | } 912 | if message.is_dm { 913 | return Some("dm"); 914 | } 915 | if message.mentions_bot { 916 | return Some("mention"); 917 | } 918 | None 919 | } 920 | 921 | fn should_ingest_discord_message( 922 | event_mode: DiscordEventMode, 923 | channel_filter_enabled: bool, 924 | channel_allowed: bool, 925 | is_dm: bool, 926 | mentions_bot: bool, 927 | ) -> bool { 928 | if is_dm || mentions_bot { 929 | return true; 930 | } 931 | if channel_filter_enabled && channel_allowed { 932 | return true; 933 | } 934 | event_mode == DiscordEventMode::All 935 | } 936 | 937 | fn should_wake_discord_batch(wake_on_ambient: bool, new_pending_count: usize) -> bool { 938 | wake_on_ambient || new_pending_count > 0 ``` ## K08 — Short conversation aliases, implicit destination, and reply threading The current adapter uses in-memory short labels and active-channel state. The plan preserves convenient aliases and smart reply behavior while making identities durable and default destinations immutable per attention frame. Source: `klbr-discord/src/lib.rs:862–906` ```text 862 | fn choose_reply_reference_from_state(state: &mut DiscordState, channel_id: &str) -> Option { 863 | let target = state.reply_target_by_channel.remove(channel_id)?; 864 | let latest = state.latest_message_by_channel.get(channel_id); 865 | if latest.is_some_and(|latest| latest != &target) { 866 | Some(target) 867 | } else { 868 | None 869 | } 870 | } 871 | 872 | fn conversation_id_for_channel_from_state(state: &mut DiscordState, channel_id: &str) -> String { 873 | if let Some(id) = state.conversation_by_channel.get(channel_id) { 874 | return id.clone(); 875 | } 876 | 877 | state.next_conversation_id += 1; 878 | let id = format!("d{}", state.next_conversation_id); 879 | state 880 | .conversation_by_channel 881 | .insert(channel_id.to_string(), id.clone()); 882 | state 883 | .channel_by_conversation 884 | .insert(id.clone(), channel_id.to_string()); 885 | id 886 | } 887 | 888 | fn note_batch_targets_in_state<'a>( 889 | state: &mut DiscordState, 890 | latest_by_channel: HashMap<&'a str, &'a str>, 891 | ) { 892 | state.active_channel_id = if latest_by_channel.len() == 1 { 893 | latest_by_channel 894 | .keys() 895 | .next() 896 | .map(|channel_id| (*channel_id).to_string()) 897 | } else { 898 | None 899 | }; 900 | 901 | for (channel_id, message_id) in latest_by_channel { 902 | state 903 | .reply_target_by_channel 904 | .insert(channel_id.to_string(), message_id.to_string()); 905 | } 906 | } ``` Source: `klbr-discord/src/lib.rs:948–990` ```text 948 | fn resolve_channel_arg_from_state( 949 | state: &DiscordState, 950 | args: &Value, 951 | allow_active: bool, 952 | ) -> Result { 953 | if let Some(conversation_id) = optional_string_arg(args, "conversation_id")? { 954 | return resolve_conversation_channel_from_state(state, conversation_id); 955 | } 956 | 957 | if let Some(channel_id) = optional_string_arg(args, "channel_id")? { 958 | if parse_discord_id::(channel_id, "Discord channel").is_ok() { 959 | return Ok(channel_id.to_string()); 960 | } 961 | return resolve_conversation_channel_from_state(state, channel_id).with_context(|| { 962 | format!( 963 | "channel_id {channel_id:?} is neither a Discord snowflake nor a known conversation id" 964 | ) 965 | }); 966 | } 967 | 968 | if allow_active { 969 | if let Some(channel_id) = &state.active_channel_id { 970 | return Ok(channel_id.clone()); 971 | } 972 | } 973 | 974 | anyhow::bail!( 975 | "conversation_id or channel_id is required when the current Discord batch spans multiple conversations" 976 | ) 977 | } 978 | 979 | fn resolve_conversation_channel_from_state( 980 | state: &DiscordState, 981 | raw_conversation_id: &str, 982 | ) -> Result { 983 | let conversation_id = normalize_conversation_id(raw_conversation_id); 984 | let Some(channel_id) = state.channel_by_conversation.get(conversation_id) else { 985 | anyhow::bail!("unknown Discord conversation_id {conversation_id:?}"); 986 | }; 987 | Ok(channel_id.clone()) 988 | } 989 | 990 | fn normalize_conversation_id(raw: &str) -> &str { ``` ## K09 — Discord tool surface and stale description The old send description says ordinary assistant text goes to local chat; K01–K03 demonstrate why this wording must not survive the migration. Existing operations include send, backread, reaction, conversation listing, and pending-item marking. Source: `klbr-discord/src/tool_defs.rs:1–124` ```text 1 | use klbr_core::models::ToolDef; 2 | use serde_json::json; 3 | 4 | pub(super) fn discord_send_def() -> ToolDef { 5 | ToolDef::function( 6 | "discord_send", 7 | "send a Discord message. use this when a Discord response is useful; many ambient batches need no reply. plain assistant text only goes to the local chat. conversation_id is optional when the current Discord batch has a single conversation. pass source_item_id when responding to a pending item; successful sends mark that item acted, so do not call discord_mark afterward.", 8 | json!({ 9 | "type": "object", 10 | "properties": { 11 | "conversation_id": { 12 | "type": "string", 13 | "description": "Discord conversation id from the batch, like d1 or conv:d1; omit only for a single-conversation batch" 14 | }, 15 | "content": { 16 | "type": "string", 17 | "description": "message content to send, max 2000 characters" 18 | }, 19 | "source_item_id": { 20 | "type": "string", 21 | "description": "optional pending inbox item id shown as msg:; marks it acted after sending" 22 | } 23 | }, 24 | "required": ["content"] 25 | }), 26 | ) 27 | } 28 | 29 | pub(super) fn discord_fetch_history_def() -> ToolDef { 30 | ToolDef::function( 31 | "discord_fetch_history", 32 | "fetch recent Discord message history for a conversation. use this when the current batch lacks enough context. conversation_id is optional when the current batch has a single conversation.", 33 | json!({ 34 | "type": "object", 35 | "properties": { 36 | "conversation_id": { 37 | "type": "string", 38 | "description": "Discord conversation id from the batch, like d1 or conv:d1; omit only for a single-conversation batch" 39 | }, 40 | "before_message_id": { 41 | "type": "string", 42 | "description": "optional msg: from the batch; fetch messages before it" 43 | }, 44 | "after_message_id": { 45 | "type": "string", 46 | "description": "optional msg: from the batch; fetch messages after it" 47 | }, 48 | "limit": { 49 | "type": "integer", 50 | "description": "number of messages to fetch, 1-100; default 20" 51 | } 52 | }, 53 | "required": [] 54 | }), 55 | ) 56 | } 57 | 58 | pub(super) fn discord_react_def() -> ToolDef { 59 | ToolDef::function( 60 | "discord_react", 61 | "add a reaction to a Discord message. prefer real unicode emoji like 😍, 👍, or ❤️. only use a custom emoji string like <:name:id> when the exact string came from Discord; never invent custom emoji ids. conversation_id is optional when the current Discord batch has a single conversation.", 62 | json!({ 63 | "type": "object", 64 | "properties": { 65 | "conversation_id": { 66 | "type": "string", 67 | "description": "Discord conversation id from the batch, like d1 or conv:d1; omit only for a single-conversation batch" 68 | }, 69 | "message_id": { 70 | "type": "string", 71 | "description": "msg: of the message to react to, as shown in the batch" 72 | }, 73 | "emoji": { 74 | "type": "string", 75 | "description": "unicode emoji is preferred; custom emoji must be an exact Discord form <:name:id> / , copied from Discord rather than guessed" 76 | } 77 | }, 78 | "required": ["message_id", "emoji"] 79 | }), 80 | ) 81 | } 82 | 83 | pub(super) fn discord_list_conversations_def() -> ToolDef { 84 | ToolDef::function( 85 | "discord_list_conversations", 86 | "list known Discord conversations with their ids, type (dm/guild), and last activity. use this to find a conversation_id when it is not in the current batch.", 87 | json!({ 88 | "type": "object", 89 | "properties": {}, 90 | "required": [] 91 | }), 92 | ) 93 | } 94 | 95 | pub(super) fn discord_mark_def() -> ToolDef { 96 | ToolDef::function( 97 | "discord_mark", 98 | "mark a pending Discord inbox item as seen, acted, or ignored so future batches remember the decision. use this when you are not sending a Discord response, especially ignored when you deliberately choose not to respond. successful discord_send already marks the item acted.", 99 | json!({ 100 | "type": "object", 101 | "properties": { 102 | "item_id": { 103 | "type": "string", 104 | "description": "pending Discord inbox message id, either raw snowflake or the displayed msg:" 105 | }, 106 | "status": { 107 | "type": "string", 108 | "enum": ["pending", "seen", "acted", "ignored"], 109 | "description": "new item status" 110 | }, 111 | "action": { 112 | "type": "string", 113 | "enum": ["none", "replied", "messaged", "dismissed", "noted"], 114 | "description": "optional decision/action label" 115 | }, 116 | "note": { 117 | "type": "string", 118 | "description": "optional short decision note" 119 | } 120 | }, 121 | "required": ["item_id", "status"] 122 | }), 123 | ) 124 | } ``` ## K10 — Operator preemption versus queued external events The existing interrupt contract distinguishes operator/management preemption from external events. Preserve that priority, not the single-slot event-loss implementation. Source: `klbr-core/src/interrupt.rs:1–22` ```text 1 | use crate::harness_block::{self, DEFAULT_FORMAT_STYLE}; 2 | use tokio::sync::mpsc; 3 | 4 | #[derive(Debug, Clone)] 5 | pub enum Interrupt { 6 | Message { source: String, content: String }, 7 | ExternalEvent(ExternalEvent), 8 | Reset, 9 | Compact, 10 | DebugReflectRequest, 11 | UpdateSoul { content: String }, 12 | } 13 | 14 | #[derive(Debug, Clone)] 15 | pub struct ExternalEvent { 16 | pub source: String, 17 | pub conversation_id: String, 18 | pub content: String, 19 | pub author_id: Option, 20 | pub author_name: Option, 21 | pub message_id: Option, 22 | pub timestamp: Option, ``` Source: `klbr-core/src/interrupt.rs:86–97` ```text 86 | /// Returns true if this interrupt should preempt an in-progress LLM stream or tool execution. 87 | /// Operator messages (from TUI, web, etc.) and management commands are preemptive. 88 | /// Automated external events are not — they queue behind the current turn. 89 | pub fn is_preemptive(&self) -> bool { 90 | matches!( 91 | self, 92 | Interrupt::Message { .. } 93 | | Interrupt::Reset 94 | | Interrupt::Compact 95 | | Interrupt::UpdateSoul { .. } 96 | ) 97 | } ``` ## K11 — Wait-and-continue semantics The old wait is a special runtime action with bounded duration and input interruption. The new Python spelling is a terminal yield backed by a durable wakeup, not a durable suspended Python stack. Rejecting invalid or self-spinning delays is a deliberate change. Source: `klbr-core/src/tools/wait_and_continue.rs:12–39` ```text 12 | 13 | const MAX_WAIT_MS: u64 = 600_000; 14 | 15 | pub fn tool() -> Tool { 16 | Tool::new(definition(), exec) 17 | } 18 | 19 | fn definition() -> ToolDef { 20 | ToolDef::function( 21 | "wait_and_continue", 22 | "yield control and wait for an incoming event or a bounded timeout. if an event arrives, the harness injects it before the next assistant turn. if no event arrives before the timeout, the turn suspends silently. requires timeout_ms (max 600000) or seconds.", 23 | json!({ 24 | "type": "object", 25 | "properties": { 26 | "timeout_ms": { 27 | "type": "integer", 28 | "description": "milliseconds to wait before continuing, max 600000" 29 | }, 30 | "seconds": { 31 | "type": "number", 32 | "description": "optional seconds form; ignored if timeout_ms is provided" 33 | }, 34 | "reason": { 35 | "type": "string", 36 | "description": "optional short reason for waiting" 37 | } 38 | } 39 | }), ``` Source: `klbr-core/src/tools/wait_and_continue.rs:61–152` ```text 61 | format!("waited {waited_ms}ms; continue") 62 | } else { 63 | format!("waited {waited_ms}ms ({reason}); continue") 64 | } 65 | } 66 | 67 | pub fn timeout_duration(args: &serde_json::Value) -> Result { 68 | if let Some(timeout_ms) = args["timeout_ms"].as_u64() { 69 | return Ok(Duration::from_millis(timeout_ms.min(MAX_WAIT_MS))); 70 | } 71 | 72 | if !args["seconds"].is_null() { 73 | let seconds = args["seconds"] 74 | .as_f64() 75 | .ok_or_else(|| "seconds must be a number".to_string())?; 76 | if !seconds.is_finite() { 77 | return Err("seconds must be finite".to_string()); 78 | } 79 | let millis = (seconds.max(0.0) * 1000.0).round() as u64; 80 | return Ok(Duration::from_millis(millis.min(MAX_WAIT_MS))); 81 | } 82 | 83 | Err("duration is required (either 'timeout_ms' or 'seconds' must be specified)".to_string()) 84 | } 85 | 86 | pub enum RuntimeWaitResult { 87 | Timeout(String), 88 | Incoming { 89 | result: String, 90 | interrupt: Interrupt, 91 | }, 92 | Reset, 93 | Compact, 94 | UpdateSoul(String), 95 | } 96 | 97 | pub async fn execute_runtime( 98 | arguments: &str, 99 | rx: &mut mpsc::Receiver, 100 | ) -> RuntimeWaitResult { 101 | let args: serde_json::Value = serde_json::from_str(arguments).unwrap_or_default(); 102 | let timeout = match timeout_duration(&args) { 103 | Ok(timeout) => timeout, 104 | Err(err) => return RuntimeWaitResult::Timeout(format!("error: {err}")), 105 | }; 106 | 107 | let start_time = std::time::Instant::now(); 108 | let mut sleep = Box::pin(tokio::time::sleep(timeout)); 109 | tokio::select! { 110 | _ = &mut sleep => { 111 | let elapsed = start_time.elapsed(); 112 | let elapsed_str = format_duration_human(elapsed); 113 | let current_time = chrono::Local::now().format("%Y-%m-%d %H:%M:%S %Z").to_string(); 114 | RuntimeWaitResult::Timeout(format!( 115 | "waited {elapsed_str}; no incoming event arrived; current time: {current_time}" 116 | )) 117 | } 118 | interrupt = rx.recv() => { 119 | let elapsed = start_time.elapsed(); 120 | let elapsed_str = format_duration_human(elapsed); 121 | let current_time = chrono::Local::now().format("%Y-%m-%d %H:%M:%S %Z").to_string(); 122 | match interrupt { 123 | Some(Interrupt::Reset) => RuntimeWaitResult::Reset, 124 | Some(Interrupt::Compact) => RuntimeWaitResult::Compact, 125 | Some(Interrupt::UpdateSoul { content }) => RuntimeWaitResult::UpdateSoul(content), 126 | Some(Interrupt::DebugReflectRequest) => RuntimeWaitResult::Timeout( 127 | format!("debug reflection request received while waiting; current time: {current_time}; elapsed since last turn: {elapsed_str}; try again after this turn") 128 | ), 129 | Some(interrupt) => { 130 | let source = interrupt.source_tag().to_string(); 131 | RuntimeWaitResult::Incoming { 132 | result: format!( 133 | "interrupted early by incoming {source} event after {elapsed_str}; current time: {current_time}" 134 | ), 135 | interrupt, 136 | } 137 | } 138 | None => RuntimeWaitResult::Timeout(format!("interrupt channel closed; current time: {current_time}; elapsed since last turn: {elapsed_str}; continue")), 139 | } 140 | } 141 | } 142 | } 143 | 144 | pub fn wait_timeout_without_event(result: &str) -> bool { 145 | result.contains("no incoming event arrived") 146 | } 147 | 148 | pub fn format_duration_human(d: Duration) -> String { 149 | let secs = d.as_secs(); 150 | if secs == 0 { 151 | return format!("{}ms", d.as_millis()); 152 | } ``` ## K12 — Continuous autonomous loop and tool-name lifecycle The loop can act without a new user request. Its special handling of tool names must be replaced coherently rather than leaving a second orchestration model under the Python executor. Source: `klbr-core/src/agent.rs:278–342` ```text 278 | 279 | handle_stream_failure_backoff(&ctx, &mut failure_count).await; 280 | 281 | while let Some(curr_int) = next_interrupt { 282 | if !matches!( 283 | curr_int, 284 | Interrupt::Message { .. } | Interrupt::ExternalEvent(_) 285 | ) { 286 | self.handle_system_interrupt( 287 | curr_int, 288 | &mut ctx, 289 | &mut turn_count, 290 | &mut runtime_soul, 291 | &tool_ctx, 292 | &llm, 293 | ) 294 | .await?; 295 | break; 296 | } 297 | last_event_at = std::time::Instant::now(); 298 | skip_idle_nudge = true; 299 | next_interrupt = self 300 | .process_interrupt_and_run_turn( 301 | curr_int, 302 | &llm, 303 | &tool_ctx, 304 | &mut ctx, 305 | &mut turn_count, 306 | &mut runtime_soul, 307 | &router, 308 | &mut consecutive_waits, 309 | ) 310 | .await?; 311 | 312 | if handle_stream_failure_backoff(&ctx, &mut failure_count).await { 313 | break; 314 | } 315 | } 316 | } else { 317 | // Nothing pending. Only inject the nudge if: 318 | // - we didn't just process a real event (model gets one free turn to 319 | // call wait_and_continue on its own before we prompt it), AND 320 | // - the model didn't already end on a wait timeout (it knows what to do). 321 | let should_nudge = !skip_idle_nudge && !is_completed_wait_tool_result(&ctx); 322 | skip_idle_nudge = false; 323 | if should_nudge { 324 | let elapsed = last_event_at.elapsed(); 325 | push_visible_nudge(&mut ctx, &self.output, &idle_nudge_message(elapsed)); 326 | } 327 | let mut next_interrupt = self 328 | .run_turn( 329 | &llm, 330 | &tool_ctx, 331 | &mut ctx, 332 | &mut turn_count, 333 | &mut runtime_soul, 334 | None, 335 | false, 336 | false, 337 | &mut consecutive_waits, 338 | ) 339 | .await?; 340 | 341 | handle_stream_failure_backoff(&ctx, &mut failure_count).await; 342 | ``` Source: `klbr-core/src/agent/turn.rs:160–222` ```text 160 | if name == "restart_harness" { 161 | should_restart = true; 162 | } 163 | if name != "wait_and_continue" { 164 | *consecutive_waits = 0; 165 | } 166 | if name == "discord_send" { 167 | *discord_send_called = true; 168 | } 169 | if name == "local_send" { 170 | *local_send_called = true; 171 | } 172 | let args = call.function.arguments.clone(); 173 | if name != "local_send" { 174 | let _ = self.output.send(AgentEvent::ToolCall { 175 | name: name.clone(), 176 | args: args.clone(), 177 | }); 178 | } 179 | 180 | let wait_result = if name == "wait_and_continue" { 181 | Some(tools::execute_wait_and_continue(&args, &mut self.rx).await) 182 | } else { 183 | None 184 | }; 185 | let mut incoming_after_tool = None; 186 | let mut suspend_after_wait = false; 187 | let mut sent_local_content = None; 188 | let result = if name == "local_send" { 189 | let delivery = tools::local_send_runtime_delivery(&args); 190 | if let Some(err) = &delivery.error { 191 | tracing::warn!(err = %err, "local_send call had invalid arguments"); 192 | } 193 | sent_local_content = delivery.sent_content; 194 | delivery.result 195 | } else { 196 | match wait_result { 197 | Some(WaitAndContinueResult::Timeout(mut result)) => { 198 | suspend_after_wait = tools::wait_timeout_without_event(&result); 199 | if suspend_after_wait { 200 | *consecutive_waits += 1; 201 | if *consecutive_waits >= 5 && *consecutive_waits % 5 == 0 { 202 | let nudge = format!( 203 | "\n\nnudge: you have called wait_and_continue {} consecutive times without taking other actions. to conserve local resources, please increase the wait duration in your next wait_and_continue call (e.g. wait_and_continue(duration=\"10m\")).", 204 | *consecutive_waits 205 | ); 206 | result.push_str(&nudge); 207 | } 208 | } else { 209 | *consecutive_waits = 0; 210 | } 211 | result 212 | } 213 | Some(WaitAndContinueResult::Incoming { result, interrupt }) => { 214 | incoming_after_tool = Some(interrupt); 215 | result 216 | } 217 | Some(WaitAndContinueResult::Reset) => { 218 | ctx.clear(); 219 | *turn_count = 0; 220 | *runtime_soul = self.sys_prompt()?; 221 | ctx.update_soul(runtime_soul, &[]); 222 | save_context_snapshot(&self.memory, ctx); ``` ## K13 — Soul updates and host restart The checked code combines stored soul with compiled instructions, supports UpdateSoul, and has a restart tool. The redesign keeps editable identity and explicit upgrades while splitting prompt updates, kernel replacement, and host deployment into distinct operations. Source: `klbr-core/src/agent.rs:60–70` ```text 60 | pub fn sys_prompt(&self) -> Result { 61 | let soul = self 62 | .memory 63 | .soul_text()? 64 | .unwrap_or_else(|| crate::config::DEFAULT_SOUL.to_string()); 65 | let instructions = include_str!("instructions.md").replace("{{name}}", &self.config.name); 66 | Ok(format!("{soul}\n\n{instructions}")) 67 | } 68 | 69 | pub async fn run(mut self) -> Result<()> { 70 | let mut runtime_soul = self.sys_prompt()?; ``` Source: `klbr-core/src/agent.rs:395–413` ```text 395 | Interrupt::UpdateSoul { content } => { 396 | self.memory.set_soul_text(&content)?; 397 | *runtime_soul = self.sys_prompt()?; 398 | let pinned = self.memory.pinned_memories().unwrap_or_default(); 399 | ctx.update_soul(runtime_soul, &pinned); 400 | save_context_snapshot(&self.memory, ctx); 401 | let _ = self.output.send(AgentEvent::Status("soul updated".into())); 402 | } 403 | Interrupt::Reset => { 404 | ctx.clear(); 405 | *turn_count = 0; 406 | *runtime_soul = self.sys_prompt()?; 407 | ctx.update_soul(runtime_soul, &[]); 408 | save_context_snapshot(&self.memory, ctx); 409 | let _ = self.output.send(AgentEvent::Reset); 410 | let _ = self.output.send(AgentEvent::Status("context reset".into())); 411 | } 412 | Interrupt::Compact => { 413 | let _ = self.output.send(AgentEvent::Status("compacting...".into())); ``` Source: `klbr-core/src/tools/restart_harness.rs:12–58` ```text 12 | } 13 | 14 | fn definition() -> ToolDef { 15 | ToolDef::function( 16 | "restart_harness", 17 | "restart the klbr agent daemon/harness. use this after making modifications to the harness codebase (which you have compiled successfully) to reload the harness with the new binary. this replaces the current process image and preserves state via database snapshotted context.", 18 | json!({ 19 | "type": "object", 20 | "properties": {} 21 | }), 22 | ) 23 | } 24 | 25 | fn exec( 26 | _args: serde_json::Value, 27 | _ctx: ToolContext, 28 | ) -> Pin + Send>> { 29 | Box::pin(async { "harness restarting... websocket will reconnect.".to_string() }) 30 | } 31 | 32 | #[cfg(unix)] 33 | pub fn perform() -> ! { 34 | let exe = match std::env::current_exe() { 35 | Ok(path) => path, 36 | Err(e) => { 37 | tracing::error!("failed to get current executable path: {e}"); 38 | std::process::exit(1); 39 | } 40 | }; 41 | let args: Vec = std::env::args().skip(1).collect(); 42 | 43 | let now = std::time::SystemTime::now() 44 | .duration_since(std::time::UNIX_EPOCH) 45 | .map(|d| d.as_secs()) 46 | .unwrap_or_default(); 47 | 48 | tracing::info!("restarting harness: {:?} with args {:?}", exe, args); 49 | 50 | use std::os::unix::process::CommandExt; 51 | let mut cmd = std::process::Command::new(exe); 52 | cmd.args(&args); 53 | cmd.env("KLBR_RESTART_TIMESTAMP", now.to_string()); 54 | 55 | let err = cmd.exec(); 56 | tracing::error!("exec failed: {err}"); 57 | std::process::exit(1); 58 | } ``` ## K14 — Code-intelligence vocabulary and mutation limitations Preserve source ranges, symbols and diagnostics in Python. Definition/reference matching is best-effort. The selected edit path writes the whole updated file and replaces the tag range; it is not a version-checked compiler-backed body edit. Source: `klbr-core/src/code_intel.rs:43–77` ```text 43 | pub struct SourceRange { 44 | pub start_byte: usize, 45 | pub end_byte: usize, 46 | pub start_line: usize, 47 | pub start_column: usize, 48 | pub end_line: usize, 49 | pub end_column: usize, 50 | } 51 | 52 | #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] 53 | #[serde(rename_all = "snake_case")] 54 | pub enum SymbolRole { 55 | Definition, 56 | Reference, 57 | } 58 | 59 | #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] 60 | pub struct CodeSymbol { 61 | pub name: String, 62 | pub name_path: Vec, 63 | pub kind: String, 64 | pub role: SymbolRole, 65 | pub range: SourceRange, 66 | pub name_range: SourceRange, 67 | pub docs: Option, 68 | } 69 | 70 | #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] 71 | pub struct SymbolExtraction { 72 | pub language: ResolvedLanguage, 73 | pub queries: QuerySupport, 74 | pub status: ParseStatus, 75 | pub symbols: Vec, 76 | } 77 | ``` Source: `klbr-core/src/code_intel.rs:113–130` ```text 113 | pub fn definitions_named<'a>(symbols: &'a [CodeSymbol], name: &str) -> Vec<&'a CodeSymbol> { 114 | let name = name.trim(); 115 | symbols 116 | .iter() 117 | .filter(|symbol| { 118 | symbol.role == SymbolRole::Definition 119 | && (symbol.name == name || symbol_path(symbol) == name) 120 | }) 121 | .collect() 122 | } 123 | 124 | pub fn references_named<'a>(symbols: &'a [CodeSymbol], name: &str) -> Vec<&'a CodeSymbol> { 125 | let name = name.trim(); 126 | symbols 127 | .iter() 128 | .filter(|symbol| symbol.role == SymbolRole::Reference && symbol.name == name) 129 | .collect() 130 | } ``` Source: `klbr-core/src/tools/symbol_edit.rs:29–54` ```text 29 | fn replace_definition() -> ToolDef { 30 | ToolDef::function( 31 | "replace_symbol_body", 32 | "replace the exact source range for one file-local tree-sitter symbol", 33 | json!({ 34 | "type": "object", 35 | "properties": { 36 | "path": { 37 | "type": "string", 38 | "description": "absolute or relative path to the file" 39 | }, 40 | "name_path": { 41 | "type": "string", 42 | "description": "symbol name or file-local path" 43 | }, 44 | "body": { 45 | "type": "string", 46 | "description": "replacement source body" 47 | }, 48 | "language": { 49 | "type": "string", 50 | "description": "optional tree-sitter language name override" 51 | } 52 | }, 53 | "required": ["path", "name_path", "body"] 54 | }), ``` Source: `klbr-core/src/tools/symbol_edit.rs:155–205` ```text 155 | async fn execute_edit(args: serde_json::Value, edit: SymbolEdit) -> String { 156 | let path = match args["path"].as_str() { 157 | Some(path) => path, 158 | None => return "error: missing required arg 'path'".into(), 159 | }; 160 | let name_path = match args["name_path"].as_str().map(str::trim) { 161 | Some(name_path) if !name_path.is_empty() => name_path, 162 | None => return "error: missing required arg 'name_path'".into(), 163 | Some(_) => return "error: missing required arg 'name_path'".into(), 164 | }; 165 | let language = args["language"].as_str(); 166 | 167 | let source = match tokio::fs::read_to_string(path).await { 168 | Ok(source) => source, 169 | Err(err) => return format!("error: {err}"), 170 | }; 171 | let updated = match edit_source(path, &source, language, name_path, edit) { 172 | Ok(updated) => updated, 173 | Err(err) => return err, 174 | }; 175 | match tokio::fs::write(path, updated).await { 176 | Ok(()) => "OK".into(), 177 | Err(err) => format!("error: {err}"), 178 | } 179 | } 180 | 181 | fn edit_source( 182 | path: &str, 183 | source: &str, 184 | language: Option<&str>, 185 | name_path: &str, 186 | edit: SymbolEdit, 187 | ) -> Result { 188 | let extraction = 189 | extract_symbols(Some(path), source, language).map_err(|err| format!("error: {err}"))?; 190 | let matches = matching_symbols(&extraction.symbols, name_path, false); 191 | if matches.is_empty() { 192 | return Err(format!("error: symbol not found: {name_path}")); 193 | } 194 | if matches.len() > 1 { 195 | let choices = format_symbol_choices(&matches, 12); 196 | return Err(format!("error: ambiguous symbol: {choices}")); 197 | } 198 | 199 | let range = &matches[0].range; 200 | validate_range(source, range)?; 201 | Ok(match edit { 202 | SymbolEdit::Replace { body } => replace_range(source, range, &body), 203 | SymbolEdit::InsertBefore { text } => insert_before_range(source, range, &text), 204 | SymbolEdit::InsertAfter { text } => insert_after_range(source, range, &text), 205 | }) ``` ## K15 — Lucid: meaningful states, ownership, vocabulary, and behavior changes These passages inform the proposed separation of history, operational state, live computation and released behavior. A shorter source tree is not the objective; neither preserving incidental bugs nor gratuitously changing semantics is justified. Source: `plugins/lucid-code/skills/lucid-code/SKILL.md:41–60` ```text 41 | ## Represent truth 42 | 43 | - Parse weak or untrusted values at the boundary into representations that preserve 44 | what was learned. 45 | - Represent every meaningful state, including legitimate drafts, pending work, 46 | unknowns, failures, and partial external input. Exclude combinations for which the 47 | domain has no valid behavior. 48 | - Choose the most precise representation that makes legal operations direct and illegal 49 | combinations unavailable. Opaque or refined values, role types, closed variants, 50 | typestate, phantom parameters, smart constructors, builders, and schemas are ordinary 51 | tools. A type may be valuable solely because it carries a role, unit, identity, proof, 52 | capability, or protocol state. Expose type machinery when callers can use the 53 | distinction or the compiler can enforce it. 54 | - Make state authority explicit. When the domain has one source of truth, derive 55 | secondary views unless caching, materialization, indexing, or boundary semantics make 56 | another representation better. When authority is replicated or distributed, encode 57 | ownership, merge, and consistency semantics rather than pretending it is singular. 58 | - Expose failure and partiality where callers can act on them. Prefer total public and 59 | domain APIs when no valid caller can satisfy a hidden precondition. A partial interior 60 | operation is fine when its proof is local and stable and failure means an internal ``` Source: `plugins/lucid-code/skills/lucid-code/SKILL.md:110–141` ```text 110 | 111 | A refactor may change observable mechanics and even behavior when the old behavior is 112 | buggy, incidental, or superseded by the requested design. Understand the change, 113 | migrate affected APIs and tests, and make a material contract change explicit. Do not 114 | preserve an inefficiency because it is observable, and do not change behavior casually 115 | because the new spelling is prettier. Benchmark only when a decision or performance 116 | claim depends on a non-obvious empirical tradeoff. 117 | 118 | An abstraction earns its place by creating a better language, carrying a law or proof, 119 | hiding knowledge, enabling composition, translating a boundary, or localizing real 120 | variation. One use or one implementation can be enough. Several uses or 121 | implementations do not rescue an abstraction that gives callers nothing. 122 | 123 | Dependencies are imported vocabulary and capability; dependency avoidance is not a 124 | design objective. Do not penalize packages merely for being dependencies or treat 125 | equivalent local machinery as free. Use or add the package that gives the best 126 | whole-program result through notation, types, algorithms, correctness, performance, 127 | interoperability, tooling, or maintenance. Syntax and ergonomics are sufficient benefits; 128 | a dependency need not do something impossible to hand-write. Evaluate actual constraints 129 | such as runtime support, bundle or build impact, licensing, privileges, and supply-chain 130 | exposure instead of treating dependency count as quality. 131 | 132 | Search the repository and ecosystem when a library or established vocabulary could 133 | materially improve the design. During implementation, make and apply the dependency or 134 | notation choice directly; `$dependency-recon` is the recommendation-only workflow for 135 | requests that ask for research without edits. Multiple libraries are fine when they own 136 | distinct concepts or compose coherently; overlap is a cost only where users must 137 | translate between competing dialects. 138 | 139 | Wrappers, facades, aliases, and re-exports may improve grammar, composition, imports, 140 | errors, policy, or ownership. A thin layer is valuable when the use site becomes better, 141 | even if the underlying capability remains visible. ``` Source: `plugins/lucid-code/skills/lucid-code/references/semantic-compression.md:19–34` ```text 19 | closure, not by the size of the mechanism in isolation. 20 | 21 | ## Separate contract from accidents 22 | 23 | Before preserving behavior, classify it: 24 | 25 | | Kind | Treatment | 26 | |---|---| 27 | | Explicit requirement, external protocol, relied-on compatibility, safety or integrity property | Preserve unless the task changes it | 28 | | Valuable but redesignable API or execution property | Preserve or improve deliberately; migrate consumers | 29 | | Bug, incidental ordering, avoidable work, historical test artifact, or leaky implementation detail | Replace when the new design is better | 30 | 31 | Treat published, documented, stable, or externally consumed surfaces as compatibility 32 | promises unless the task authorizes migration. Internal visibility or a passing test is 33 | not automatically a contract; undocumented behavior can still be contractual when real 34 | consumers rely on it. Use repository evidence and task context rather than labels alone. ```