llm: drop model/agent scope matching; add instance-wide compaction model picker
Nightly Build / build (push) Successful in 6m50s

Remove the scope system end-to-end (llm_models.scope column, agent meta
scope field, scope-based tier in model selection, UI checkboxes/pills):
it was only a soft ranking hint, had drifted (6 UI scopes vs 3 used by
agents, 'general' not even selectable) and duplicated what strength
already decides. Strength stays the single AUTO-selection axis.

Compaction: the summary model is now pickable from the Settings page
via a new PropertyType::LlmModel config property (registry key
compaction_model), instance-wide and live (no restart). Fallback chain:
explicit pick -> compaction.strength from config.yml -> priority order;
a deleted configured model degrades to AUTO. ContextCompactor reads the
key at compact time through GlobalConfigManager.
This commit is contained in:
2026-07-25 10:48:09 +01:00
parent 9dafc4bfaa
commit 5081ec2afe
39 changed files with 174 additions and 160 deletions
+4 -4
View File
@@ -81,11 +81,11 @@ Two rules keep the boundary real, and both are enforced by the compiler:
| `crates/skald-core/src/db/` | sqlx SQLite — see below |
| `crates/skald-core/src/users/` | `UserManager` (§11): user directory CRUD on `system.db`, credential check, and the map `userid → SqlitePool` of **unlocked** databases. The pool *is* the unlock token — its connect options carry the DEK as SQLCipher's raw key, so an open pool means the key is in RAM (§9) and dropping it re-locks. Knows nothing about cookies: whatever maps an HTTP session to a user id sits above it |
| `crates/skald-core/src/crypto/` | Envelope encryption (§4/§5.1). A random 256-bit DEK encrypts `{userid}.db`; `users.database_password` holds it sealed with AES-256-GCM under `Argon2id(password, salt)`. **The AEAD tag is the password verifier** — one derivation both authenticates and yields the key, and no second hash sits in the admin-readable DB. Cleartext users store the Argon2id output directly, compared constant-time. Argon2 runs in `spawn_blocking` behind a 2-permit semaphore (256 MiB per derivation) |
| `src/config.rs` | Loads `config.yml`; LLM clients, strength/use_cases, data root. All relative paths (db, logs, data, …) resolve against the launch cwd |
| `src/config.rs` | Loads `config.yml`; LLM clients, strength, data root. All relative paths (db, logs, data, …) resolve against the launch cwd |
| `crates/skald-core/src/mcp/` | MCP runtimes + the `McpProvider` seam (§7): the shared host **global** runtime and the per-user **container** runtimes, unioned per session as `UserMcpView`. See the MCP connectors section |
| `crates/skald-core/src/plugin/` | Plugin system: discovery, enable/disable, tool registration, per-user access grants + per-user config |
| `crates/skald-core/src/cron/` | Scheduled job runner |
| `crates/skald-core/src/compactor.rs` | Context compaction (summarises history when token budget exceeded) |
| `crates/skald-core/src/compactor.rs` | Context compaction (summarises history when token budget exceeded). Model for the summary call: the instance-wide Settings pick (`compaction_model`, a `PropertyType::LlmModel` config property declared by `compactor::config_set`) wins; else AUTO by `compaction.strength` (config.yml); a missing configured model degrades to the same AUTO path |
| `crates/skald-core/src/approval/` | Approval rules engine |
| `crates/skald-core/src/clarification/` | `ClarificationManager`: background-session question/answer |
| `crates/skald-core/src/elicitation/` | `ElicitationManager` + bridge: MCP server-initiated input (`elicitation/create`), surfaced in the Inbox; secrets never logged/persisted |
@@ -215,8 +215,8 @@ The chat streams tokens live, as a **parallel best-effort side-channel** that ne
- Max recursion depth: `MAX_AGENT_DEPTH = 5`.
- **Parallel batches:** when a single assistant response emits **≥2** sync sub-agent calls and *nothing else*, `run_agent_turn` fans them out concurrently via `handle_sub_agent_batch` (bounded by `max_parallel_subagents`, default `4`). Ordering is preserved by allocating every `chat_llm_tools` row up front in call order (the LLM reconstructs results by row id), then recording outcomes back in call order; only the middle dispatch is concurrent. Any other shape (a lone call, or a mix with regular tools) keeps the strictly sequential `handle_tool_call` loop — the two paths share the same lower-level seams. Siblings share the session's scratchpad blackboard (session-keyed): concurrent writes to the *same* key are last-writer-wins by design.
- **Restart recovery of a parallel batch** is intentionally lossy (single-user app): `resume_turn` first calls `reap_interrupted_parallel_batches`, which detects a batch by ≥2 active `chat_sessions_stack` frames at the same depth (impossible for a linear stack), fails their spawning tool calls and terminates the frames, then lets the normal linear cascade resume the parent. A lone interrupted sub-agent is untouched and still recovers via the cascade.
- Client resolution order: `args.client``meta.json client` → AUTO selection by scope/strength.
- **The parent's resolved client is NOT inherited.** Passing a concrete model name to `resolve()` bypasses strength/scope checks; sub-agents always auto-select unless overridden explicitly.
- Client resolution order: `args.client``meta.json client` → AUTO selection by strength.
- **The parent's resolved client is NOT inherited.** Passing a concrete model name to `resolve()` bypasses AUTO selection; sub-agents always auto-select unless overridden explicitly.
- `list_agents` is a plain tool; returns JSON of **task** agents only (excludes `chat`/`system` agents like the `assistant` entry agent).
- `resume_turn` (+ its cascade) is kept only for: app-restart recovery of an active child stack, async task result injection (`inject_async_result`), and the WS resume message — not for the normal sync dispatch.
- **The cascade runs each frame with ITS OWN agent's config, not the session root's.** `resume_turn` builds the root config from `self.agent_id`, but for any non-root frame (deepest seed + each parent it walks up) it derives a per-frame config via `build_recovery_frame_config``build_sub_agent_config` (keyed on `frame.agent_id`), so a resumed sub-agent runs with its own prompt/tools/client — not the root's (it would otherwise resume e.g. a `researcher` as the `assistant`). `build_sub_agent_config` is the **single** source of a sub-agent's config, shared by live `dispatch_sub_agent` and this recovery path so they can't drift; the per-dispatch `client` override isn't persisted, so recovery re-resolves the model from the frame's agent meta.
-1
View File
@@ -14,7 +14,6 @@
},
"instructions": "Pass the idea, the draft business plan, and any market/competitor evidence you have. Specify an output path/dir for the critique report. The more evidence you provide, the sharper the critique — missing evidence is flagged as open questions, not guessed.",
"type": "task",
"scope": "reasoning",
"strength": "high",
"icon": "icon.png"
}
-1
View File
@@ -14,7 +14,6 @@
},
"instructions": "Give it a concrete question or area to investigate (a bug, a module, an architecture concern). It writes a Markdown report to data/explorer/ and returns a summary. It never edits code or plans work.",
"type": "task",
"scope": "reasoning",
"strength": "high",
"icon": "icon.png"
}
-1
View File
@@ -14,7 +14,6 @@
},
"instructions": "Hand it a fully-specified task: what to change and where. It executes but does not plan, decide scope, or QA its own output, so be explicit about the desired outcome.",
"type": "task",
"scope": "general",
"strength": "average",
"icon": "icon.png"
}
-1
View File
@@ -13,7 +13,6 @@
}
},
"type": "chat",
"scope": "reasoning",
"strength": "average",
"inject_memory": ["user-memory/index.md", "shared-memory/index.md", "__PROJECT_ROOT__/SKALD.md"],
"icon": "icon.png"
-1
View File
@@ -14,7 +14,6 @@
},
"instructions": "Pass a specific research question; optionally hint at depth (how many sources) or a time horizon. Optionally specify an output file/dir in the prompt to write the report outside the default `data/research/`. Returns a path + one-line summary, also saved to the scratchpad.",
"type": "task",
"scope": "general",
"strength": "average",
"icon": "icon.png"
}
-1
View File
@@ -14,7 +14,6 @@
},
"instructions": "Describe the change or feature and the relevant part of the codebase. It produces an implementation plan and may delegate the actual edits to software-engineer. Use it when the work needs design before coding.",
"type": "task",
"scope": "reasoning",
"strength": "very_high",
"icon": "icon.png"
}
-1
View File
@@ -14,7 +14,6 @@
},
"instructions": "Give it a clear, scoped implementation task: which files or behaviour to change and the intended result. Best for executing an already-decided design — pair with software-architect when the approach is still open.",
"type": "task",
"scope": "coding",
"strength": "high",
"icon": "icon.png"
}
-1
View File
@@ -14,7 +14,6 @@
},
"instructions": "Provide the idea, the goals, and any constraints. It researches and produces a thorough Markdown spec document. It never writes implementation code — use it before building, not during.",
"type": "task",
"scope": "reasoning",
"strength": "high",
"icon": "icon.png"
}
-1
View File
@@ -14,7 +14,6 @@
},
"instructions": "Point it at project documentation or high-level requirements (and the working directory if relevant). It decomposes the work, sequences tasks by dependency, and orchestrates software-architect/software-engineer to deliver. Best for whole-project builds, not single edits.",
"type": "task",
"scope": "reasoning",
"strength": "very_high",
"icon": "icon.png"
}
+3
View File
@@ -27,6 +27,9 @@ pub enum PropertyType {
SecurityGroup,
/// Dropdown of the interface languages the instance supports.
Locale,
/// Dropdown of the LLM models configured on the instance (by model name,
/// the resolution key). Nullable: empty means "auto-select".
LlmModel,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
-1
View File
@@ -44,7 +44,6 @@ pub struct LlmModelRecord {
pub model_id: String,
pub name: String,
pub strength: Option<LlmStrength>,
pub scope: Vec<String>,
pub is_default: bool,
pub priority: i32,
pub extra_params: Option<serde_json::Value>,
+1 -9
View File
@@ -60,8 +60,6 @@ struct RawMeta {
#[serde(default)]
client: Option<String>,
#[serde(default)]
scope: Option<String>,
#[serde(default)]
strength: Option<LlmStrength>,
/// Required: declares the agent's role. A `meta.json` without `type` fails to load.
#[serde(rename = "type")]
@@ -104,10 +102,6 @@ pub struct AgentMeta {
/// If unset, the sub-agent inherits the caller's client.
#[serde(default)]
pub client: Option<String>,
/// Task domain this agent operates in (e.g. "coding", "reasoning").
/// Used by AUTO client selection to find a matching LLM.
#[serde(default)]
pub scope: Option<String>,
/// Minimum LLM capability required to run this agent reliably.
/// AUTO selection skips clients weaker than this threshold.
#[serde(default)]
@@ -197,13 +191,12 @@ pub fn discover() -> Result<Vec<AgentMeta>> {
instructions: raw.instructions,
inject_memory: raw.inject_memory,
client: raw.client,
scope: raw.scope,
strength: raw.strength,
agent_type: raw.agent_type,
inject_skills: raw.inject_skills,
icon: raw.icon,
};
trace!(agent_id = %meta.id, client = ?meta.client, scope = ?meta.scope, strength = ?meta.strength, "agent meta loaded");
trace!(agent_id = %meta.id, client = ?meta.client, strength = ?meta.strength, "agent meta loaded");
debug!(agent_id = %meta.id, name = %meta.name, "agent discovered");
agents.push(meta);
}
@@ -230,7 +223,6 @@ pub fn load_meta(agent_id: &str) -> Result<AgentMeta> {
instructions: raw.instructions,
inject_memory: raw.inject_memory,
client: raw.client,
scope: raw.scope,
strength: raw.strength,
agent_type: raw.agent_type,
inject_skills: raw.inject_skills,
+50 -4
View File
@@ -49,12 +49,40 @@ use serde_json::json;
use sqlx::SqlitePool;
use tracing::{debug, info, warn};
use core_api::{ConfigProperty, ConfigSet, PropertyType};
use crate::chat_event_bus::{ChatEventBus, CompactionEvent};
use crate::chatbot::ChatOptions;
use crate::config::CompactionConfig;
use crate::config_store::GlobalConfigManager;
use crate::db::{chat_history, chat_llm_tools, chat_summaries};
use crate::llm::LlmManager;
/// Registry `config` key holding the name of the LLM model to use for
/// compaction summaries. Set from the Settings page (instance-wide); empty /
/// unset means AUTO selection by `CompactionConfig.strength` (config.yml).
pub const COMPACTION_MODEL_KEY: &str = "compaction_model";
/// Settings-page section for compaction (see `i18n::config_set` for the
/// pattern). Registered in `Runtime::config_properties`.
pub fn config_set() -> ConfigSet {
ConfigSet {
name: "Compaction".into(),
description: "How conversation history is summarised when the context grows too large.".into(),
properties: vec![
ConfigProperty {
key: COMPACTION_MODEL_KEY.into(),
name: "Compaction model".into(),
description: "Model used to summarise compacted history, for the whole instance. \
A cheap model is usually enough. Leave empty to auto-select \
(by `compaction.strength` in config.yml).".into(),
property_type: PropertyType::LlmModel,
default_value: None,
},
],
}
}
// ── Compaction constants (ported from Hermes context_compressor.py) ──────────
//
// SUMMARY_PREFIX — prepended to every stored summary when injected as context.
@@ -161,6 +189,7 @@ pub struct ContextCompactor {
config: CompactionConfig,
llm_manager: Arc<LlmManager>,
event_bus: Arc<ChatEventBus>,
config_store: Arc<GlobalConfigManager>,
}
impl ContextCompactor {
@@ -168,8 +197,9 @@ impl ContextCompactor {
config: CompactionConfig,
llm_manager: Arc<LlmManager>,
event_bus: Arc<ChatEventBus>,
config_store: Arc<GlobalConfigManager>,
) -> Self {
Self { config, llm_manager, event_bus }
Self { config, llm_manager, event_bus, config_store }
}
/// Attempt to compact the conversation history for `stack_id`.
@@ -291,9 +321,25 @@ impl ContextCompactor {
.format_for_summary(pool, to_summarise, prior_summary.as_ref().map(|s| s.content.as_str()))
.await?;
let (client_name, llm) = self.llm_manager
.resolve(None, None, self.config.strength)
.await?;
// Model for the summary call: the instance-wide Settings pick
// (`compaction_model`) wins; empty/unset falls back to AUTO selection by
// `compaction.strength` from config.yml. A configured model that no
// longer exists (renamed/deleted) degrades to the same AUTO path.
let configured = self.config_store.get(COMPACTION_MODEL_KEY).await
.ok()
.flatten()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
let (client_name, llm) = match configured {
Some(name) => match self.llm_manager.resolve(Some(&name), None).await {
Ok(r) => r,
Err(e) => {
warn!(model = %name, error = %e, "compactor: configured compaction model unavailable, falling back to AUTO selection");
self.llm_manager.resolve(None, self.config.strength).await?
}
},
None => self.llm_manager.resolve(None, self.config.strength).await?,
};
info!(
stack_id,
-1
View File
@@ -206,7 +206,6 @@ async fn create_registry_tables(pool: &SqlitePool) -> Result<()> {
model_id TEXT NOT NULL,
name TEXT NOT NULL UNIQUE,
strength TEXT,
scope TEXT NOT NULL DEFAULT '[]',
is_default INTEGER NOT NULL DEFAULT 0,
priority INTEGER NOT NULL DEFAULT 100,
extra_params TEXT,
+7 -15
View File
@@ -93,7 +93,6 @@ struct ModelRow {
model_id: String,
name: String,
strength: Option<String>,
scope: String,
is_default: i64,
priority: i64,
extra_params: Option<String>,
@@ -106,7 +105,7 @@ struct ModelRow {
pub async fn load_all_models(pool: &SqlitePool) -> Result<Vec<LlmModelRecord>> {
let rows = sqlx::query_as::<_, ModelRow>(
"SELECT id, provider_id, model_id, name, strength, scope, is_default, priority, extra_params,
"SELECT id, provider_id, model_id, name, strength, is_default, priority, extra_params,
context_length, max_output_tokens, knowledge_cutoff, capabilities, reasoning
FROM llm_models
WHERE removed_at IS NULL
@@ -120,7 +119,6 @@ pub async fn load_all_models(pool: &SqlitePool) -> Result<Vec<LlmModelRecord>> {
}
pub async fn insert_model(pool: &SqlitePool, r: &LlmModelRecord) -> Result<i64> {
let scope = serde_json::to_string(&r.scope)?;
let extra_params = r.extra_params.as_ref().map(|v| v.to_string());
let capabilities = serde_json::to_string(&r.capabilities)?;
let reasoning = r.reasoning.as_ref().map(|v| v.to_string());
@@ -132,14 +130,13 @@ pub async fn insert_model(pool: &SqlitePool, r: &LlmModelRecord) -> Result<i64>
// soft-deleted row. Upsert on `name` so that existing row is revived
// (removed_at cleared) and every field overwritten.
let id = sqlx::query_scalar::<_, i64>(
"INSERT INTO llm_models (provider_id, model_id, name, strength, scope, is_default, priority, extra_params,
"INSERT INTO llm_models (provider_id, model_id, name, strength, is_default, priority, extra_params,
context_length, max_output_tokens, knowledge_cutoff, capabilities, reasoning)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)
ON CONFLICT(name) DO UPDATE SET
provider_id = excluded.provider_id,
model_id = excluded.model_id,
strength = excluded.strength,
scope = excluded.scope,
is_default = excluded.is_default,
priority = excluded.priority,
extra_params = excluded.extra_params,
@@ -155,7 +152,6 @@ pub async fn insert_model(pool: &SqlitePool, r: &LlmModelRecord) -> Result<i64>
.bind(&r.model_id)
.bind(&r.name)
.bind(r.strength.map(strength_str))
.bind(scope)
.bind(r.is_default as i64)
.bind(r.priority as i64)
.bind(extra_params)
@@ -172,23 +168,21 @@ pub async fn insert_model(pool: &SqlitePool, r: &LlmModelRecord) -> Result<i64>
}
pub async fn update_model(pool: &SqlitePool, id: i64, r: &LlmModelRecord) -> Result<()> {
let scope = serde_json::to_string(&r.scope)?;
let extra_params = r.extra_params.as_ref().map(|v| v.to_string());
let capabilities = serde_json::to_string(&r.capabilities)?;
let reasoning = r.reasoning.as_ref().map(|v| v.to_string());
sqlx::query(
"UPDATE llm_models
SET provider_id=?1, model_id=?2, name=?3, strength=?4,
scope=?5, is_default=?6, priority=?7, extra_params=?8,
context_length=?9, max_output_tokens=?10, knowledge_cutoff=?11, capabilities=?12,
reasoning=?13
WHERE id=?14",
is_default=?5, priority=?6, extra_params=?7,
context_length=?8, max_output_tokens=?9, knowledge_cutoff=?10, capabilities=?11,
reasoning=?12
WHERE id=?13",
)
.bind(r.provider_id)
.bind(&r.model_id)
.bind(&r.name)
.bind(r.strength.map(strength_str))
.bind(scope)
.bind(r.is_default as i64)
.bind(r.priority as i64)
.bind(extra_params)
@@ -267,7 +261,6 @@ fn provider_row_to_record(r: ProviderRow) -> Result<LlmProviderRecord> {
}
fn model_row_to_record(r: ModelRow) -> Result<LlmModelRecord> {
let scope: Vec<String> = serde_json::from_str(&r.scope).unwrap_or_default();
let extra_params = r.extra_params
.as_deref()
.and_then(|s| serde_json::from_str(s).ok());
@@ -281,7 +274,6 @@ fn model_row_to_record(r: ModelRow) -> Result<LlmModelRecord> {
model_id: r.model_id,
name: r.name,
strength: r.strength.as_deref().and_then(parse_strength),
scope,
is_default: r.is_default != 0,
priority: r.priority as i32,
extra_params,
+8 -19
View File
@@ -100,12 +100,11 @@ impl LlmManager {
pub async fn resolve(
&self,
client_name: Option<&str>,
required_scope: Option<&str>,
required_strength: Option<LlmStrength>,
) -> Result<(String, Arc<LlmEntry>)> {
let name = match client_name {
None | Some(AUTO_CLIENT) => {
let (name, entry) = self.select(required_scope, required_strength).await?;
let (name, entry) = self.select(required_strength).await?;
self.maybe_refresh_meta(&name).await;
return Ok((name, entry));
}
@@ -368,7 +367,6 @@ impl LlmManager {
model_id: slot.model.model_id.clone(),
name: slot.model.name.clone(),
strength: slot.model.strength,
scope: slot.model.scope.clone(),
is_default: slot.model.is_default,
priority: slot.model.priority,
extra_params: slot.model.extra_params.clone(),
@@ -391,7 +389,6 @@ impl LlmManager {
pub async fn select_excluding(
&self,
excluded: &[&str],
required_scope: Option<&str>,
required_strength: Option<LlmStrength>,
) -> Result<(String, Arc<LlmEntry>)> {
let state = self.state.read().await;
@@ -401,7 +398,7 @@ impl LlmManager {
if slots.is_empty() {
anyhow::bail!("no alternative LLM models available");
}
sort_slots_for_agent(&mut slots, required_scope, required_strength);
sort_slots_for_agent(&mut slots, required_strength);
if let Some((name, slot)) = slots.iter().find(|(_, s)| s.health.status != ClientStatus::Down) {
return Ok((name.to_string(), slot.entry.clone()));
}
@@ -414,7 +411,6 @@ impl LlmManager {
async fn select(
&self,
required_scope: Option<&str>,
required_strength: Option<LlmStrength>,
) -> Result<(String, Arc<LlmEntry>)> {
let state = self.state.read().await;
@@ -424,7 +420,7 @@ impl LlmManager {
}
let mut slots: Vec<(&String, &ModelSlot)> = state.models.iter().collect();
sort_slots_for_agent(&mut slots, required_scope, required_strength);
sort_slots_for_agent(&mut slots, required_strength);
if let Some((name, slot)) = slots.iter().find(|(_, s)| s.health.status != ClientStatus::Down) {
return Ok((name.to_string(), slot.entry.clone()));
@@ -526,7 +522,6 @@ fn build_entry(
model: model.model_id.clone(),
model_db_id,
strength: model.strength,
scope: model.scope.clone(),
extra_params: extra,
context_length: model.context_length,
prompt_cache,
@@ -548,28 +543,24 @@ fn build_entry(
pub fn sort_models_for_agent(
mut models: Vec<LlmModelInfo>,
scope: Option<&str>,
strength: Option<LlmStrength>,
) -> Vec<LlmModelInfo> {
models.sort_by_key(|m| (model_tier(m.strength, m.scope.as_slice(), scope, strength), m.priority));
models.sort_by_key(|m| (model_tier(m.strength, strength), m.priority));
models
}
fn sort_slots_for_agent(
slots: &mut Vec<(&String, &ModelSlot)>,
scope: Option<&str>,
strength: Option<LlmStrength>,
) {
slots.sort_by_key(|(_, s)| (
model_tier(s.model.strength, s.model.scope.as_slice(), scope, strength),
model_tier(s.model.strength, strength),
s.model.priority,
));
}
fn model_tier(
model_strength: Option<LlmStrength>,
model_scope: &[String],
req_scope: Option<&str>,
req_strength: Option<LlmStrength>,
) -> u8 {
let strength_ok = match (req_strength, model_strength) {
@@ -583,11 +574,9 @@ fn model_tier(
(Some(req), Some(avail)) => avail == req,
_ => true,
};
let scope_ok = req_scope.map_or(true, |sc| model_scope.iter().any(|x| x == sc));
match (strength_ok && scope_ok, exact_match && scope_ok, strength_ok) {
(true, true, _) => 0, // exact strength + scope ok
(true, false, _) => 1, // over-qualified but scope ok
(false, _, true) => 2, // strength ok, scope mismatch
match (strength_ok, exact_match) {
(true, true) => 0, // exact strength
(true, false) => 1, // over-qualified
_ => 3, // doesn't meet minimum bar
}
}
-2
View File
@@ -17,7 +17,6 @@ pub struct LlmEntry {
pub model: String,
pub model_db_id: i64,
pub strength: Option<LlmStrength>,
pub scope: Vec<String>,
pub extra_params: Option<serde_json::Value>,
/// Max input context window in tokens, if known.
pub context_length: Option<i64>,
@@ -96,7 +95,6 @@ pub struct LlmModelInfo {
pub model_id: String,
pub name: String,
pub strength: Option<LlmStrength>,
pub scope: Vec<String>,
pub is_default: bool,
pub priority: i32,
pub extra_params: Option<serde_json::Value>,
@@ -57,7 +57,6 @@ impl ChatSessionHandler {
let explicit_client = args["client"].as_str().or(target_meta.client.as_deref());
let (resolved_client, _) = self.llm_manager.resolve(
explicit_client,
target_meta.scope.as_deref(),
target_meta.strength,
).await.map_err(|e| anyhow::anyhow!("dispatch_sub_agent: {e}"))?;
@@ -241,7 +240,7 @@ impl ChatSessionHandler {
let meta = crate::agents::load_task_meta(&frame.agent_id)
.map_err(|e| anyhow::anyhow!("resume: cannot load sub-agent `{}`: {e}", frame.agent_id))?;
let (client, _) = self.llm_manager.resolve(
meta.client.as_deref(), meta.scope.as_deref(), meta.strength,
meta.client.as_deref(), meta.strength,
).await?;
self.build_sub_agent_config(root_config, &frame.agent_id, client.to_string(), frame.id, frame.depth).await
}
@@ -50,7 +50,6 @@ impl ChatSessionHandler {
let meta = crate::agents::load_meta(&self.agent_id).ok();
let (key, _) = self.llm_manager.resolve(
client_name.as_deref(),
meta.as_ref().and_then(|m| m.scope.as_deref()),
meta.as_ref().and_then(|m| m.strength),
).await?;
@@ -45,7 +45,6 @@ impl ChatSessionHandler {
stack_id: i64,
config: &AgentRunConfig,
active_grants: &HashSet<String>,
req_scope: Option<&str>,
req_strength: Option<LlmStrength>,
cur_name: &mut String,
cur_llm: &mut Arc<LlmEntry>,
@@ -155,7 +154,7 @@ impl ChatSessionHandler {
}
let excluded: Vec<&str> = tried_this_round.iter().map(String::as_str).collect();
match self.llm_manager.select_excluding(&excluded, req_scope, req_strength).await {
match self.llm_manager.select_excluding(&excluded, req_strength).await {
Ok((next_name, next_llm)) => {
warn!(session_id = self.session_id, from = %cur_name, to = %next_name, "LLM fallback");
em.model_fallback(cur_name.clone(), next_name.clone(), first_line(&e.to_string())).await;
@@ -65,9 +65,8 @@ impl ChatSessionHandler {
let mut cur_llm = self.llm_manager.get(&cur_name).await
.ok_or_else(|| anyhow::anyhow!("LLM client '{}' not found", cur_name))?;
// Scope/strength needed for fallback re-selection.
// Strength needed for fallback re-selection.
let meta = crate::agents::load_meta(&config.agent_id).ok();
let req_scope = meta.as_ref().and_then(|m| m.scope.as_deref()).map(str::to_string);
let req_strength = meta.as_ref().and_then(|m| m.strength);
// Accumulates tool calls across all rounds for the event bus.
@@ -132,7 +131,7 @@ impl ChatSessionHandler {
// retriable errors. `cur_name`/`cur_llm`/`messages` are updated in place.
let turn_result = match self.call_llm_round(
stack_id, config, &active_grants_snapshot,
req_scope.as_deref(), req_strength,
req_strength,
&mut cur_name, &mut cur_llm, &mut messages, token, &em,
).await {
RoundLlm::Turn(t) => t,
+1
View File
@@ -331,6 +331,7 @@ impl Conversation {
cfg.clone(),
Arc::clone(&models.llm_manager),
Arc::clone(&rt.event_bus),
Arc::clone(&rt.config),
))
});
if compactor.is_none() {
+5 -1
View File
@@ -63,7 +63,11 @@ impl Runtime {
users,
sessions,
config,
config_properties: vec![crate::i18n::config_set(), crate::tic::config_set()],
config_properties: vec![
crate::i18n::config_set(),
crate::tic::config_set(),
crate::compactor::config_set(),
],
system_bus,
event_bus,
global_tx,
@@ -43,6 +43,7 @@ use crate::chat_hub::ChatHub;
use crate::clarification::ClarificationManager;
use crate::compactor::ContextCompactor;
use crate::config::{CompactionConfig, CoreConfig, DatetimeConfig};
use crate::config_store::GlobalConfigManager;
use crate::container::ContainerManager;
use crate::cron::TaskManager;
use crate::elicitation::ElicitationManager;
@@ -132,6 +133,7 @@ pub(super) struct UserContextFactory {
event_bus: Arc<ChatEventBus>,
supervisor: Arc<super::supervisor::TaskSupervisor>,
shutdown_token: CancellationToken,
config_store: Arc<GlobalConfigManager>,
max_history_messages: usize,
max_tool_rounds: usize,
max_parallel_subagents: usize,
@@ -166,6 +168,7 @@ impl UserContextFactory {
event_bus: Arc::clone(&rt.event_bus),
supervisor: Arc::clone(&rt.supervisor),
shutdown_token: rt.shutdown_token.clone(),
config_store: Arc::clone(&rt.config),
max_history_messages: config.llm.max_history_messages,
max_tool_rounds: config.llm.max_tool_rounds.unwrap_or(DEFAULT_MAX_TOOL_ROUNDS),
max_parallel_subagents: config.llm.max_parallel_subagents.unwrap_or(DEFAULT_MAX_PARALLEL_SUBAGENTS),
@@ -207,6 +210,7 @@ impl UserContextFactory {
cfg.clone(),
Arc::clone(&self.llm_manager),
Arc::clone(&event_bus),
Arc::clone(&self.config_store),
))
});
+4 -1
View File
@@ -30,7 +30,7 @@ marketplace:
# ── LLM clients ────────────────────────────────────────────────────────────────
# LLM clients (providers, models, API keys, strength, scope) are configured
# LLM clients (providers, models, API keys, strength) are configured
# via the web app and stored in the database — not in this file.
# ───────────────────────────────────────────────────────────────────────────────
llm:
@@ -72,6 +72,9 @@ llm:
# selector (same strength levels used for agent assignment). Compaction is a
# simple writing task — `low` or `average` is usually sufficient.
# Omit `strength` to use whatever AUTO picks.
# NOTE: the Settings page has an instance-wide "Compaction model" picker
# (registry config key `compaction_model`) — when set, it wins over this
# `strength` fallback and needs no restart.
#
# When the LLM provider does not report token usage (e.g. some LM Studio
# setups), a rough estimate (total chars / 4) is used as a fallback.
+1
View File
@@ -11,6 +11,7 @@ This index will grow over time. Right now it covers projects and plugins; more s
| Document | What it covers |
| --- | --- |
| [projects.md](projects.md) | Projects: shared folders with their own assistant chat, a live file explorer, and member sharing |
| [settings.md](settings.md) | The admin's Config page: interface language, TIC agent, the compaction model picker, debug mode |
## Plugins
+27
View File
@@ -0,0 +1,27 @@
# Settings (Config page)
The **Config** page holds instance-wide settings. It is admin-only: what an admin changes here applies to every user of the instance.
Each setting is saved individually with its own **Save** button (a few, like the language, save as soon as they are changed).
## Interface
- **Language** — the default interface language for the whole instance. Each user can override it on their own profile page.
## TIC Agent
TIC is a background agent that watches events from connected MCP servers (new emails, calendar updates, WhatsApp messages…) and decides which ones are worth surfacing as notifications.
- **Enabled** — turn TIC on or off.
- **Security Group** — the tool permission group TIC's sessions run with; leave empty for the default group.
- **Check Interval (minutes)** — how often TIC runs; leave empty for the value from `config.yml`.
## Compaction
When a conversation grows too large, the oldest messages are automatically summarised so the context stays within limits. The summary replaces those messages in future turns while the most recent ones are kept verbatim.
- **Compaction model** — the model used to write those summaries, for the whole instance. Summarising is a simple writing task, so a cheap, fast model is usually the right choice — there is no reason to spend premium-model tokens on it. Leave it empty for automatic selection (by the `compaction.strength` value in `config.yml`, or the instance's default priority order). If the chosen model is later deleted, compaction silently falls back to automatic selection.
## Developer
- **Debug mode** — shows extra technical diagnostics in the interface.
+1 -1
View File
@@ -52,7 +52,7 @@ pub async fn get(
meta.localize(&locale);
let prompt = skald_core::agents::load_prompt(&id)?;
let all = skald.llm_manager().list_models_info().await;
let models = sort_models_for_agent(all, meta.scope.as_deref(), meta.strength);
let models = sort_models_for_agent(all, meta.strength);
Ok(Json(AgentDetail { meta, prompt, models }))
}
+10
View File
@@ -65,6 +65,15 @@ pub async fn list_properties(
name: skald_core::i18n::native_language_name(code),
})
.collect::<Vec<_>>();
// Configured LLM models, keyed by `name` (the resolution key LlmManager
// uses), labelled with the provider for disambiguation.
let llm_models = skald.llm_manager().list_models_info().await
.into_iter()
.map(|m| SelectOption {
id: m.name.clone(),
name: format!("{} ({})", m.name, m.provider_name),
})
.collect::<Vec<_>>();
let mut sets = Vec::with_capacity(skald.config_properties().len());
for set in skald.config_properties() {
@@ -78,6 +87,7 @@ pub async fn list_properties(
PropertyType::String => ("string", None),
PropertyType::SecurityGroup => ("security_group", Some(security_groups.clone())),
PropertyType::Locale => ("locale", Some(locales.clone())),
PropertyType::LlmModel => ("llm_model", Some(llm_models.clone())),
};
props.push(PropertyView {
key: prop.key.clone(),
-2
View File
@@ -162,7 +162,6 @@ pub struct ModelPayload {
pub model_id: String,
pub name: String,
pub strength: Option<String>,
pub scope: Option<Vec<String>>,
pub is_default: Option<bool>,
pub priority: Option<i32>,
pub extra_params: Option<serde_json::Value>,
@@ -184,7 +183,6 @@ impl TryFrom<ModelPayload> for LlmModelRecord {
model_id: p.model_id.clone(),
name: if p.name.is_empty() { p.model_id } else { p.name },
strength: p.strength.as_deref().map(parse_strength).transpose()?,
scope: p.scope.unwrap_or_default(),
is_default: p.is_default.unwrap_or(false),
priority: p.priority.unwrap_or(100),
extra_params: p.extra_params,
-12
View File
@@ -111,10 +111,6 @@ export class AgentsPage extends LightElement {
`;
}
_scopePill(scope) {
return html`<span class="agent-scope-pill">${scope}</span>`;
}
// ── List view ─────────────────────────────────────────────────────────────
_renderCard(agent) {
@@ -137,7 +133,6 @@ export class AgentsPage extends LightElement {
<span>${this._strengthLabel(agent.strength)}</span>
</span>
` : ''}
${agent.scope ? html`${this._scopePill(agent.scope)}` : ''}
${agent.client ? html`
<span class="agent-meta-item text-muted" style="font-size:0.75rem">
<i class="bi bi-pin-fill me-1" style="font-size:0.65rem"></i>${agent.client}
@@ -189,9 +184,6 @@ export class AgentsPage extends LightElement {
${m.is_default ? html`<span class="badge bg-primary ms-1" style="font-size:0.6rem">${t('agents.detail.default')}</span>` : ''}
</td>
<td class="text-muted agent-model-id">${m.model_id}</td>
<td>
${(m.scope ?? []).map(s => this._scopePill(s))}
</td>
</tr>
`;
}
@@ -235,9 +227,6 @@ export class AgentsPage extends LightElement {
</td>
</tr>
` : ''}
${meta.scope ? html`
<tr><td class="agent-meta-key">${t('agents.detail.scope')}</td><td>${this._scopePill(meta.scope)}</td></tr>
` : ''}
${meta.client ? html`
<tr><td class="agent-meta-key">${t('agents.detail.pinned_model')}</td><td><code>${meta.client}</code></td></tr>
` : ''}
@@ -266,7 +255,6 @@ export class AgentsPage extends LightElement {
<th>${t('agents.table.strength')}</th>
<th>${t('agents.table.name')}</th>
<th>${t('agents.table.model_id')}</th>
<th>${t('agents.table.scope')}</th>
</tr>
</thead>
<tbody>
+15
View File
@@ -11,6 +11,7 @@ function _configSetSlug(name) {
const slugs = {
'Interface': 'interface',
'TIC Agent': 'tic_agent',
'Compaction': 'compaction',
};
return slugs[name] ?? null;
}
@@ -193,6 +194,20 @@ export class ConfigPage extends LightElement {
</select>`;
}
if (prop.property_type === 'llm_model') {
// Configured LLM models, by name. Nullable: the empty choice means
// "auto-select" (the backend's own resolution order applies).
const models = prop.options ?? [];
return html`
<select class="form-select form-select-sm config-input"
.value=${val}
@change=${e => this._setValue(prop.key, e.target.value)}>
<option value=""> ${t('config.llm_model.auto')} </option>
${models.map(m => html`
<option value=${m.id} ?selected=${val === m.id}>${m.name}</option>`)}
</select>`;
}
return html`
<input type="text"
class="form-control form-control-sm config-input"
+7 -37
View File
@@ -20,12 +20,11 @@ const STRENGTH_LABELS = {
};
const STRENGTH_OPTIONS = ['very_low', 'low', 'average', 'high', 'very_high'];
const SCOPE_OPTIONS = ['coding', 'writing', 'reasoning', 'math', 'basic', 'search'];
function emptyMeta() {
// `reasoning` is the selected reasoning value: a string for a ValueSet mode,
// a number for a Range mode, or null (off). Interpreted per provider.
return { strength: '', scope: [], priority: 100, is_default: false, reasoning: null };
return { strength: '', priority: 100, is_default: false, reasoning: null };
}
function emptyOrForm() {
@@ -139,7 +138,6 @@ export class ModelsLlmSection extends LightElement {
model_id: m.model_id,
name: m.name,
strength: m.strength ?? null,
scope: m.scope,
is_default: m.is_default,
priority: (i + 1) * 10,
extra_params: m.extra_params ?? null,
@@ -208,7 +206,6 @@ export class ModelsLlmSection extends LightElement {
const record = await res.json();
this._form = {
strength: record.strength ?? '',
scope: record.scope ?? [],
priority: record.priority,
is_default: record.is_default,
provider_id: record.provider_id,
@@ -269,7 +266,6 @@ export class ModelsLlmSection extends LightElement {
model_id: f.model_id,
name: f.name || f.model_id,
strength: f.strength || null,
scope: f.scope,
is_default: f.is_default,
priority: Number(f.priority),
extra_params,
@@ -310,7 +306,6 @@ export class ModelsLlmSection extends LightElement {
model_id: f.model_id,
name: f.name || f.model_id,
strength: f.strength || null,
scope: f.scope,
is_default: f.is_default,
priority: Number(f.priority),
extra_params: Object.keys(extra_params).length ? extra_params : null,
@@ -355,7 +350,6 @@ export class ModelsLlmSection extends LightElement {
model_id: f.model_id,
name: f.name,
strength: f.strength || null,
scope: f.scope,
is_default: f.is_default,
priority: Number(f.priority),
extra_params,
@@ -399,16 +393,6 @@ export class ModelsLlmSection extends LightElement {
this._orForm = { ...this._orForm, [field]: value };
}
_toggleScope(scope, isOr = false) {
if (isOr) {
const s = this._orForm.scope;
this._orForm = { ...this._orForm, scope: s.includes(scope) ? s.filter(x => x !== scope) : [...s, scope] };
} else {
const s = this._form.scope;
this._form = { ...this._form, scope: s.includes(scope) ? s.filter(x => x !== scope) : [...s, scope] };
}
}
_closeModal() { this._modal = null; this._error = null; }
// ── Render helpers ───────────────────────────────────────────────────────────
@@ -487,10 +471,9 @@ export class ModelsLlmSection extends LightElement {
${this._renderPriceCell(m)}
</div>
${(m.scope ?? []).length > 0 || m.extra_params ? html`
${m.extra_params ? html`
<div class="llm-card-row3">
${(m.scope ?? []).map(s => html`<span class="llm-scope-pill">${s}</span>`)}
${m.extra_params ? html`<span class="llm-scope-pill llm-params-pill" title=${JSON.stringify(m.extra_params)}>+${t('models.extra_params').toLowerCase()}</span>` : ''}
<span class="llm-scope-pill llm-params-pill" title=${JSON.stringify(m.extra_params)}>+${t('models.extra_params').toLowerCase()}</span>
</div>
` : ''}
</div>
@@ -540,7 +523,7 @@ export class ModelsLlmSection extends LightElement {
</div>`;
}
_renderMetaFields(form, setField, toggleScope, reasoningMode) {
_renderMetaFields(form, setField, reasoningMode) {
return html`
${this._renderReasoning(form, setField, reasoningMode)}
<div class="row g-3 mb-3">
@@ -561,19 +544,6 @@ export class ModelsLlmSection extends LightElement {
</div>
</div>
<div class="mb-3">
<label class="form-label fw-semibold" style="font-size:0.82rem">${t('models.scope')}</label>
<div class="llm-scope-grid">
${SCOPE_OPTIONS.map(s => html`
<div class="form-check">
<input class="form-check-input" type="checkbox" id="scope-${s}"
.checked=${form.scope.includes(s)} @change=${() => toggleScope(s)} />
<label class="form-check-label" for="scope-${s}" style="font-size:0.82rem">${s}</label>
</div>
`)}
</div>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="m-is-default"
@@ -644,7 +614,7 @@ export class ModelsLlmSection extends LightElement {
@input=${(e) => this._setField('extra_params', e.target.value)}
style="font-size:0.78rem;resize:vertical"></textarea>
</div>
${this._renderMetaFields(f, (k, v) => this._setField(k, v), (s) => this._toggleScope(s), this._reasoningMode)}
${this._renderMetaFields(f, (k, v) => this._setField(k, v), this._reasoningMode)}
<div class="agent-dialog-actions">
<button type="button" class="btn btn-sm btn-secondary" @click=${() => this._closeModal()}>${t('models.cancel')}</button>
<button type="submit" class="btn btn-sm btn-primary" ?disabled=${this._saving}>
@@ -738,7 +708,7 @@ export class ModelsLlmSection extends LightElement {
</div>
` : ''}
${this._renderMetaFields(f, (k, v) => this._setOrField(k, v), (s) => this._toggleScope(s, true), selected?.reasoning)}
${this._renderMetaFields(f, (k, v) => this._setOrField(k, v), selected?.reasoning)}
<div class="agent-dialog-actions">
<button type="button" class="btn btn-sm btn-secondary" @click=${() => this._closeModal()}>${t('models.cancel')}</button>
@@ -776,7 +746,7 @@ export class ModelsLlmSection extends LightElement {
@input=${(e) => this._setField('name', e.target.value)} />
<div class="form-text" style="font-size:0.75rem">${unsafeHTML(t('models.name_help'))}</div>
</div>
${this._renderMetaFields(f, (k, v) => this._setField(k, v), (s) => this._toggleScope(s), this._modal.reasoning_mode)}
${this._renderMetaFields(f, (k, v) => this._setField(k, v), this._modal.reasoning_mode)}
<div class="agent-dialog-actions">
<button type="button" class="btn btn-sm btn-secondary" @click=${() => this._closeModal()}>${t('models.cancel')}</button>
<button type="submit" class="btn btn-sm btn-primary" ?disabled=${this._saving}>
-10
View File
@@ -145,16 +145,6 @@ agents-page {
flex-shrink: 0;
}
.agent-scope-pill {
display: inline-block;
font-size: 0.7rem;
padding: 0.1rem 0.45rem;
border-radius: 999px;
background: var(--bs-secondary-bg);
color: var(--bs-secondary-color);
border: 1px solid var(--bs-border-color);
}
/* ── Detail view ─────────────────────────────────────────────────────────── */
.agent-detail {
-8
View File
@@ -356,14 +356,6 @@
max-width: 680px;
}
/* ── Scope checkbox grid ── */
.llm-scope-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.35rem 1rem;
}
/* ── Provider picker grid ── */
.llm-provider-grid {
+5 -3
View File
@@ -177,11 +177,14 @@ export default {
'config.error_save':'Error saving "{name}": {msg}',
'config.enabled': 'Enabled',
'config.disabled': 'Disabled',
'config.llm_model.auto': 'auto',
'config.set.interface.name': 'Interface',
'config.set.interface.desc': 'Look and feel of the web interface.',
'config.set.tic_agent.name': 'TIC Agent',
'config.set.tic_agent.desc': 'TIC is a background agent that monitors all async events generated by connected MCP servers (new emails, calendar updates, WhatsApp messages, etc.). It reads your notification rules from data/notifications.md and your memory to decide — via an LLM call — which events are worth surfacing. Relevant notifications are forwarded to the home agent set via /sethome.',
'config.set.compaction.name': 'Compaction',
'config.set.compaction.desc': 'When a conversation grows too large, older messages are summarised by an LLM to keep the context within limits.',
'config.prop.ui_locale.name': 'Language',
'config.prop.ui_locale.desc': 'Default interface language for the whole instance. Each user can override it on their profile.',
@@ -191,6 +194,8 @@ export default {
'config.prop.tic__security_group.desc': 'Tool permission group applied to each TIC agent session. Leave empty to use the default group.',
'config.prop.tic__interval_minutes.name': 'Check Interval (minutes)',
'config.prop.tic__interval_minutes.desc': 'How often TIC runs, in minutes. Leave empty to use the value from config.yml (tic.interval_secs).',
'config.prop.compaction_model.name': 'Compaction model',
'config.prop.compaction_model.desc': 'Model used to summarise compacted conversations, for the whole instance. A cheap model is usually enough. Leave empty for automatic selection.',
// ── Projects ────────────────────────────────────────────────────────────────
'projects.title': 'Projects',
@@ -444,7 +449,6 @@ export default {
'models.reasoning.thinking': 'Reasoning (thinking)',
'models.strength': 'Strength',
'models.priority': 'Priority',
'models.scope': 'Scope',
'models.default_model': 'Default model',
'models.extra_params': 'Extra params',
'models.extra_params_hint': '(JSON, optional)',
@@ -743,7 +747,6 @@ export default {
'agents.detail.meta': 'Metadata',
'agents.detail.id': 'ID',
'agents.detail.strength': 'Strength',
'agents.detail.scope': 'Scope',
'agents.detail.pinned_model': 'Pinned model',
'agents.detail.memory_files': 'Memory files',
'agents.detail.model_order': 'Model resolution order',
@@ -756,7 +759,6 @@ export default {
'agents.table.strength': 'Strength',
'agents.table.name': 'Name',
'agents.table.model_id': 'Model ID',
'agents.table.scope': 'Scope',
'agents.banner.title': '<strong>Read-only view.</strong> Agents are defined by files in <code>agents/</code> — to add, remove, or modify an agent, edit the corresponding <code>AGENT.md</code> file in that directory.',
'agents.banner.text': 'You can also ask <strong>Copilot</strong> (top bar) to create a new agent for you — just describe what it should do and it will set up all the files automatically.',
+5 -3
View File
@@ -177,11 +177,14 @@ export default {
'config.error_save':'Erreur lors de l\'enregistrement de "{name}" : {msg}',
'config.enabled': 'Activé',
'config.disabled': 'Désactivé',
'config.llm_model.auto': 'auto',
'config.set.interface.name': 'Interface',
'config.set.interface.desc': 'Aspect et style de l\'interface web.',
'config.set.tic_agent.name': 'Agent TIC',
'config.set.tic_agent.desc': 'TIC est un agent d\'arrière-plan qui surveille tous les événements asynchrones générés par les serveurs MCP connectés (nouveaux e-mails, mises à jour du calendrier, messages WhatsApp, etc.). Il lit vos règles de notification dans data/notifications.md et votre mémoire pour décider — via un appel LLM — quels événements méritent d\'être signalés. Les notifications pertinentes sont transmises à l\'agent d\'accueil défini via /sethome.',
'config.set.compaction.name': 'Compaction',
'config.set.compaction.desc': 'Lorsqu\'une conversation devient trop longue, les messages les plus anciens sont résumés par un LLM pour garder le contexte dans les limites.',
'config.prop.ui_locale.name': 'Langue',
'config.prop.ui_locale.desc': 'Langue d\'interface par défaut pour l\'ensemble de l\'instance. Chaque utilisateur peut la modifier dans son profil.',
@@ -191,6 +194,8 @@ export default {
'config.prop.tic__security_group.desc': 'Groupe de permissions d\'outils appliqué à chaque session de l\'agent TIC. Laissez vide pour utiliser le groupe par défaut.',
'config.prop.tic__interval_minutes.name': 'Intervalle de vérification (minutes)',
'config.prop.tic__interval_minutes.desc': 'Fréquence d\'exécution de TIC, en minutes. Laissez vide pour utiliser la valeur de config.yml (tic.interval_secs).',
'config.prop.compaction_model.name': 'Modèle de compaction',
'config.prop.compaction_model.desc': 'Modèle utilisé pour résumer les conversations compactées, pour toute l\'instance. Un modèle économique suffit généralement. Laissez vide pour une sélection automatique.',
// ── Projects ────────────────────────────────────────────────────────────────
'projects.title': 'Projets',
@@ -444,7 +449,6 @@ export default {
'models.reasoning.thinking': 'Raisonnement (réflexion)',
'models.strength': 'Puissance',
'models.priority': 'Priorité',
'models.scope': 'Portée',
'models.default_model': 'Modèle par défaut',
'models.extra_params': 'Paramètres supplémentaires',
'models.extra_params_hint': '(JSON, facultatif)',
@@ -743,7 +747,6 @@ export default {
'agents.detail.meta': 'Métadonnées',
'agents.detail.id': 'ID',
'agents.detail.strength': 'Puissance',
'agents.detail.scope': 'Portée',
'agents.detail.pinned_model': 'Modèle épinglé',
'agents.detail.memory_files': 'Fichiers mémoire',
'agents.detail.model_order': 'Ordre de résolution des modèles',
@@ -756,7 +759,6 @@ export default {
'agents.table.strength': 'Puissance',
'agents.table.name': 'Nom',
'agents.table.model_id': 'ID du modèle',
'agents.table.scope': 'Portée',
'agents.banner.title': '<strong>Vue en lecture seule.</strong> Les agents sont définis par des fichiers dans <code>agents/</code> — pour ajouter, supprimer ou modifier un agent, modifiez le fichier <code>AGENT.md</code> correspondant dans ce répertoire.',
'agents.banner.text': 'Vous pouvez aussi demander au <strong>Copilot</strong> (barre supérieure) de créer un nouvel agent pour vous — décrivez simplement ce qu\'il doit faire et il configurera tous les fichiers automatiquement.',
+5 -3
View File
@@ -201,11 +201,14 @@ export default {
'config.error_save':'Errore durante il salvataggio di "{name}": {msg}',
'config.enabled': 'Attivato',
'config.disabled': 'Disattivato',
'config.llm_model.auto': 'automatico',
'config.set.interface.name': 'Interfaccia',
'config.set.interface.desc': 'Aspetto e stile dell\'interfaccia web.',
'config.set.tic_agent.name': 'Agente TIC',
'config.set.tic_agent.desc': 'TIC è un agente in background che monitora tutti gli eventi asincroni generati dai server MCP connessi (nuove email, aggiornamenti del calendario, messaggi WhatsApp, ecc.). Legge le regole di notifica da data/notifications.md e la memoria per decidere — tramite una chiamata LLM — quali eventi vale la pena segnalare. Le notifiche rilevanti vengono inoltrate all\'agente predefinito impostato tramite /sethome.',
'config.set.compaction.name': 'Compattazione',
'config.set.compaction.desc': 'Quando una conversazione diventa troppo lunga, i messaggi più vecchi vengono riassunti da un LLM per mantenere il contesto entro i limiti.',
'config.prop.ui_locale.name': 'Lingua',
'config.prop.ui_locale.desc': 'Lingua predefinita per l\'intera istanza. Ogni utente può modificarla nel proprio profilo.',
@@ -215,6 +218,8 @@ export default {
'config.prop.tic__security_group.desc': 'Gruppo di permessi strumenti applicato a ogni sessione dell\'agente TIC. Lascia vuoto per usare il gruppo predefinito.',
'config.prop.tic__interval_minutes.name': 'Intervallo di controllo (minuti)',
'config.prop.tic__interval_minutes.desc': 'Ogni quanto TIC viene eseguito, in minuti. Lascia vuoto per usare il valore da config.yml (tic.interval_secs).',
'config.prop.compaction_model.name': 'Modello per la compattazione',
'config.prop.compaction_model.desc': 'Modello usato per riassumere le conversazioni compattate, per tutta l\'istanza. Un modello economico di solito è sufficiente. Lascia vuoto per la selezione automatica.',
// ── Projects ────────────────────────────────────────────────────────────────
'projects.title': 'Progetti',
@@ -444,7 +449,6 @@ export default {
'models.reasoning.thinking': 'Ragionamento (thinking)',
'models.strength': 'Forza',
'models.priority': 'Priorità',
'models.scope': 'Ambito',
'models.default_model': 'Modello predefinito',
'models.extra_params': 'Parametri extra',
'models.extra_params_hint': '(JSON, opzionale)',
@@ -743,7 +747,6 @@ export default {
'agents.detail.meta': 'Metadati',
'agents.detail.id': 'ID',
'agents.detail.strength': 'Forza',
'agents.detail.scope': 'Ambito',
'agents.detail.pinned_model': 'Modello fissato',
'agents.detail.memory_files': 'File di memoria',
'agents.detail.model_order': 'Ordine di risoluzione modelli',
@@ -756,7 +759,6 @@ export default {
'agents.table.strength': 'Forza',
'agents.table.name': 'Nome',
'agents.table.model_id': 'ID modello',
'agents.table.scope': 'Ambito',
'agents.banner.title': '<strong>Vista sola lettura.</strong> Gli agenti sono definiti da file in <code>agents/</code> — per aggiungere, rimuovere o modificare un agente, modifica il corrispondente file <code>AGENT.md</code> in quella directory.',
'agents.banner.text': 'Puoi anche chiedere a <strong>Copilot</strong> (barra superiore) di creare un nuovo agente per te — descrivi cosa dovrebbe fare e configurerà automaticamente tutti i file necessari.',