Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 20 additions & 9 deletions agentmain.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from plugins.hooks import discover_and_load; discover_and_load()
except Exception: pass
from ga import GenericAgentHandler, smart_format, get_global_memory, format_error, consume_file
import paths

script_dir = os.path.dirname(os.path.abspath(__file__))
BANNED_TOOLS = (['ask_user', 'start_long_term_update'] if '--no-user-tools' in sys.argv else [])
Expand All @@ -23,7 +24,7 @@ def load_tool_schema(suffix=''):
load_tool_schema()

lang_suffix = '_en' if os.environ.get('GA_LANG', '') == 'en' else ''
mem_dir = os.path.join(script_dir, 'memory')
mem_dir = paths.memory_dir()
if not os.path.exists(mem_dir): os.makedirs(mem_dir)
mem_txt = os.path.join(mem_dir, 'global_mem.txt')
if not os.path.exists(mem_txt): open(mem_txt, 'w', encoding='utf-8').write('# [Global Memory - L2]\n')
Expand All @@ -34,6 +35,16 @@ def load_tool_schema(suffix=''):

def get_system_prompt():
with open(os.path.join(script_dir, f'assets/sys_prompt{lang_suffix}.txt'), 'r', encoding='utf-8') as f: prompt = f.read()
# P1-A: layer an optional user override from the writable data root on top of the
# bundled baseline. Kept as an append (not a replace) so the agent never loses its
# core operating contract; users add house rules without editing the read-only bundle.
try:
ov = paths.sys_prompt_override(lang_suffix)
if os.path.isfile(ov):
with open(ov, 'r', encoding='utf-8') as f:
extra = f.read().strip()
if extra: prompt += f"\n\n# ── User overrides (sys_prompt_override) ──\n{extra}\n"
except Exception: pass
prompt += f"\nToday: {time.strftime('%Y-%m-%d %a')}\n"
prompt += get_global_memory()
return prompt
Expand All @@ -44,7 +55,7 @@ def get_system_prompt():
# output2_queue = agent.put_task(prompt2)
class GenericAgent:
def __init__(self):
os.makedirs(os.path.join(script_dir, 'temp'), exist_ok=True)
os.makedirs(paths.temp_dir(), exist_ok=True)
self.lock = threading.Lock()
self.task_dir = None
self.history = []; self.handler = None;
Expand All @@ -54,7 +65,7 @@ def __init__(self):
self.peer_hint = True
self.force_non_stream = False
logid = f'{(time.time_ns() + random.randrange(1_000_000)) % 1_000_000:06d}'
self.log_path = os.path.join(script_dir, f'temp/model_responses/model_responses_{logid}.txt')
self.log_path = paths.temp_dir('model_responses', f'model_responses_{logid}.txt')
self.llmclient = None
self.load_llm_sessions()
self.extra_sys_prompts = []
Expand Down Expand Up @@ -122,7 +133,7 @@ def _handle_slash_cmd(self, raw_query, display_queue):
if not raw_query.startswith('/'): return raw_query
if _sm := re.match(r'/session\.(\w+)=(.*)', raw_query.strip()):
k, v = _sm.group(1), _sm.group(2)
vfile = os.path.join(script_dir, 'temp', v)
vfile = paths.temp_dir(v)
if os.path.isfile(vfile): v = open(vfile, encoding='utf-8').read().strip()
try: v = json.loads(v) # cover number parsing
except (json.JSONDecodeError, ValueError): pass
Expand All @@ -143,14 +154,14 @@ def run(self):
self.task_queue.task_done(); continue
self.is_running = True
if len(raw_query) > 2000:
task_file = os.path.join(script_dir, 'temp', f'user_prompt_{os.getpid()}_{time.time_ns()}.md')
task_file = paths.temp_dir(f'user_prompt_{os.getpid()}_{time.time_ns()}.md')
with open(task_file, 'w', encoding='utf-8') as f: f.write(raw_query)
raw_query = f'Long user prompt saved to {task_file}. Read and execute.'
rquery = smart_format(raw_query.replace('\n', ' '), max_str_len=200)
self.history.append(f"[USER]: {rquery}")
sys_prompt = get_system_prompt() + '\n'.join(self.extra_sys_prompts) + getattr(self.llmclient.backend, 'extra_sys_prompt', '')
if self.peer_hint: sys_prompt += f"\n[Peer] 用户提及其他会话/后台任务状态时: temp/model_responses/ (只找近期修改的文件尾部)\n"
handler = GenericAgentHandler(self, self.history, os.path.join(script_dir, 'temp'))
handler = GenericAgentHandler(self, self.history, paths.temp_dir())
if getattr(self, 'no_print', False): handler.print = lambda *a, **k: None
if self.handler and 'key_info' in self.handler.working:
ki = re.sub(r'\n\[SYSTEM\] 此为.*?工作记忆[。\n]*', '', self.handler.working['key_info']) # 去旧
Expand Down Expand Up @@ -213,7 +224,7 @@ def run(self):
import subprocess, platform
cmd = [sys.executable, os.path.abspath(__file__)] + [a for a in sys.argv[1:]] + ['--nobg']
if args.task:
d = os.path.join(script_dir, f'temp/{args.task}'); os.makedirs(d, exist_ok=True)
d = paths.temp_dir(args.task); os.makedirs(d, exist_ok=True)
out = open(os.path.join(d, 'stdout.log'), 'w', encoding='utf-8')
err = open(os.path.join(d, 'stderr.log'), 'w', encoding='utf-8')
else: out, err = subprocess.DEVNULL, subprocess.DEVNULL
Expand All @@ -230,7 +241,7 @@ def run(self):

histfile = args.history
if args.task:
agent.task_dir = d = os.path.join(script_dir, f'temp/{args.task}'); nround = ''
agent.task_dir = d = paths.temp_dir(args.task); nround = ''
infile = os.path.join(d, 'input.txt'); outfile = f'{d}/output{nround}.txt'
if args.input:
os.makedirs(d, exist_ok=True)
Expand Down Expand Up @@ -288,7 +299,7 @@ def run(self):
except Exception as e:
if getattr(mod, 'ONCE', False): raise
print(f'[Reflect] drain error: {e}'); result = f'[ERROR] {e}'
log_dir = os.path.join(script_dir, 'temp/reflect_logs'); os.makedirs(log_dir, exist_ok=True)
log_dir = paths.temp_dir('reflect_logs'); os.makedirs(log_dir, exist_ok=True)
script_name = os.path.splitext(os.path.basename(args.reflect))[0]
open(os.path.join(log_dir, f'{script_name}_{datetime.now():%Y-%m-%d}.log'), 'a', encoding='utf-8').write(f'[{datetime.now():%m-%d %H:%M}]\n{result}\n\n')
if (on_done := getattr(mod, 'on_done', None)):
Expand Down
16 changes: 14 additions & 2 deletions assets/configure_mykey.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,18 @@
}

PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
MYKPY_PATH = os.path.join(PROJECT_ROOT, 'mykey.py')
# P1-D: write credentials to the user-writable data root (packaged bundle is read-only).
# Falls back to the project root for plain source checkouts where paths is unavailable
# or DATA_DIR == PROJECT_ROOT anyway.
if PROJECT_ROOT not in sys.path:
sys.path.insert(0, PROJECT_ROOT)
try:
import paths as _paths
MYKPY_PATH = _paths.mykey_path()
_BACKUP_DIR = _paths.DATA_DIR
except Exception:
MYKPY_PATH = os.path.join(PROJECT_ROOT, 'mykey.py')
_BACKUP_DIR = PROJECT_ROOT

# ── 模型厂商定义 ───────────────────────────────────────────────────────────

Expand Down Expand Up @@ -1246,7 +1257,7 @@ def _backup_with_name(model_names, platform_ids):
safe_name = 'mykey_backup' # 避免和源文件同名
if len(safe_name) > 100:
safe_name = safe_name[:100]
backup_path = os.path.join(PROJECT_ROOT, f'{safe_name}.py')
backup_path = os.path.join(_BACKUP_DIR, f'{safe_name}.py')
shutil.copy2(MYKPY_PATH, backup_path)
return backup_path

Expand Down Expand Up @@ -1371,6 +1382,7 @@ def main():
print(f"\n {C['green']}✓ 旧配置已备份至:{C['reset']} {C['dim']}{backup}{C['reset']}")

# 写入
os.makedirs(os.path.dirname(MYKPY_PATH), exist_ok=True)
with open(MYKPY_PATH, 'w', encoding='utf-8') as f:
f.write(content)
print(f"\n {C['green']}✓ mykey.py 已生成!{C['reset']}")
Expand Down
4 changes: 3 additions & 1 deletion assets/ga_ultraplan.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
__all__ = ["plan", "phase", "parallel", "mapchain"]

_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if _ROOT not in sys.path: sys.path.insert(0, _ROOT)
import paths
_PORT = int(os.environ.get("GA_ULTRAPLAN_PORT", "47831"))
_T0 = time(); _phases = []; _phase_stack = []; _tasks = []; _current = "idle"; _events = []; _srv = None; _last = time(); _lock = threading.Lock(); _exec_lock = threading.Lock()
_TASK_SLUG = "task"; _FUNC_SEQ = 0; _PLANNED = False; _SESSION = None; _sessions = {}
_RUN_DIR = os.path.abspath(os.environ.get("GA_ULTRAPLAN_RUNDIR", os.path.join(_ROOT, "temp", "ultraplan_default")))
_RUN_DIR = os.path.abspath(os.environ.get("GA_ULTRAPLAN_RUNDIR", paths.temp_dir("ultraplan_default")))
os.makedirs(_RUN_DIR, exist_ok=True)

def _bind(rundir):
Expand Down
8 changes: 6 additions & 2 deletions frontends/cost_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@
same `[Cache]` / `[Output]` print lines from `temp/*/stdout.log`.
"""
from __future__ import annotations
import glob, os, re, threading, time
import glob, os, re, sys, threading, time
from dataclasses import dataclass, field

_repo = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if _repo not in sys.path: sys.path.insert(0, _repo)
import paths


@dataclass
class TokenStats:
Expand Down Expand Up @@ -76,7 +80,7 @@ def scan_subagent_logs(since: float = 0.0, root: str | None = None) -> TokenStat
`since=tui_start_time` to scope to this run. Best-effort: bad logs skipped."""
out = TokenStats()
if since > 0: out.started_at = since
pattern = os.path.join(root, _SUBAGENT_GLOB) if root else _SUBAGENT_GLOB
pattern = os.path.join(root, _SUBAGENT_GLOB) if root else os.path.join(paths.temp_dir(), "*", "stdout.log")
for p in glob.glob(pattern):
try:
if since and os.path.getmtime(p) < since: continue
Expand Down
6 changes: 5 additions & 1 deletion frontends/desktop/src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
"allow-set-ga-source",
"allow-clear-ga-source",
"allow-shortcut-should-ask",
"allow-shortcut-decide"
"allow-shortcut-decide",
"allow-get-data-dir-override",
"allow-set-data-dir",
"allow-clear-data-dir",
"allow-move-data-dir"
]
}
20 changes: 20 additions & 0 deletions frontends/desktop/src-tauri/permissions/bridge-commands.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,23 @@ commands.allow = ["shortcut_should_ask"]
identifier = "allow-shortcut-decide"
description = "Persist the user's desktop-shortcut choice and create it if enabled."
commands.allow = ["shortcut_decide"]

[[permission]]
identifier = "allow-get-data-dir-override"
description = "Read the user-selected GenericAgent data directory override."
commands.allow = ["get_data_dir_override"]

[[permission]]
identifier = "allow-set-data-dir"
description = "Set the GenericAgent data directory override and restart the bridge."
commands.allow = ["set_data_dir"]

[[permission]]
identifier = "allow-clear-data-dir"
description = "Clear the GenericAgent data directory override and restart the bridge."
commands.allow = ["clear_data_dir"]

[[permission]]
identifier = "allow-move-data-dir"
description = "Move current data directory under a selected parent, delete the old directory, set it, and restart the bridge."
commands.allow = ["move_data_dir"]
105 changes: 104 additions & 1 deletion frontends/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,47 @@ fn write_shortcut_pref(enabled: bool) {
merge_settings(serde_json::json!({ "desktop_shortcut": enabled }));
}

/// User-selected data directory override. Empty means the Python paths.py default
/// (~/GenericAgent_Data, or in-place for source/dev checkouts).
fn data_dir_override() -> Option<String> {
let s = read_settings()
.get("data_dir_override")
.and_then(|v| v.as_str())
.unwrap_or("")
.trim()
.to_string();
if s.is_empty() { None } else { Some(s) }
}

fn set_data_dir_override_value(dir: &str) -> Result<String, String> {
let p = PathBuf::from(dir);
std::fs::create_dir_all(&p).map_err(|e| format!("cannot create data dir: {}", e))?;
let canon = p.canonicalize().unwrap_or(p);
merge_settings(serde_json::json!({ "data_dir_override": canon.to_string_lossy().to_string() }));
Ok(canon.to_string_lossy().to_string())
}

fn copy_dir_replace(src: &std::path::Path, dst: &std::path::Path) -> Result<(), String> {
std::fs::create_dir_all(dst).map_err(|e| format!("create {:?}: {}", dst, e))?;
for entry in std::fs::read_dir(src).map_err(|e| format!("read {:?}: {}", src, e))? {
let entry = entry.map_err(|e| e.to_string())?;
let sp = entry.path();
let dp = dst.join(entry.file_name());
let ft = entry.file_type().map_err(|e| e.to_string())?;
if ft.is_dir() {
if dp.exists() && !dp.is_dir() {
std::fs::remove_file(&dp).map_err(|e| format!("remove {:?}: {}", dp, e))?;
}
copy_dir_replace(&sp, &dp)?;
} else if ft.is_file() {
if let Some(parent) = dp.parent() {
std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
}
std::fs::copy(&sp, &dp).map_err(|e| format!("copy {:?} -> {:?}: {}", sp, dp, e))?;
}
}
Ok(())
}
/// Create (or overwrite) a desktop shortcut pointing at the CURRENT exe. Overwriting on every
/// enabled launch is what makes the portable bundle relocatable: move the folder, relaunch, and
/// the shortcut is rewritten to the new path. Windows-only (uses a .lnk via WScript.Shell).
Expand Down Expand Up @@ -480,6 +521,10 @@ fn sanitize_bundle_env(cmd: &mut Command) {
Some(src) => { cmd.env("GA_ROOT", src); }
None => { cmd.env_remove("GA_ROOT"); }
}
match data_dir_override() {
Some(dir) => { cmd.env("GA_DATA_DIR", dir); }
None => { cmd.env_remove("GA_DATA_DIR"); }
}
}

/// Run the offline prepare (install_windows.ps1 -Mode PrepareOnly) using bundled python + wheels.
Expand Down Expand Up @@ -876,6 +921,64 @@ fn get_ga_source() -> String {
.to_string()
}

#[tauri::command]
fn get_data_dir_override() -> String {
data_dir_override().unwrap_or_default()
}

#[tauri::command]
fn set_data_dir(app_handle: tauri::AppHandle, dir: String) -> Result<String, String> {
let saved = set_data_dir_override_value(&dir)?;
switch_bridge(&app_handle)?;
Ok(saved)
}

#[tauri::command]
fn clear_data_dir(app_handle: tauri::AppHandle) -> Result<String, String> {
remove_setting("data_dir_override");
switch_bridge(&app_handle)?;
Ok(String::new())
}

#[tauri::command]
fn move_data_dir(app_handle: tauri::AppHandle, current: String, dir: String) -> Result<String, String> {
let src = PathBuf::from(current.trim());
let parent = PathBuf::from(dir.trim());
if current.trim().is_empty() {
return Err("current data dir is empty; open settings after bridge is ready and try again".into());
}
if dir.trim().is_empty() {
return Err("target parent directory is empty".into());
}
if !src.exists() {
return Err(format!("current data dir does not exist: {}", src.to_string_lossy()));
}
if !src.is_dir() {
return Err(format!("current data dir is not a directory: {}", src.to_string_lossy()));
}
std::fs::create_dir_all(&parent).map_err(|e| format!("cannot create target parent dir: {}", e))?;
let src_c = src.canonicalize().unwrap_or(src.clone());
let parent_c = parent.canonicalize().unwrap_or(parent.clone());
let data_dir_name = src_c
.file_name()
.ok_or_else(|| "current data dir has no folder name".to_string())?;
let dst = parent_c.join(data_dir_name);
let dst_c = if dst.exists() { dst.canonicalize().unwrap_or(dst.clone()) } else { dst.clone() };
if src_c == dst_c {
return Ok(set_data_dir_override_value(dst_c.to_string_lossy().as_ref())?);
}
if dst_c.starts_with(&src_c) {
return Err("target data dir cannot be inside the current data dir".into());
}
if dst_c.exists() {
return Err(format!("target data dir already exists: {}", dst_c.to_string_lossy()));
}
copy_dir_replace(&src_c, &dst_c)?;
let saved = set_data_dir_override_value(dst_c.to_string_lossy().as_ref())?;
switch_bridge(&app_handle)?;
std::fs::remove_dir_all(&src_c).map_err(|e| format!("remove old data dir {:?}: {}", src_c, e))?;
Ok(saved)
}
/// Run the contract probe (frontends/ga_contract_probe.py, shipped with the bundle) against a
/// target ga_root using the bundle python. Returns Ok(()) if the核 satisfies the bridge/conductor
/// contract, or Err(message) listing what's missing / why it's incompatible.
Expand Down Expand Up @@ -984,7 +1087,7 @@ pub fn run() {
let _ = w.set_focus();
}
}))
.invoke_handler(tauri::generate_handler![start_bridge_with_config, start_bridge, get_config, export_mykey, pick_directory, get_ga_source, set_ga_source, clear_ga_source, shortcut_should_ask, shortcut_decide, get_prepare_error])
.invoke_handler(tauri::generate_handler![start_bridge_with_config, start_bridge, get_config, export_mykey, pick_directory, get_ga_source, set_ga_source, clear_ga_source, get_data_dir_override, set_data_dir, clear_data_dir, move_data_dir, shortcut_should_ask, shortcut_decide, get_prepare_error])
.setup(move |app| {
// Show the loading window immediately so the first-run prepare isn't a blank screen.
// The window starts on loading.html (a local page), so no "connection refused" flash.
Expand Down
Loading