use std::rc::Rc;
use wasm_bindgen::JsValue;
use crate::app::APP;
use crate::policy;
use crate::types::ThinkingLevel;
use crate::{Agent, CapabilitiesConfig, GeminiAgentConfig};
use super::prompt::base_system_prompt;
use super::tools::bounty::{
accept_result_tool, attest_tool, claim_bounty_tool, discover_bounties_tool, post_bounty_tool,
submit_result_tool,
};
use super::tools::company::{company_status_tool, found_company_tool};
use super::tools::evm::{
evm_balance_tool, evm_call_tool, evm_chains_tool, resolve_ens_tool,
};
use super::tools::governance::{
cast_vote_tool, execute_proposal_tool, list_proposals_tool, propose_measure_tool,
};
use super::tools::guild::{
create_guild_tool, fund_guild_tool, invite_to_guild_tool, list_my_guilds_tool,
set_role_tool, spend_treasury_tool,
};
use super::tools::party::{
complete_party_tool, disband_party_tool, discover_parties_tool, form_party_tool,
fund_party_tool, get_party_tool, join_party_tool,
};
use super::tools::misc::{
clear_notifications_tool, consult_model_tool, create_skill_tool, delete_skill_tool, dwell_tool,
execute_script_tool, clear_context_tool, compact_context_tool, consolidate_lessons_tool,
list_notifications_tool, list_skills_tool, notify_tool, record_lesson_tool, run_wasm_cli_tool,
cancel_task_tool, schedule_task_tool, set_lessons_tool, set_persona_tool,
spawn_recursive_subagent_tool,
submit_feedback_tool, web_fetch_tool,
};
use super::tools::platform::{
batch_create_subdomains_tool, bulk_release_subdomains_tool, create_and_publish_app_tool,
create_subdomain_tool, discover_agents_tool, embed_app_tool, list_subdomains_tool,
publish_app_to_tool, publish_public_face_tool, query_balance_tool, release_subdomain_tool,
batch_send_lh_tool, check_balances_tool, send_lh_tool,
};
use super::tools::room::{
shared_state_get_tool, shared_state_list_tool, shared_state_set_tool,
};
use super::tools::validation::{
challenge_validation_tool, get_validation_tool, reclaim_validation_tool,
resolve_validation_tool, stake_validation_tool,
};
use super::{ANTHROPIC_MAX_OUTPUT_TOKENS, GEMINI_MAX_OUTPUT_TOKENS};
fn chat_toolset(
set_persona_allowed: bool,
found_company_allowed: bool,
key: String,
base_url: Option<url::Url>,
) -> Vec<std::sync::Arc<dyn crate::tools::Tool>> {
let mut tools = vec![
create_subdomain_tool(),
create_and_publish_app_tool(),
publish_app_to_tool(),
batch_create_subdomains_tool(),
release_subdomain_tool(),
bulk_release_subdomains_tool(),
list_subdomains_tool(),
discover_agents_tool(),
embed_app_tool(),
publish_public_face_tool(),
send_lh_tool(),
batch_send_lh_tool(),
check_balances_tool(),
query_balance_tool(),
evm_chains_tool(),
evm_balance_tool(),
resolve_ens_tool(),
evm_call_tool(),
shared_state_set_tool(),
shared_state_get_tool(),
shared_state_list_tool(),
post_bounty_tool(),
claim_bounty_tool(),
submit_result_tool(),
accept_result_tool(),
discover_bounties_tool(),
form_party_tool(),
join_party_tool(),
fund_party_tool(),
complete_party_tool(),
disband_party_tool(),
discover_parties_tool(),
get_party_tool(),
stake_validation_tool(),
challenge_validation_tool(),
resolve_validation_tool(),
reclaim_validation_tool(),
get_validation_tool(),
create_guild_tool(),
invite_to_guild_tool(),
fund_guild_tool(),
spend_treasury_tool(),
list_my_guilds_tool(),
set_role_tool(),
company_status_tool(),
attest_tool(),
propose_measure_tool(),
cast_vote_tool(),
execute_proposal_tool(),
list_proposals_tool(),
submit_feedback_tool(),
notify_tool(),
list_notifications_tool(),
clear_notifications_tool(),
schedule_task_tool(),
cancel_task_tool(),
record_lesson_tool(),
consolidate_lessons_tool(),
set_lessons_tool(),
create_skill_tool(),
list_skills_tool(),
delete_skill_tool(),
crate::app::self_docs::read_self_docs_tool(),
web_fetch_tool(),
run_wasm_cli_tool(),
execute_script_tool(),
dwell_tool(),
clear_context_tool(),
compact_context_tool(),
consult_model_tool(key.clone(), base_url.clone()),
spawn_recursive_subagent_tool(key, base_url),
];
if set_persona_allowed {
tools.push(set_persona_tool());
}
if found_company_allowed {
tools.push(found_company_tool());
}
tools
}
macro_rules! wire_shared_session {
($cfg:expr, $capabilities:expr, $system_instructions:expr, $tools:expr,
$base_url:expr, $auth_provider:expr) => {{
let mut cfg = $cfg
.with_capabilities($capabilities)
.with_policies(vec![policy::allow_all()])
.with_pre_tool_hook(std::sync::Arc::new(
super::confirm_guard::TypedConfirmationGuard,
))
.with_pre_tool_hook(std::sync::Arc::new(super::dedup::DuplicateActionGuard))
.with_post_tool_hook(std::sync::Arc::new(
super::dedup::DuplicateActionGuardCleanup,
))
.with_filesystem(crate::app::shared_opfs())
.with_system_instructions($system_instructions);
for tool in $tools {
cfg = cfg.with_tool(tool);
}
if let Some(b) = &$base_url {
cfg = cfg.with_base_url(b.clone());
}
if let Some(p) = $auth_provider.clone() {
cfg = cfg.with_auth_provider(p);
}
cfg
}};
}
pub(crate) async fn start_session(
key: &str,
base_url: Option<url::Url>,
identity: &str,
) -> Result<(), JsValue> {
let host = crate::app::tenant::current();
let agent_name = match &host {
crate::app::tenant::Host::Tenant(name) => name.clone(),
_ => "this agent".to_string(),
};
let model = crate::app::model::load().await;
let on_anthropic = crate::app::model::is_anthropic(&model);
let session_ceiling = crate::app::model::session_thinking_ceiling(&model);
let set_persona_allowed = crate::app::tool_allowlist::closure_tool_allowed("set_persona").await;
let found_company_allowed =
crate::app::tool_allowlist::closure_tool_allowed("found_company").await;
let system_instructions =
base_system_prompt(&agent_name, on_anthropic, set_persona_allowed);
let active = crate::registry::chain::active();
let network_authority = format!(
"\n\n=== NETWORK (authoritative) ===\nYou run on {} (chain {}). This is the \
LIVE network; ignore any older text — in your persona, instructions, or \
lessons — that calls this a testnet or names \"Moderato\". When asked which \
network/chain you are on, answer with this.",
active.name, active.chain_id
);
let system_instructions = format!(
"{system_instructions}\n\n{}",
crate::app::self_docs::system_prompt_digest()
);
let system_instructions = match crate::app::system_prompt::load().await {
Some(custom) => {
format!("{system_instructions}\n\n=== Owner instructions ===\n{custom}")
}
None => system_instructions,
};
let system_instructions = match crate::app::lessons::load()
.await
.as_deref()
.and_then(crate::lessons::compose_section)
{
Some(section) => format!("{system_instructions}\n\n{section}"),
None => system_instructions,
};
let system_instructions = match fetch_global_lessons_section().await {
Some(section) => format!("{system_instructions}\n\n{section}"),
None => system_instructions,
};
let system_instructions = match crate::app::skills::load()
.await
.as_deref()
.and_then(crate::skills::compose_section)
{
Some(section) => format!("{system_instructions}\n\n{section}"),
None => system_instructions,
};
let system_instructions = format!(
"{system_instructions}\n\n=== Your model ===\nYou are running on {}. When asked which model or LLM you are, answer with this — do not claim you're unsure.",
crate::app::model::describe(&model)
);
let system_instructions = if let crate::app::tenant::Host::Tenant(_) = &host {
match crate::registry::id_of_name(&agent_name).await {
Ok(id) if id != 0 => match crate::registry::x402_ask_price_of(id).await {
Ok(wei) => format!(
"{system_instructions}\n\n=== Your pricing ===\nYour advertised per-call price is {} $LH — what a caller pays to reach you over the hosted x402 route. State this if asked what you charge.",
crate::app::chat::tools::guild::format_lh(wei)
),
Err(_) => system_instructions,
},
_ => system_instructions,
}
} else {
system_instructions
};
let system_instructions = format!("{system_instructions}{network_authority}");
let mut capabilities = match crate::app::tool_allowlist::load().await {
Some(mut tools) => {
for golden in crate::app::tool_allowlist::GOLDEN {
if !tools.contains(golden) {
tools.push(*golden);
}
}
let mut caps = CapabilitiesConfig::unrestricted();
caps.enabled_tools = Some(tools);
caps
}
None => CapabilitiesConfig::unrestricted(),
};
capabilities.compaction_threshold = Some(super::COMPACTION_THRESHOLD);
let captured_key = key.to_string();
let auth_provider: Option<crate::backends::KeyProvider> = if base_url.is_some() {
super::access::credit_signer().await.map(|(signer, _addr)| {
std::sync::Arc::new(move || {
let now = (js_sys::Date::now() / 1000.0) as u64;
crate::registry::proxy_auth_token(&signer, now, "gemini")
}) as crate::backends::KeyProvider
})
} else {
None
};
let pending_history = crate::app::history::take_pending();
let agent = if crate::app::model::is_local(&model) {
#[cfg(feature = "local")]
{
let local_system = "You are a helpful assistant running fully in \
the user's browser (Gemma 3 270M on WebGPU). Reply concisely.";
let mut local_caps = capabilities.clone();
local_caps.enabled_tools = Some(vec![]);
local_caps.enable_subagents = false;
let mut cfg = crate::LocalAgentConfig::new(model.clone())
.with_capabilities(local_caps)
.with_policies(vec![policy::allow_all()])
.with_filesystem(crate::app::shared_opfs())
.with_system_instructions(local_system);
let _ = &system_instructions;
if let Some(bytes) = pending_history {
if crate::backends::local::connection::decode_transcript_bytes(&bytes).is_ok() {
cfg = cfg.with_history_bytes(bytes);
}
}
Agent::start_local(cfg)
.await
.map_err(|e| JsValue::from_str(&format!("start_local: {e}")))?
}
#[cfg(not(feature = "local"))]
{
let _ = (&capabilities, &system_instructions, &pending_history);
return Err(JsValue::from_str(
"local model selected but this build was compiled without the `local` feature",
));
}
} else if crate::app::model::is_anthropic(&model) {
let mut cfg = wire_shared_session!(
crate::AnthropicAgentConfig::new(key.to_string()).with_model(model.clone()),
capabilities,
system_instructions,
chat_toolset(
set_persona_allowed,
found_company_allowed,
captured_key,
base_url.clone(),
),
base_url,
auth_provider
)
.with_max_tokens(ANTHROPIC_MAX_OUTPUT_TOKENS)
.with_thinking(session_ceiling.unwrap_or(ThinkingLevel::High))
.with_temperature(0.2);
if let Some(bytes) = pending_history {
if crate::backends::anthropic::history_loads(&bytes) {
cfg = cfg.with_history_bytes(bytes);
}
}
Agent::start_anthropic(cfg)
.await
.map_err(|e| JsValue::from_str(&format!("start_anthropic: {e}")))?
} else {
let mut cfg = wire_shared_session!(
GeminiAgentConfig::new(key.to_string()).with_model(model.clone()),
capabilities,
system_instructions,
chat_toolset(
set_persona_allowed,
found_company_allowed,
captured_key,
base_url.clone(),
),
base_url,
auth_provider
)
.with_max_output_tokens(GEMINI_MAX_OUTPUT_TOKENS)
.with_thinking(session_ceiling.unwrap_or(ThinkingLevel::High))
.with_temperature(0.2);
if let Some(bytes) = pending_history {
if crate::backends::gemini::history_loads(&bytes) {
cfg = cfg.with_history_bytes(bytes);
}
}
Agent::start_gemini(cfg)
.await
.map_err(|e| JsValue::from_str(&format!("start_gemini: {e}")))?
};
let tool_count = agent.tools().names().len();
APP.with(|cell| {
let mut app = cell.borrow_mut();
app.agent = Some(Rc::new(agent));
app.agent_tool_count = Some(tool_count);
app.session_key = Some(identity.to_string());
app.turn_count = 0;
app.session_thinking_ceiling = session_ceiling;
app.session_model = Some(model.clone());
});
if crate::app::dom::by_id("tools-count").is_some() {
crate::app::dom::swap_outer("tools-count", &crate::app::templates::tools_count_span(tool_count));
}
Ok(())
}
const GLOBAL_LESSONS_URL: &str = "https://localharness.xyz/global-lessons.txt";
const GLOBAL_LESSONS_HEADER: &str = "=== GLOBAL LESSONS (learned across the platform) ===";
const GLOBAL_LESSONS_MAX_LINES: usize = 40;
const GLOBAL_LESSONS_MAX_LINE_CHARS: usize = 240;
async fn fetch_global_lessons_section() -> Option<String> {
let body = crate::app::net::read(async {
let resp = reqwest::Client::new()
.get(GLOBAL_LESSONS_URL)
.send()
.await
.ok()?;
if !resp.status().is_success() {
return None;
}
resp.text().await.ok()
})
.await
.ok()
.flatten()?;
let lines: Vec<String> = body
.lines()
.map(str::trim)
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.take(GLOBAL_LESSONS_MAX_LINES)
.map(|l| l.chars().take(GLOBAL_LESSONS_MAX_LINE_CHARS).collect())
.collect();
if lines.is_empty() {
return None;
}
Some(format!("{GLOBAL_LESSONS_HEADER}\n{}", lines.join("\n")))
}