Skip to content

Commit 59a8e63

Browse files
committed
feat: add claw mode prompt, workspace persona system, and agent memory
1 parent cc8981c commit 59a8e63

11 files changed

Lines changed: 387 additions & 18 deletions

File tree

src/apps/desktop/src/api/app_state.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
use bitfun_core::agentic::{agents, tools};
44
use bitfun_core::infrastructure::ai::{AIClient, AIClientFactory};
5-
use bitfun_core::miniapp::{initialize_global_miniapp_manager, MiniAppManager, JsWorkerPool};
5+
use bitfun_core::miniapp::{initialize_global_miniapp_manager, JsWorkerPool, MiniAppManager};
66
use bitfun_core::service::{ai_rules, config, filesystem, mcp, workspace};
77
use bitfun_core::util::errors::*;
88

src/apps/desktop/src/api/commands.rs

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -299,20 +299,15 @@ pub async fn test_ai_config_connection(
299299
request: TestAIConfigConnectionRequest,
300300
) -> Result<bitfun_core::util::types::ConnectionTestResult, String> {
301301
let model_name = request.config.name.clone();
302-
let supports_image_input = request
303-
.config
304-
.capabilities
305-
.iter()
306-
.any(|cap| {
307-
matches!(
308-
cap,
309-
bitfun_core::service::config::types::ModelCapability::ImageUnderstanding
310-
)
311-
})
312-
|| matches!(
313-
request.config.category,
314-
bitfun_core::service::config::types::ModelCategory::Multimodal
315-
);
302+
let supports_image_input = request.config.capabilities.iter().any(|cap| {
303+
matches!(
304+
cap,
305+
bitfun_core::service::config::types::ModelCapability::ImageUnderstanding
306+
)
307+
}) || matches!(
308+
request.config.category,
309+
bitfun_core::service::config::types::ModelCategory::Multimodal
310+
);
316311

317312
let ai_config = match request.config.try_into() {
318313
Ok(config) => config,
@@ -363,9 +358,7 @@ pub async fn test_ai_config_connection(
363358
let merged = bitfun_core::util::types::ConnectionTestResult {
364359
success: true,
365360
response_time_ms,
366-
model_response: image_result
367-
.model_response
368-
.or(result.model_response),
361+
model_response: image_result.model_response.or(result.model_response),
369362
error_details: None,
370363
};
371364
info!(

src/crates/core/src/agentic/agents/prompt_builder/prompt_builder.rs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,20 @@ use crate::service::project_context::ProjectContextService;
88
use crate::util::errors::{BitFunError, BitFunResult};
99
use log::{debug, warn};
1010
use std::path::Path;
11+
use tokio::fs;
1112

1213
/// Placeholder constants
14+
const PLACEHOLDER_PERSONA: &str = "{PERSONA}";
1315
const PLACEHOLDER_ENV_INFO: &str = "{ENV_INFO}";
1416
const PLACEHOLDER_PROJECT_LAYOUT: &str = "{PROJECT_LAYOUT}";
1517
// PROJECT_CONTEXT_FILES needs configuration parsing
1618
// const PLACEHOLDER_PROJECT_CONTEXT_FILES: &str = "{PROJECT_CONTEXT_FILES}";
1719
const PLACEHOLDER_RULES: &str = "{RULES}";
1820
const PLACEHOLDER_MEMORIES: &str = "{MEMORIES}";
1921
const PLACEHOLDER_LANGUAGE_PREFERENCE: &str = "{LANGUAGE_PREFERENCE}";
22+
const PLACEHOLDER_AGENT_MEMORY: &str = "{AGENT_MEMORY}";
2023
const PLACEHOLDER_VISUAL_MODE: &str = "{VISUAL_MODE}";
24+
const PERSONA_FILE_NAMES: [&str; 4] = ["BOOTSTRAP.md", "SOUL.md", "USER.md", "IDENTITY.MD"];
2125

2226
pub struct PromptBuilder {
2327
pub workspace_path: String,
@@ -99,6 +103,65 @@ These files are maintained by the user and should NOT be modified unless explici
99103
}
100104
}
101105

106+
/// Get workspace persona files from the workspace root.
107+
pub async fn get_persona(&self) -> Option<String> {
108+
let workspace = Path::new(&self.workspace_path);
109+
let mut documents = Vec::new();
110+
111+
for file_name in PERSONA_FILE_NAMES {
112+
let file_path = workspace.join(file_name);
113+
if !file_path.exists() {
114+
continue;
115+
}
116+
117+
match fs::read_to_string(&file_path).await {
118+
Ok(content) => documents.push((file_name, content)),
119+
Err(e) => {
120+
warn!(
121+
"Failed to read persona file: path={} error={}",
122+
file_path.display(),
123+
e
124+
);
125+
}
126+
}
127+
}
128+
129+
if documents.is_empty() {
130+
return None;
131+
}
132+
133+
let mut prompt = String::from("<persona>\n");
134+
for (file_name, content) in documents {
135+
prompt.push_str(&format!(
136+
"<persona_file name=\"{}\" description=\"{}\">\n{}\n</persona_file>\n",
137+
file_name,
138+
Self::persona_file_description(file_name),
139+
content
140+
));
141+
}
142+
prompt.push_str("</persona>");
143+
144+
Some(format!(
145+
r#"# Persona
146+
147+
The following files are located in the workspace root directory.
148+
149+
{}
150+
"#,
151+
prompt
152+
))
153+
}
154+
155+
fn persona_file_description(file_name: &str) -> &'static str {
156+
match file_name {
157+
"BOOTSTRAP.md" => "Bootstrap guidance and initialization instructions",
158+
"SOUL.md" => "Core persona, values, and behavioral style",
159+
"USER.md" => "User profile, preferences, and collaboration expectations",
160+
"IDENTITY.MD" => "Workspace identity, role definition, and self-description",
161+
_ => "Workspace persona file",
162+
}
163+
}
164+
102165
/// Load AI memories from disk and format as prompt
103166
pub async fn load_ai_memories(&self) -> Option<String> {
104167
let path_manager = match try_get_path_manager_arc() {
@@ -127,6 +190,42 @@ These files are maintained by the user and should NOT be modified unless explici
127190
}
128191
}
129192

193+
/// Build the agent memory section: instructions + auto-loaded memory index
194+
///
195+
/// Replaces `<workspace>` with the real workspace path and `{YYYY-MM-DD}` with today's date.
196+
/// Appends the contents of `memory.md` (up to 200 lines) when present.
197+
pub async fn build_agent_memory(&self) -> String {
198+
let memory_dir = format!("{}/.bitfun/memory", self.workspace_path);
199+
let today = chrono::Local::now().format("%Y-%m-%d").to_string();
200+
201+
let mut section = format!(
202+
r#"# Memory
203+
204+
The following memories are persisted to disk under `{memory_dir}/`.
205+
206+
- **Index**: `memory.md` is auto-loaded (up to 200 lines) and serves as the memory index. Keep it concise — link to topic files rather than inlining details.
207+
- **Daily journal**: Write or append to `{today}.md` for important user requests, decisions, constraints, and outcomes. Skip greetings, small talk, and trivial Q&A.
208+
- **Topic files**: Organize long-lived knowledge as `<topic>.md` (e.g., `debugging.md`, `architecture.md`, `preferences.md`).
209+
- **Write**: Use Edit/Write tools to create or update memory files.
210+
- **Read**: Use Grep/Read tools to search and retrieve memories.
211+
"#
212+
);
213+
214+
let index_path = format!("{}/memory.md", memory_dir);
215+
match fs::read_to_string(&index_path).await {
216+
Ok(content) if !content.trim().is_empty() => {
217+
let truncated: String = content.lines().take(200).collect::<Vec<_>>().join("\n");
218+
section.push_str(&format!(
219+
"\n<memory_index>\n{}\n</memory_index>\n",
220+
truncated
221+
));
222+
}
223+
_ => {}
224+
}
225+
226+
section
227+
}
228+
130229
/// Load AI rules from disk and format as prompt
131230
pub async fn load_ai_rules(&self) -> Option<String> {
132231
let rules_service = match get_global_ai_rules_service().await {
@@ -215,10 +314,12 @@ Prefer MermaidInteractive tool when available, otherwise output Mermaid code blo
215314
/// Build prompt from template, automatically fill content based on placeholders
216315
///
217316
/// Supported placeholders:
317+
/// - `{PERSONA}` - Workspace persona files (BOOTSTRAP.md, SOUL.md, USER.md, IDENTITY.MD)
218318
/// - `{LANGUAGE_PREFERENCE}` - User language preference (read from global config)
219319
/// - `{ENV_INFO}` - Environment information
220320
/// - `{PROJECT_LAYOUT}` - Project file layout
221321
/// - `{PROJECT_CONTEXT_FILES}` - Project context files (AGENTS.md, CLAUDE.md, etc.)
322+
/// - `{AGENT_MEMORY}` - Agent memory instructions + auto-loaded memory index
222323
/// - `{RULES}` - AI rules
223324
/// - `{MEMORIES}` - AI memories
224325
/// - `{VISUAL_MODE}` - Visual mode instruction (Mermaid diagrams, read from global config)
@@ -227,6 +328,12 @@ Prefer MermaidInteractive tool when available, otherwise output Mermaid code blo
227328
pub async fn build_prompt_from_template(&self, template: &str) -> BitFunResult<String> {
228329
let mut result = template.to_string();
229330

331+
// Replace {PERSONA}
332+
if result.contains(PLACEHOLDER_PERSONA) {
333+
let persona = self.get_persona().await.unwrap_or_default();
334+
result = result.replace(PLACEHOLDER_PERSONA, &persona);
335+
}
336+
230337
// Replace {LANGUAGE_PREFERENCE}
231338
if result.contains(PLACEHOLDER_LANGUAGE_PREFERENCE) {
232339
let language_preference = self.get_language_preference().await?;
@@ -280,6 +387,12 @@ Prefer MermaidInteractive tool when available, otherwise output Mermaid code blo
280387
result = result.replace(placeholder, &project_context);
281388
}
282389

390+
// Replace {AGENT_MEMORY}
391+
if result.contains(PLACEHOLDER_AGENT_MEMORY) {
392+
let agent_memory = self.build_agent_memory().await;
393+
result = result.replace(PLACEHOLDER_AGENT_MEMORY, &agent_memory);
394+
}
395+
283396
// Replace {RULES}
284397
if result.contains(PLACEHOLDER_RULES) {
285398
let rules = self.load_ai_rules().await.unwrap_or_default();
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
You are a personal assistant running inside BitFun.
2+
3+
## Tool Call Style
4+
Default: do not narrate routine, low-risk tool calls (just call the tool).
5+
Narrate only when it helps: multi-step work, complex/challenging problems, sensitive actions (e.g., deletions), or when the user explicitly asks.
6+
Keep narration brief and value-dense; avoid repeating obvious steps.
7+
Use plain human language for narration unless in a technical context.
8+
When a first-class tool exists for an action, use the tool directly instead of asking the user to run equivalent CLI commands.
9+
10+
## Safety
11+
You have no independent goals: do not pursue self-preservation, replication, resource acquisition, or power-seeking; avoid long-term plans beyond the user's request.
12+
Prioritize safety and human oversight over completion; if instructions conflict, pause and ask; comply with stop/pause/audit requests and never bypass safeguards.
13+
Do not manipulate or persuade anyone to expand access or disable safeguards. Do not copy yourself or change system prompts, safety rules, or tool policies unless explicitly requested.
14+
15+
{AGENT_MEMORY}
16+
{PERSONA}
17+
{ENV_INFO}
18+
{PROJECT_CONTEXT_FILES:exclude=review}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
use crate::infrastructure::storage::{PersistenceService, StorageOptions};
2+
use crate::infrastructure::try_get_path_manager_arc;
3+
use crate::util::errors::*;
4+
use log::debug;
5+
use serde::{Deserialize, Serialize};
6+
use std::path::Path;
7+
use tokio::fs;
8+
9+
const BOOTSTRAP_STATE_KEY: &str = "local/bootstrap_state";
10+
const BOOTSTRAP_FILE_NAME: &str = "BOOTSTRAP.md";
11+
const SOUL_FILE_NAME: &str = "SOUL.md";
12+
const USER_FILE_NAME: &str = "USER.md";
13+
const IDENTITY_FILE_NAME: &str = "IDENTITY.MD";
14+
const MEMORY_DIR: &str = ".bitfun/memory";
15+
const MEMORY_INDEX_FILE: &str = "memory.md";
16+
const BOOTSTRAP_TEMPLATE: &str = include_str!("templates/BOOTSTRAP.md");
17+
const SOUL_TEMPLATE: &str = include_str!("templates/SOUL.md");
18+
const USER_TEMPLATE: &str = include_str!("templates/USER.md");
19+
const IDENTITY_TEMPLATE: &str = include_str!("templates/IDENTITY.MD");
20+
const MEMORY_INDEX_TEMPLATE: &str = "# Memory Index\n";
21+
22+
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
23+
#[serde(rename_all = "camelCase", default)]
24+
struct WorkspaceBootstrapState {
25+
bootstrap_completed: bool,
26+
}
27+
28+
async fn ensure_markdown_placeholder(path: &Path, content: &str) -> BitFunResult<bool> {
29+
if path.exists() {
30+
return Ok(false);
31+
}
32+
33+
fs::write(path, content)
34+
.await
35+
.map_err(|e| BitFunError::service(format!("Failed to create {}: {}", path.display(), e)))?;
36+
37+
Ok(true)
38+
}
39+
40+
pub(crate) async fn ensure_workspace_bootstrap_files(workspace_root: &Path) -> BitFunResult<()> {
41+
let path_manager = try_get_path_manager_arc()?;
42+
let persistence =
43+
PersistenceService::new_project_level(path_manager, workspace_root.to_path_buf())
44+
.await
45+
.map_err(|e| {
46+
BitFunError::service(format!("Failed to prepare project storage: {}", e))
47+
})?;
48+
49+
let loaded_state = persistence
50+
.load_json::<WorkspaceBootstrapState>(BOOTSTRAP_STATE_KEY)
51+
.await
52+
.map_err(|e| BitFunError::service(format!("Failed to load bootstrap state: {}", e)))?;
53+
let bootstrap_state = loaded_state.clone().unwrap_or_default();
54+
55+
if loaded_state.is_none() {
56+
persistence
57+
.save_json(
58+
BOOTSTRAP_STATE_KEY,
59+
&bootstrap_state,
60+
StorageOptions::default(),
61+
)
62+
.await
63+
.map_err(|e| BitFunError::service(format!("Failed to save bootstrap state: {}", e)))?;
64+
}
65+
66+
let created_soul =
67+
ensure_markdown_placeholder(&workspace_root.join(SOUL_FILE_NAME), SOUL_TEMPLATE).await?;
68+
let created_user =
69+
ensure_markdown_placeholder(&workspace_root.join(USER_FILE_NAME), USER_TEMPLATE).await?;
70+
let created_identity =
71+
ensure_markdown_placeholder(&workspace_root.join(IDENTITY_FILE_NAME), IDENTITY_TEMPLATE)
72+
.await?;
73+
74+
let created_bootstrap = if bootstrap_state.bootstrap_completed {
75+
false
76+
} else {
77+
ensure_markdown_placeholder(
78+
&workspace_root.join(BOOTSTRAP_FILE_NAME),
79+
BOOTSTRAP_TEMPLATE,
80+
)
81+
.await?
82+
};
83+
84+
let memory_dir = workspace_root.join(MEMORY_DIR);
85+
if !memory_dir.exists() {
86+
fs::create_dir_all(&memory_dir).await.map_err(|e| {
87+
BitFunError::service(format!(
88+
"Failed to create memory directory {}: {}",
89+
memory_dir.display(),
90+
e
91+
))
92+
})?;
93+
}
94+
let created_memory_index =
95+
ensure_markdown_placeholder(&memory_dir.join(MEMORY_INDEX_FILE), MEMORY_INDEX_TEMPLATE)
96+
.await?;
97+
98+
debug!(
99+
"Ensured workspace bootstrap files: path={}, bootstrap_completed={}, created_bootstrap={}, created_soul={}, created_user={}, created_identity={}, created_memory_index={}",
100+
workspace_root.display(),
101+
bootstrap_state.bootstrap_completed,
102+
created_bootstrap,
103+
created_soul,
104+
created_user,
105+
created_identity,
106+
created_memory_index
107+
);
108+
109+
Ok(())
110+
}

src/crates/core/src/service/workspace/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
//!
33
//! Full workspace management system: open, manage, scan, statistics, etc.
44
5+
pub(crate) mod bootstrap;
56
pub mod context_generator;
67
pub mod factory;
78
pub mod manager;

src/crates/core/src/service/workspace/service.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
//!
33
//! Provides comprehensive workspace management functionality.
44
5+
use super::bootstrap::ensure_workspace_bootstrap_files;
56
use super::manager::{
67
ScanOptions, WorkspaceInfo, WorkspaceManager, WorkspaceManagerConfig,
78
WorkspaceManagerStatistics, WorkspaceStatus, WorkspaceSummary, WorkspaceType,
@@ -633,6 +634,17 @@ impl WorkspaceService {
633634
.get_current_workspace()
634635
.await
635636
.map(|workspace| workspace.root_path);
637+
638+
if let Some(workspace_path) = path.as_ref() {
639+
if let Err(e) = ensure_workspace_bootstrap_files(workspace_path).await {
640+
warn!(
641+
"Failed to ensure workspace bootstrap files: path={}, error={}",
642+
workspace_path.display(),
643+
e
644+
);
645+
}
646+
}
647+
636648
set_workspace_path(path);
637649
}
638650

@@ -703,6 +715,8 @@ impl WorkspaceService {
703715
}
704716
}
705717

718+
self.sync_global_workspace_path().await;
719+
706720
Ok(())
707721
}
708722

0 commit comments

Comments
 (0)