Release 0.2.0 #4
@@ -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/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/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) |
|
| `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/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/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/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/approval/` | Approval rules engine |
|
||||||
| `crates/skald-core/src/clarification/` | `ClarificationManager`: background-session question/answer |
|
| `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 |
|
| `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`.
|
- 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.
|
- **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.
|
- **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.
|
- 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 strength/scope checks; sub-agents always auto-select unless overridden explicitly.
|
- **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).
|
- `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.
|
- `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.
|
- **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.
|
||||||
|
|||||||
@@ -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.",
|
"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",
|
"type": "task",
|
||||||
"scope": "reasoning",
|
|
||||||
"strength": "high",
|
"strength": "high",
|
||||||
"icon": "icon.png"
|
"icon": "icon.png"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.",
|
"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",
|
"type": "task",
|
||||||
"scope": "reasoning",
|
|
||||||
"strength": "high",
|
"strength": "high",
|
||||||
"icon": "icon.png"
|
"icon": "icon.png"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.",
|
"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",
|
"type": "task",
|
||||||
"scope": "general",
|
|
||||||
"strength": "average",
|
"strength": "average",
|
||||||
"icon": "icon.png"
|
"icon": "icon.png"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "chat",
|
"type": "chat",
|
||||||
"scope": "reasoning",
|
|
||||||
"strength": "average",
|
"strength": "average",
|
||||||
"inject_memory": ["user-memory/index.md", "shared-memory/index.md", "__PROJECT_ROOT__/SKALD.md"],
|
"inject_memory": ["user-memory/index.md", "shared-memory/index.md", "__PROJECT_ROOT__/SKALD.md"],
|
||||||
"icon": "icon.png"
|
"icon": "icon.png"
|
||||||
|
|||||||
@@ -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.",
|
"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",
|
"type": "task",
|
||||||
"scope": "general",
|
|
||||||
"strength": "average",
|
"strength": "average",
|
||||||
"icon": "icon.png"
|
"icon": "icon.png"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.",
|
"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",
|
"type": "task",
|
||||||
"scope": "reasoning",
|
|
||||||
"strength": "very_high",
|
"strength": "very_high",
|
||||||
"icon": "icon.png"
|
"icon": "icon.png"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.",
|
"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",
|
"type": "task",
|
||||||
"scope": "coding",
|
|
||||||
"strength": "high",
|
"strength": "high",
|
||||||
"icon": "icon.png"
|
"icon": "icon.png"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.",
|
"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",
|
"type": "task",
|
||||||
"scope": "reasoning",
|
|
||||||
"strength": "high",
|
"strength": "high",
|
||||||
"icon": "icon.png"
|
"icon": "icon.png"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.",
|
"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",
|
"type": "task",
|
||||||
"scope": "reasoning",
|
|
||||||
"strength": "very_high",
|
"strength": "very_high",
|
||||||
"icon": "icon.png"
|
"icon": "icon.png"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ pub enum PropertyType {
|
|||||||
SecurityGroup,
|
SecurityGroup,
|
||||||
/// Dropdown of the interface languages the instance supports.
|
/// Dropdown of the interface languages the instance supports.
|
||||||
Locale,
|
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)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ pub struct LlmModelRecord {
|
|||||||
pub model_id: String,
|
pub model_id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub strength: Option<LlmStrength>,
|
pub strength: Option<LlmStrength>,
|
||||||
pub scope: Vec<String>,
|
|
||||||
pub is_default: bool,
|
pub is_default: bool,
|
||||||
pub priority: i32,
|
pub priority: i32,
|
||||||
pub extra_params: Option<serde_json::Value>,
|
pub extra_params: Option<serde_json::Value>,
|
||||||
|
|||||||
@@ -60,8 +60,6 @@ struct RawMeta {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
client: Option<String>,
|
client: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
scope: Option<String>,
|
|
||||||
#[serde(default)]
|
|
||||||
strength: Option<LlmStrength>,
|
strength: Option<LlmStrength>,
|
||||||
/// Required: declares the agent's role. A `meta.json` without `type` fails to load.
|
/// Required: declares the agent's role. A `meta.json` without `type` fails to load.
|
||||||
#[serde(rename = "type")]
|
#[serde(rename = "type")]
|
||||||
@@ -104,10 +102,6 @@ pub struct AgentMeta {
|
|||||||
/// If unset, the sub-agent inherits the caller's client.
|
/// If unset, the sub-agent inherits the caller's client.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub client: Option<String>,
|
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.
|
/// Minimum LLM capability required to run this agent reliably.
|
||||||
/// AUTO selection skips clients weaker than this threshold.
|
/// AUTO selection skips clients weaker than this threshold.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -197,13 +191,12 @@ pub fn discover() -> Result<Vec<AgentMeta>> {
|
|||||||
instructions: raw.instructions,
|
instructions: raw.instructions,
|
||||||
inject_memory: raw.inject_memory,
|
inject_memory: raw.inject_memory,
|
||||||
client: raw.client,
|
client: raw.client,
|
||||||
scope: raw.scope,
|
|
||||||
strength: raw.strength,
|
strength: raw.strength,
|
||||||
agent_type: raw.agent_type,
|
agent_type: raw.agent_type,
|
||||||
inject_skills: raw.inject_skills,
|
inject_skills: raw.inject_skills,
|
||||||
icon: raw.icon,
|
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");
|
debug!(agent_id = %meta.id, name = %meta.name, "agent discovered");
|
||||||
agents.push(meta);
|
agents.push(meta);
|
||||||
}
|
}
|
||||||
@@ -230,7 +223,6 @@ pub fn load_meta(agent_id: &str) -> Result<AgentMeta> {
|
|||||||
instructions: raw.instructions,
|
instructions: raw.instructions,
|
||||||
inject_memory: raw.inject_memory,
|
inject_memory: raw.inject_memory,
|
||||||
client: raw.client,
|
client: raw.client,
|
||||||
scope: raw.scope,
|
|
||||||
strength: raw.strength,
|
strength: raw.strength,
|
||||||
agent_type: raw.agent_type,
|
agent_type: raw.agent_type,
|
||||||
inject_skills: raw.inject_skills,
|
inject_skills: raw.inject_skills,
|
||||||
|
|||||||
@@ -49,12 +49,40 @@ use serde_json::json;
|
|||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, warn};
|
||||||
|
|
||||||
|
use core_api::{ConfigProperty, ConfigSet, PropertyType};
|
||||||
|
|
||||||
use crate::chat_event_bus::{ChatEventBus, CompactionEvent};
|
use crate::chat_event_bus::{ChatEventBus, CompactionEvent};
|
||||||
use crate::chatbot::ChatOptions;
|
use crate::chatbot::ChatOptions;
|
||||||
use crate::config::CompactionConfig;
|
use crate::config::CompactionConfig;
|
||||||
|
use crate::config_store::GlobalConfigManager;
|
||||||
use crate::db::{chat_history, chat_llm_tools, chat_summaries};
|
use crate::db::{chat_history, chat_llm_tools, chat_summaries};
|
||||||
use crate::llm::LlmManager;
|
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) ──────────
|
// ── Compaction constants (ported from Hermes context_compressor.py) ──────────
|
||||||
//
|
//
|
||||||
// SUMMARY_PREFIX — prepended to every stored summary when injected as context.
|
// SUMMARY_PREFIX — prepended to every stored summary when injected as context.
|
||||||
@@ -161,6 +189,7 @@ pub struct ContextCompactor {
|
|||||||
config: CompactionConfig,
|
config: CompactionConfig,
|
||||||
llm_manager: Arc<LlmManager>,
|
llm_manager: Arc<LlmManager>,
|
||||||
event_bus: Arc<ChatEventBus>,
|
event_bus: Arc<ChatEventBus>,
|
||||||
|
config_store: Arc<GlobalConfigManager>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContextCompactor {
|
impl ContextCompactor {
|
||||||
@@ -168,8 +197,9 @@ impl ContextCompactor {
|
|||||||
config: CompactionConfig,
|
config: CompactionConfig,
|
||||||
llm_manager: Arc<LlmManager>,
|
llm_manager: Arc<LlmManager>,
|
||||||
event_bus: Arc<ChatEventBus>,
|
event_bus: Arc<ChatEventBus>,
|
||||||
|
config_store: Arc<GlobalConfigManager>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self { config, llm_manager, event_bus }
|
Self { config, llm_manager, event_bus, config_store }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempt to compact the conversation history for `stack_id`.
|
/// 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()))
|
.format_for_summary(pool, to_summarise, prior_summary.as_ref().map(|s| s.content.as_str()))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let (client_name, llm) = self.llm_manager
|
// Model for the summary call: the instance-wide Settings pick
|
||||||
.resolve(None, None, self.config.strength)
|
// (`compaction_model`) wins; empty/unset falls back to AUTO selection by
|
||||||
.await?;
|
// `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!(
|
info!(
|
||||||
stack_id,
|
stack_id,
|
||||||
|
|||||||
@@ -206,7 +206,6 @@ async fn create_registry_tables(pool: &SqlitePool) -> Result<()> {
|
|||||||
model_id TEXT NOT NULL,
|
model_id TEXT NOT NULL,
|
||||||
name TEXT NOT NULL UNIQUE,
|
name TEXT NOT NULL UNIQUE,
|
||||||
strength TEXT,
|
strength TEXT,
|
||||||
scope TEXT NOT NULL DEFAULT '[]',
|
|
||||||
is_default INTEGER NOT NULL DEFAULT 0,
|
is_default INTEGER NOT NULL DEFAULT 0,
|
||||||
priority INTEGER NOT NULL DEFAULT 100,
|
priority INTEGER NOT NULL DEFAULT 100,
|
||||||
extra_params TEXT,
|
extra_params TEXT,
|
||||||
|
|||||||
@@ -93,7 +93,6 @@ struct ModelRow {
|
|||||||
model_id: String,
|
model_id: String,
|
||||||
name: String,
|
name: String,
|
||||||
strength: Option<String>,
|
strength: Option<String>,
|
||||||
scope: String,
|
|
||||||
is_default: i64,
|
is_default: i64,
|
||||||
priority: i64,
|
priority: i64,
|
||||||
extra_params: Option<String>,
|
extra_params: Option<String>,
|
||||||
@@ -106,7 +105,7 @@ struct ModelRow {
|
|||||||
|
|
||||||
pub async fn load_all_models(pool: &SqlitePool) -> Result<Vec<LlmModelRecord>> {
|
pub async fn load_all_models(pool: &SqlitePool) -> Result<Vec<LlmModelRecord>> {
|
||||||
let rows = sqlx::query_as::<_, ModelRow>(
|
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
|
context_length, max_output_tokens, knowledge_cutoff, capabilities, reasoning
|
||||||
FROM llm_models
|
FROM llm_models
|
||||||
WHERE removed_at IS NULL
|
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> {
|
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 extra_params = r.extra_params.as_ref().map(|v| v.to_string());
|
||||||
let capabilities = serde_json::to_string(&r.capabilities)?;
|
let capabilities = serde_json::to_string(&r.capabilities)?;
|
||||||
let reasoning = r.reasoning.as_ref().map(|v| v.to_string());
|
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
|
// soft-deleted row. Upsert on `name` so that existing row is revived
|
||||||
// (removed_at cleared) and every field overwritten.
|
// (removed_at cleared) and every field overwritten.
|
||||||
let id = sqlx::query_scalar::<_, i64>(
|
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)
|
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
|
ON CONFLICT(name) DO UPDATE SET
|
||||||
provider_id = excluded.provider_id,
|
provider_id = excluded.provider_id,
|
||||||
model_id = excluded.model_id,
|
model_id = excluded.model_id,
|
||||||
strength = excluded.strength,
|
strength = excluded.strength,
|
||||||
scope = excluded.scope,
|
|
||||||
is_default = excluded.is_default,
|
is_default = excluded.is_default,
|
||||||
priority = excluded.priority,
|
priority = excluded.priority,
|
||||||
extra_params = excluded.extra_params,
|
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.model_id)
|
||||||
.bind(&r.name)
|
.bind(&r.name)
|
||||||
.bind(r.strength.map(strength_str))
|
.bind(r.strength.map(strength_str))
|
||||||
.bind(scope)
|
|
||||||
.bind(r.is_default as i64)
|
.bind(r.is_default as i64)
|
||||||
.bind(r.priority as i64)
|
.bind(r.priority as i64)
|
||||||
.bind(extra_params)
|
.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<()> {
|
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 extra_params = r.extra_params.as_ref().map(|v| v.to_string());
|
||||||
let capabilities = serde_json::to_string(&r.capabilities)?;
|
let capabilities = serde_json::to_string(&r.capabilities)?;
|
||||||
let reasoning = r.reasoning.as_ref().map(|v| v.to_string());
|
let reasoning = r.reasoning.as_ref().map(|v| v.to_string());
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
"UPDATE llm_models
|
"UPDATE llm_models
|
||||||
SET provider_id=?1, model_id=?2, name=?3, strength=?4,
|
SET provider_id=?1, model_id=?2, name=?3, strength=?4,
|
||||||
scope=?5, is_default=?6, priority=?7, extra_params=?8,
|
is_default=?5, priority=?6, extra_params=?7,
|
||||||
context_length=?9, max_output_tokens=?10, knowledge_cutoff=?11, capabilities=?12,
|
context_length=?8, max_output_tokens=?9, knowledge_cutoff=?10, capabilities=?11,
|
||||||
reasoning=?13
|
reasoning=?12
|
||||||
WHERE id=?14",
|
WHERE id=?13",
|
||||||
)
|
)
|
||||||
.bind(r.provider_id)
|
.bind(r.provider_id)
|
||||||
.bind(&r.model_id)
|
.bind(&r.model_id)
|
||||||
.bind(&r.name)
|
.bind(&r.name)
|
||||||
.bind(r.strength.map(strength_str))
|
.bind(r.strength.map(strength_str))
|
||||||
.bind(scope)
|
|
||||||
.bind(r.is_default as i64)
|
.bind(r.is_default as i64)
|
||||||
.bind(r.priority as i64)
|
.bind(r.priority as i64)
|
||||||
.bind(extra_params)
|
.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> {
|
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
|
let extra_params = r.extra_params
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.and_then(|s| serde_json::from_str(s).ok());
|
.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,
|
model_id: r.model_id,
|
||||||
name: r.name,
|
name: r.name,
|
||||||
strength: r.strength.as_deref().and_then(parse_strength),
|
strength: r.strength.as_deref().and_then(parse_strength),
|
||||||
scope,
|
|
||||||
is_default: r.is_default != 0,
|
is_default: r.is_default != 0,
|
||||||
priority: r.priority as i32,
|
priority: r.priority as i32,
|
||||||
extra_params,
|
extra_params,
|
||||||
|
|||||||
@@ -100,12 +100,11 @@ impl LlmManager {
|
|||||||
pub async fn resolve(
|
pub async fn resolve(
|
||||||
&self,
|
&self,
|
||||||
client_name: Option<&str>,
|
client_name: Option<&str>,
|
||||||
required_scope: Option<&str>,
|
|
||||||
required_strength: Option<LlmStrength>,
|
required_strength: Option<LlmStrength>,
|
||||||
) -> Result<(String, Arc<LlmEntry>)> {
|
) -> Result<(String, Arc<LlmEntry>)> {
|
||||||
let name = match client_name {
|
let name = match client_name {
|
||||||
None | Some(AUTO_CLIENT) => {
|
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;
|
self.maybe_refresh_meta(&name).await;
|
||||||
return Ok((name, entry));
|
return Ok((name, entry));
|
||||||
}
|
}
|
||||||
@@ -368,7 +367,6 @@ impl LlmManager {
|
|||||||
model_id: slot.model.model_id.clone(),
|
model_id: slot.model.model_id.clone(),
|
||||||
name: slot.model.name.clone(),
|
name: slot.model.name.clone(),
|
||||||
strength: slot.model.strength,
|
strength: slot.model.strength,
|
||||||
scope: slot.model.scope.clone(),
|
|
||||||
is_default: slot.model.is_default,
|
is_default: slot.model.is_default,
|
||||||
priority: slot.model.priority,
|
priority: slot.model.priority,
|
||||||
extra_params: slot.model.extra_params.clone(),
|
extra_params: slot.model.extra_params.clone(),
|
||||||
@@ -391,7 +389,6 @@ impl LlmManager {
|
|||||||
pub async fn select_excluding(
|
pub async fn select_excluding(
|
||||||
&self,
|
&self,
|
||||||
excluded: &[&str],
|
excluded: &[&str],
|
||||||
required_scope: Option<&str>,
|
|
||||||
required_strength: Option<LlmStrength>,
|
required_strength: Option<LlmStrength>,
|
||||||
) -> Result<(String, Arc<LlmEntry>)> {
|
) -> Result<(String, Arc<LlmEntry>)> {
|
||||||
let state = self.state.read().await;
|
let state = self.state.read().await;
|
||||||
@@ -401,7 +398,7 @@ impl LlmManager {
|
|||||||
if slots.is_empty() {
|
if slots.is_empty() {
|
||||||
anyhow::bail!("no alternative LLM models available");
|
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) {
|
if let Some((name, slot)) = slots.iter().find(|(_, s)| s.health.status != ClientStatus::Down) {
|
||||||
return Ok((name.to_string(), slot.entry.clone()));
|
return Ok((name.to_string(), slot.entry.clone()));
|
||||||
}
|
}
|
||||||
@@ -414,7 +411,6 @@ impl LlmManager {
|
|||||||
|
|
||||||
async fn select(
|
async fn select(
|
||||||
&self,
|
&self,
|
||||||
required_scope: Option<&str>,
|
|
||||||
required_strength: Option<LlmStrength>,
|
required_strength: Option<LlmStrength>,
|
||||||
) -> Result<(String, Arc<LlmEntry>)> {
|
) -> Result<(String, Arc<LlmEntry>)> {
|
||||||
let state = self.state.read().await;
|
let state = self.state.read().await;
|
||||||
@@ -424,7 +420,7 @@ impl LlmManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut slots: Vec<(&String, &ModelSlot)> = state.models.iter().collect();
|
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) {
|
if let Some((name, slot)) = slots.iter().find(|(_, s)| s.health.status != ClientStatus::Down) {
|
||||||
return Ok((name.to_string(), slot.entry.clone()));
|
return Ok((name.to_string(), slot.entry.clone()));
|
||||||
@@ -526,7 +522,6 @@ fn build_entry(
|
|||||||
model: model.model_id.clone(),
|
model: model.model_id.clone(),
|
||||||
model_db_id,
|
model_db_id,
|
||||||
strength: model.strength,
|
strength: model.strength,
|
||||||
scope: model.scope.clone(),
|
|
||||||
extra_params: extra,
|
extra_params: extra,
|
||||||
context_length: model.context_length,
|
context_length: model.context_length,
|
||||||
prompt_cache,
|
prompt_cache,
|
||||||
@@ -548,28 +543,24 @@ fn build_entry(
|
|||||||
|
|
||||||
pub fn sort_models_for_agent(
|
pub fn sort_models_for_agent(
|
||||||
mut models: Vec<LlmModelInfo>,
|
mut models: Vec<LlmModelInfo>,
|
||||||
scope: Option<&str>,
|
|
||||||
strength: Option<LlmStrength>,
|
strength: Option<LlmStrength>,
|
||||||
) -> Vec<LlmModelInfo> {
|
) -> 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
|
models
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sort_slots_for_agent(
|
fn sort_slots_for_agent(
|
||||||
slots: &mut Vec<(&String, &ModelSlot)>,
|
slots: &mut Vec<(&String, &ModelSlot)>,
|
||||||
scope: Option<&str>,
|
|
||||||
strength: Option<LlmStrength>,
|
strength: Option<LlmStrength>,
|
||||||
) {
|
) {
|
||||||
slots.sort_by_key(|(_, s)| (
|
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,
|
s.model.priority,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn model_tier(
|
fn model_tier(
|
||||||
model_strength: Option<LlmStrength>,
|
model_strength: Option<LlmStrength>,
|
||||||
model_scope: &[String],
|
|
||||||
req_scope: Option<&str>,
|
|
||||||
req_strength: Option<LlmStrength>,
|
req_strength: Option<LlmStrength>,
|
||||||
) -> u8 {
|
) -> u8 {
|
||||||
let strength_ok = match (req_strength, model_strength) {
|
let strength_ok = match (req_strength, model_strength) {
|
||||||
@@ -583,11 +574,9 @@ fn model_tier(
|
|||||||
(Some(req), Some(avail)) => avail == req,
|
(Some(req), Some(avail)) => avail == req,
|
||||||
_ => true,
|
_ => true,
|
||||||
};
|
};
|
||||||
let scope_ok = req_scope.map_or(true, |sc| model_scope.iter().any(|x| x == sc));
|
match (strength_ok, exact_match) {
|
||||||
match (strength_ok && scope_ok, exact_match && scope_ok, strength_ok) {
|
(true, true) => 0, // exact strength
|
||||||
(true, true, _) => 0, // exact strength + scope ok
|
(true, false) => 1, // over-qualified
|
||||||
(true, false, _) => 1, // over-qualified but scope ok
|
|
||||||
(false, _, true) => 2, // strength ok, scope mismatch
|
|
||||||
_ => 3, // doesn't meet minimum bar
|
_ => 3, // doesn't meet minimum bar
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ pub struct LlmEntry {
|
|||||||
pub model: String,
|
pub model: String,
|
||||||
pub model_db_id: i64,
|
pub model_db_id: i64,
|
||||||
pub strength: Option<LlmStrength>,
|
pub strength: Option<LlmStrength>,
|
||||||
pub scope: Vec<String>,
|
|
||||||
pub extra_params: Option<serde_json::Value>,
|
pub extra_params: Option<serde_json::Value>,
|
||||||
/// Max input context window in tokens, if known.
|
/// Max input context window in tokens, if known.
|
||||||
pub context_length: Option<i64>,
|
pub context_length: Option<i64>,
|
||||||
@@ -96,7 +95,6 @@ pub struct LlmModelInfo {
|
|||||||
pub model_id: String,
|
pub model_id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub strength: Option<LlmStrength>,
|
pub strength: Option<LlmStrength>,
|
||||||
pub scope: Vec<String>,
|
|
||||||
pub is_default: bool,
|
pub is_default: bool,
|
||||||
pub priority: i32,
|
pub priority: i32,
|
||||||
pub extra_params: Option<serde_json::Value>,
|
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 explicit_client = args["client"].as_str().or(target_meta.client.as_deref());
|
||||||
let (resolved_client, _) = self.llm_manager.resolve(
|
let (resolved_client, _) = self.llm_manager.resolve(
|
||||||
explicit_client,
|
explicit_client,
|
||||||
target_meta.scope.as_deref(),
|
|
||||||
target_meta.strength,
|
target_meta.strength,
|
||||||
).await.map_err(|e| anyhow::anyhow!("dispatch_sub_agent: {e}"))?;
|
).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)
|
let meta = crate::agents::load_task_meta(&frame.agent_id)
|
||||||
.map_err(|e| anyhow::anyhow!("resume: cannot load sub-agent `{}`: {e}", frame.agent_id))?;
|
.map_err(|e| anyhow::anyhow!("resume: cannot load sub-agent `{}`: {e}", frame.agent_id))?;
|
||||||
let (client, _) = self.llm_manager.resolve(
|
let (client, _) = self.llm_manager.resolve(
|
||||||
meta.client.as_deref(), meta.scope.as_deref(), meta.strength,
|
meta.client.as_deref(), meta.strength,
|
||||||
).await?;
|
).await?;
|
||||||
self.build_sub_agent_config(root_config, &frame.agent_id, client.to_string(), frame.id, frame.depth).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 meta = crate::agents::load_meta(&self.agent_id).ok();
|
||||||
let (key, _) = self.llm_manager.resolve(
|
let (key, _) = self.llm_manager.resolve(
|
||||||
client_name.as_deref(),
|
client_name.as_deref(),
|
||||||
meta.as_ref().and_then(|m| m.scope.as_deref()),
|
|
||||||
meta.as_ref().and_then(|m| m.strength),
|
meta.as_ref().and_then(|m| m.strength),
|
||||||
).await?;
|
).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ impl ChatSessionHandler {
|
|||||||
stack_id: i64,
|
stack_id: i64,
|
||||||
config: &AgentRunConfig,
|
config: &AgentRunConfig,
|
||||||
active_grants: &HashSet<String>,
|
active_grants: &HashSet<String>,
|
||||||
req_scope: Option<&str>,
|
|
||||||
req_strength: Option<LlmStrength>,
|
req_strength: Option<LlmStrength>,
|
||||||
cur_name: &mut String,
|
cur_name: &mut String,
|
||||||
cur_llm: &mut Arc<LlmEntry>,
|
cur_llm: &mut Arc<LlmEntry>,
|
||||||
@@ -155,7 +154,7 @@ impl ChatSessionHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let excluded: Vec<&str> = tried_this_round.iter().map(String::as_str).collect();
|
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)) => {
|
Ok((next_name, next_llm)) => {
|
||||||
warn!(session_id = self.session_id, from = %cur_name, to = %next_name, "LLM fallback");
|
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;
|
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
|
let mut cur_llm = self.llm_manager.get(&cur_name).await
|
||||||
.ok_or_else(|| anyhow::anyhow!("LLM client '{}' not found", cur_name))?;
|
.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 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);
|
let req_strength = meta.as_ref().and_then(|m| m.strength);
|
||||||
|
|
||||||
// Accumulates tool calls across all rounds for the event bus.
|
// 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.
|
// retriable errors. `cur_name`/`cur_llm`/`messages` are updated in place.
|
||||||
let turn_result = match self.call_llm_round(
|
let turn_result = match self.call_llm_round(
|
||||||
stack_id, config, &active_grants_snapshot,
|
stack_id, config, &active_grants_snapshot,
|
||||||
req_scope.as_deref(), req_strength,
|
req_strength,
|
||||||
&mut cur_name, &mut cur_llm, &mut messages, token, &em,
|
&mut cur_name, &mut cur_llm, &mut messages, token, &em,
|
||||||
).await {
|
).await {
|
||||||
RoundLlm::Turn(t) => t,
|
RoundLlm::Turn(t) => t,
|
||||||
|
|||||||
@@ -331,6 +331,7 @@ impl Conversation {
|
|||||||
cfg.clone(),
|
cfg.clone(),
|
||||||
Arc::clone(&models.llm_manager),
|
Arc::clone(&models.llm_manager),
|
||||||
Arc::clone(&rt.event_bus),
|
Arc::clone(&rt.event_bus),
|
||||||
|
Arc::clone(&rt.config),
|
||||||
))
|
))
|
||||||
});
|
});
|
||||||
if compactor.is_none() {
|
if compactor.is_none() {
|
||||||
|
|||||||
@@ -63,7 +63,11 @@ impl Runtime {
|
|||||||
users,
|
users,
|
||||||
sessions,
|
sessions,
|
||||||
config,
|
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,
|
system_bus,
|
||||||
event_bus,
|
event_bus,
|
||||||
global_tx,
|
global_tx,
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ use crate::chat_hub::ChatHub;
|
|||||||
use crate::clarification::ClarificationManager;
|
use crate::clarification::ClarificationManager;
|
||||||
use crate::compactor::ContextCompactor;
|
use crate::compactor::ContextCompactor;
|
||||||
use crate::config::{CompactionConfig, CoreConfig, DatetimeConfig};
|
use crate::config::{CompactionConfig, CoreConfig, DatetimeConfig};
|
||||||
|
use crate::config_store::GlobalConfigManager;
|
||||||
use crate::container::ContainerManager;
|
use crate::container::ContainerManager;
|
||||||
use crate::cron::TaskManager;
|
use crate::cron::TaskManager;
|
||||||
use crate::elicitation::ElicitationManager;
|
use crate::elicitation::ElicitationManager;
|
||||||
@@ -132,6 +133,7 @@ pub(super) struct UserContextFactory {
|
|||||||
event_bus: Arc<ChatEventBus>,
|
event_bus: Arc<ChatEventBus>,
|
||||||
supervisor: Arc<super::supervisor::TaskSupervisor>,
|
supervisor: Arc<super::supervisor::TaskSupervisor>,
|
||||||
shutdown_token: CancellationToken,
|
shutdown_token: CancellationToken,
|
||||||
|
config_store: Arc<GlobalConfigManager>,
|
||||||
max_history_messages: usize,
|
max_history_messages: usize,
|
||||||
max_tool_rounds: usize,
|
max_tool_rounds: usize,
|
||||||
max_parallel_subagents: usize,
|
max_parallel_subagents: usize,
|
||||||
@@ -166,6 +168,7 @@ impl UserContextFactory {
|
|||||||
event_bus: Arc::clone(&rt.event_bus),
|
event_bus: Arc::clone(&rt.event_bus),
|
||||||
supervisor: Arc::clone(&rt.supervisor),
|
supervisor: Arc::clone(&rt.supervisor),
|
||||||
shutdown_token: rt.shutdown_token.clone(),
|
shutdown_token: rt.shutdown_token.clone(),
|
||||||
|
config_store: Arc::clone(&rt.config),
|
||||||
max_history_messages: config.llm.max_history_messages,
|
max_history_messages: config.llm.max_history_messages,
|
||||||
max_tool_rounds: config.llm.max_tool_rounds.unwrap_or(DEFAULT_MAX_TOOL_ROUNDS),
|
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),
|
max_parallel_subagents: config.llm.max_parallel_subagents.unwrap_or(DEFAULT_MAX_PARALLEL_SUBAGENTS),
|
||||||
@@ -207,6 +210,7 @@ impl UserContextFactory {
|
|||||||
cfg.clone(),
|
cfg.clone(),
|
||||||
Arc::clone(&self.llm_manager),
|
Arc::clone(&self.llm_manager),
|
||||||
Arc::clone(&event_bus),
|
Arc::clone(&event_bus),
|
||||||
|
Arc::clone(&self.config_store),
|
||||||
))
|
))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -30,7 +30,7 @@ marketplace:
|
|||||||
|
|
||||||
|
|
||||||
# ── LLM clients ────────────────────────────────────────────────────────────────
|
# ── 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.
|
# via the web app and stored in the database — not in this file.
|
||||||
# ───────────────────────────────────────────────────────────────────────────────
|
# ───────────────────────────────────────────────────────────────────────────────
|
||||||
llm:
|
llm:
|
||||||
@@ -72,6 +72,9 @@ llm:
|
|||||||
# selector (same strength levels used for agent assignment). Compaction is a
|
# selector (same strength levels used for agent assignment). Compaction is a
|
||||||
# simple writing task — `low` or `average` is usually sufficient.
|
# simple writing task — `low` or `average` is usually sufficient.
|
||||||
# Omit `strength` to use whatever AUTO picks.
|
# 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
|
# 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.
|
# setups), a rough estimate (total chars / 4) is used as a fallback.
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ This index will grow over time. Right now it covers projects and plugins; more s
|
|||||||
| Document | What it covers |
|
| Document | What it covers |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| [projects.md](projects.md) | Projects: shared folders with their own assistant chat, a live file explorer, and member sharing |
|
| [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
|
## Plugins
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -52,7 +52,7 @@ pub async fn get(
|
|||||||
meta.localize(&locale);
|
meta.localize(&locale);
|
||||||
let prompt = skald_core::agents::load_prompt(&id)?;
|
let prompt = skald_core::agents::load_prompt(&id)?;
|
||||||
let all = skald.llm_manager().list_models_info().await;
|
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 }))
|
Ok(Json(AgentDetail { meta, prompt, models }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,15 @@ pub async fn list_properties(
|
|||||||
name: skald_core::i18n::native_language_name(code),
|
name: skald_core::i18n::native_language_name(code),
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.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());
|
let mut sets = Vec::with_capacity(skald.config_properties().len());
|
||||||
for set in skald.config_properties() {
|
for set in skald.config_properties() {
|
||||||
@@ -78,6 +87,7 @@ pub async fn list_properties(
|
|||||||
PropertyType::String => ("string", None),
|
PropertyType::String => ("string", None),
|
||||||
PropertyType::SecurityGroup => ("security_group", Some(security_groups.clone())),
|
PropertyType::SecurityGroup => ("security_group", Some(security_groups.clone())),
|
||||||
PropertyType::Locale => ("locale", Some(locales.clone())),
|
PropertyType::Locale => ("locale", Some(locales.clone())),
|
||||||
|
PropertyType::LlmModel => ("llm_model", Some(llm_models.clone())),
|
||||||
};
|
};
|
||||||
props.push(PropertyView {
|
props.push(PropertyView {
|
||||||
key: prop.key.clone(),
|
key: prop.key.clone(),
|
||||||
|
|||||||
@@ -162,7 +162,6 @@ pub struct ModelPayload {
|
|||||||
pub model_id: String,
|
pub model_id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub strength: Option<String>,
|
pub strength: Option<String>,
|
||||||
pub scope: Option<Vec<String>>,
|
|
||||||
pub is_default: Option<bool>,
|
pub is_default: Option<bool>,
|
||||||
pub priority: Option<i32>,
|
pub priority: Option<i32>,
|
||||||
pub extra_params: Option<serde_json::Value>,
|
pub extra_params: Option<serde_json::Value>,
|
||||||
@@ -184,7 +183,6 @@ impl TryFrom<ModelPayload> for LlmModelRecord {
|
|||||||
model_id: p.model_id.clone(),
|
model_id: p.model_id.clone(),
|
||||||
name: if p.name.is_empty() { p.model_id } else { p.name },
|
name: if p.name.is_empty() { p.model_id } else { p.name },
|
||||||
strength: p.strength.as_deref().map(parse_strength).transpose()?,
|
strength: p.strength.as_deref().map(parse_strength).transpose()?,
|
||||||
scope: p.scope.unwrap_or_default(),
|
|
||||||
is_default: p.is_default.unwrap_or(false),
|
is_default: p.is_default.unwrap_or(false),
|
||||||
priority: p.priority.unwrap_or(100),
|
priority: p.priority.unwrap_or(100),
|
||||||
extra_params: p.extra_params,
|
extra_params: p.extra_params,
|
||||||
|
|||||||
@@ -111,10 +111,6 @@ export class AgentsPage extends LightElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
_scopePill(scope) {
|
|
||||||
return html`<span class="agent-scope-pill">${scope}</span>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── List view ─────────────────────────────────────────────────────────────
|
// ── List view ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
_renderCard(agent) {
|
_renderCard(agent) {
|
||||||
@@ -137,7 +133,6 @@ export class AgentsPage extends LightElement {
|
|||||||
<span>${this._strengthLabel(agent.strength)}</span>
|
<span>${this._strengthLabel(agent.strength)}</span>
|
||||||
</span>
|
</span>
|
||||||
` : ''}
|
` : ''}
|
||||||
${agent.scope ? html`${this._scopePill(agent.scope)}` : ''}
|
|
||||||
${agent.client ? html`
|
${agent.client ? html`
|
||||||
<span class="agent-meta-item text-muted" style="font-size:0.75rem">
|
<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}
|
<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>` : ''}
|
${m.is_default ? html`<span class="badge bg-primary ms-1" style="font-size:0.6rem">${t('agents.detail.default')}</span>` : ''}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-muted agent-model-id">${m.model_id}</td>
|
<td class="text-muted agent-model-id">${m.model_id}</td>
|
||||||
<td>
|
|
||||||
${(m.scope ?? []).map(s => this._scopePill(s))}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -235,9 +227,6 @@ export class AgentsPage extends LightElement {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</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`
|
${meta.client ? html`
|
||||||
<tr><td class="agent-meta-key">${t('agents.detail.pinned_model')}</td><td><code>${meta.client}</code></td></tr>
|
<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.strength')}</th>
|
||||||
<th>${t('agents.table.name')}</th>
|
<th>${t('agents.table.name')}</th>
|
||||||
<th>${t('agents.table.model_id')}</th>
|
<th>${t('agents.table.model_id')}</th>
|
||||||
<th>${t('agents.table.scope')}</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ function _configSetSlug(name) {
|
|||||||
const slugs = {
|
const slugs = {
|
||||||
'Interface': 'interface',
|
'Interface': 'interface',
|
||||||
'TIC Agent': 'tic_agent',
|
'TIC Agent': 'tic_agent',
|
||||||
|
'Compaction': 'compaction',
|
||||||
};
|
};
|
||||||
return slugs[name] ?? null;
|
return slugs[name] ?? null;
|
||||||
}
|
}
|
||||||
@@ -193,6 +194,20 @@ export class ConfigPage extends LightElement {
|
|||||||
</select>`;
|
</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`
|
return html`
|
||||||
<input type="text"
|
<input type="text"
|
||||||
class="form-control form-control-sm config-input"
|
class="form-control form-control-sm config-input"
|
||||||
|
|||||||
@@ -20,12 +20,11 @@ const STRENGTH_LABELS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const STRENGTH_OPTIONS = ['very_low', 'low', 'average', 'high', 'very_high'];
|
const STRENGTH_OPTIONS = ['very_low', 'low', 'average', 'high', 'very_high'];
|
||||||
const SCOPE_OPTIONS = ['coding', 'writing', 'reasoning', 'math', 'basic', 'search'];
|
|
||||||
|
|
||||||
function emptyMeta() {
|
function emptyMeta() {
|
||||||
// `reasoning` is the selected reasoning value: a string for a ValueSet mode,
|
// `reasoning` is the selected reasoning value: a string for a ValueSet mode,
|
||||||
// a number for a Range mode, or null (off). Interpreted per provider.
|
// 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() {
|
function emptyOrForm() {
|
||||||
@@ -139,7 +138,6 @@ export class ModelsLlmSection extends LightElement {
|
|||||||
model_id: m.model_id,
|
model_id: m.model_id,
|
||||||
name: m.name,
|
name: m.name,
|
||||||
strength: m.strength ?? null,
|
strength: m.strength ?? null,
|
||||||
scope: m.scope,
|
|
||||||
is_default: m.is_default,
|
is_default: m.is_default,
|
||||||
priority: (i + 1) * 10,
|
priority: (i + 1) * 10,
|
||||||
extra_params: m.extra_params ?? null,
|
extra_params: m.extra_params ?? null,
|
||||||
@@ -208,7 +206,6 @@ export class ModelsLlmSection extends LightElement {
|
|||||||
const record = await res.json();
|
const record = await res.json();
|
||||||
this._form = {
|
this._form = {
|
||||||
strength: record.strength ?? '',
|
strength: record.strength ?? '',
|
||||||
scope: record.scope ?? [],
|
|
||||||
priority: record.priority,
|
priority: record.priority,
|
||||||
is_default: record.is_default,
|
is_default: record.is_default,
|
||||||
provider_id: record.provider_id,
|
provider_id: record.provider_id,
|
||||||
@@ -269,7 +266,6 @@ export class ModelsLlmSection extends LightElement {
|
|||||||
model_id: f.model_id,
|
model_id: f.model_id,
|
||||||
name: f.name || f.model_id,
|
name: f.name || f.model_id,
|
||||||
strength: f.strength || null,
|
strength: f.strength || null,
|
||||||
scope: f.scope,
|
|
||||||
is_default: f.is_default,
|
is_default: f.is_default,
|
||||||
priority: Number(f.priority),
|
priority: Number(f.priority),
|
||||||
extra_params,
|
extra_params,
|
||||||
@@ -310,7 +306,6 @@ export class ModelsLlmSection extends LightElement {
|
|||||||
model_id: f.model_id,
|
model_id: f.model_id,
|
||||||
name: f.name || f.model_id,
|
name: f.name || f.model_id,
|
||||||
strength: f.strength || null,
|
strength: f.strength || null,
|
||||||
scope: f.scope,
|
|
||||||
is_default: f.is_default,
|
is_default: f.is_default,
|
||||||
priority: Number(f.priority),
|
priority: Number(f.priority),
|
||||||
extra_params: Object.keys(extra_params).length ? extra_params : null,
|
extra_params: Object.keys(extra_params).length ? extra_params : null,
|
||||||
@@ -355,7 +350,6 @@ export class ModelsLlmSection extends LightElement {
|
|||||||
model_id: f.model_id,
|
model_id: f.model_id,
|
||||||
name: f.name,
|
name: f.name,
|
||||||
strength: f.strength || null,
|
strength: f.strength || null,
|
||||||
scope: f.scope,
|
|
||||||
is_default: f.is_default,
|
is_default: f.is_default,
|
||||||
priority: Number(f.priority),
|
priority: Number(f.priority),
|
||||||
extra_params,
|
extra_params,
|
||||||
@@ -399,16 +393,6 @@ export class ModelsLlmSection extends LightElement {
|
|||||||
this._orForm = { ...this._orForm, [field]: value };
|
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; }
|
_closeModal() { this._modal = null; this._error = null; }
|
||||||
|
|
||||||
// ── Render helpers ───────────────────────────────────────────────────────────
|
// ── Render helpers ───────────────────────────────────────────────────────────
|
||||||
@@ -487,10 +471,9 @@ export class ModelsLlmSection extends LightElement {
|
|||||||
${this._renderPriceCell(m)}
|
${this._renderPriceCell(m)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${(m.scope ?? []).length > 0 || m.extra_params ? html`
|
${m.extra_params ? html`
|
||||||
<div class="llm-card-row3">
|
<div class="llm-card-row3">
|
||||||
${(m.scope ?? []).map(s => html`<span class="llm-scope-pill">${s}</span>`)}
|
<span class="llm-scope-pill llm-params-pill" title=${JSON.stringify(m.extra_params)}>+${t('models.extra_params').toLowerCase()}</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>` : ''}
|
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
@@ -540,7 +523,7 @@ export class ModelsLlmSection extends LightElement {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderMetaFields(form, setField, toggleScope, reasoningMode) {
|
_renderMetaFields(form, setField, reasoningMode) {
|
||||||
return html`
|
return html`
|
||||||
${this._renderReasoning(form, setField, reasoningMode)}
|
${this._renderReasoning(form, setField, reasoningMode)}
|
||||||
<div class="row g-3 mb-3">
|
<div class="row g-3 mb-3">
|
||||||
@@ -561,19 +544,6 @@ export class ModelsLlmSection extends LightElement {
|
|||||||
</div>
|
</div>
|
||||||
</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="mb-3">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input class="form-check-input" type="checkbox" id="m-is-default"
|
<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)}
|
@input=${(e) => this._setField('extra_params', e.target.value)}
|
||||||
style="font-size:0.78rem;resize:vertical"></textarea>
|
style="font-size:0.78rem;resize:vertical"></textarea>
|
||||||
</div>
|
</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">
|
<div class="agent-dialog-actions">
|
||||||
<button type="button" class="btn btn-sm btn-secondary" @click=${() => this._closeModal()}>${t('models.cancel')}</button>
|
<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}>
|
<button type="submit" class="btn btn-sm btn-primary" ?disabled=${this._saving}>
|
||||||
@@ -738,7 +708,7 @@ export class ModelsLlmSection extends LightElement {
|
|||||||
</div>
|
</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">
|
<div class="agent-dialog-actions">
|
||||||
<button type="button" class="btn btn-sm btn-secondary" @click=${() => this._closeModal()}>${t('models.cancel')}</button>
|
<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)} />
|
@input=${(e) => this._setField('name', e.target.value)} />
|
||||||
<div class="form-text" style="font-size:0.75rem">${unsafeHTML(t('models.name_help'))}</div>
|
<div class="form-text" style="font-size:0.75rem">${unsafeHTML(t('models.name_help'))}</div>
|
||||||
</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">
|
<div class="agent-dialog-actions">
|
||||||
<button type="button" class="btn btn-sm btn-secondary" @click=${() => this._closeModal()}>${t('models.cancel')}</button>
|
<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}>
|
<button type="submit" class="btn btn-sm btn-primary" ?disabled=${this._saving}>
|
||||||
|
|||||||
@@ -145,16 +145,6 @@ agents-page {
|
|||||||
flex-shrink: 0;
|
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 ─────────────────────────────────────────────────────────── */
|
/* ── Detail view ─────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
.agent-detail {
|
.agent-detail {
|
||||||
|
|||||||
@@ -356,14 +356,6 @@
|
|||||||
max-width: 680px;
|
max-width: 680px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Scope checkbox grid ── */
|
|
||||||
|
|
||||||
.llm-scope-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 0.35rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Provider picker grid ── */
|
/* ── Provider picker grid ── */
|
||||||
|
|
||||||
.llm-provider-grid {
|
.llm-provider-grid {
|
||||||
|
|||||||
+5
-3
@@ -177,11 +177,14 @@ export default {
|
|||||||
'config.error_save':'Error saving "{name}": {msg}',
|
'config.error_save':'Error saving "{name}": {msg}',
|
||||||
'config.enabled': 'Enabled',
|
'config.enabled': 'Enabled',
|
||||||
'config.disabled': 'Disabled',
|
'config.disabled': 'Disabled',
|
||||||
|
'config.llm_model.auto': 'auto',
|
||||||
|
|
||||||
'config.set.interface.name': 'Interface',
|
'config.set.interface.name': 'Interface',
|
||||||
'config.set.interface.desc': 'Look and feel of the web interface.',
|
'config.set.interface.desc': 'Look and feel of the web interface.',
|
||||||
'config.set.tic_agent.name': 'TIC Agent',
|
'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.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.name': 'Language',
|
||||||
'config.prop.ui_locale.desc': 'Default interface language for the whole instance. Each user can override it on their profile.',
|
'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__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.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.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 ────────────────────────────────────────────────────────────────
|
||||||
'projects.title': 'Projects',
|
'projects.title': 'Projects',
|
||||||
@@ -444,7 +449,6 @@ export default {
|
|||||||
'models.reasoning.thinking': 'Reasoning (thinking)',
|
'models.reasoning.thinking': 'Reasoning (thinking)',
|
||||||
'models.strength': 'Strength',
|
'models.strength': 'Strength',
|
||||||
'models.priority': 'Priority',
|
'models.priority': 'Priority',
|
||||||
'models.scope': 'Scope',
|
|
||||||
'models.default_model': 'Default model',
|
'models.default_model': 'Default model',
|
||||||
'models.extra_params': 'Extra params',
|
'models.extra_params': 'Extra params',
|
||||||
'models.extra_params_hint': '(JSON, optional)',
|
'models.extra_params_hint': '(JSON, optional)',
|
||||||
@@ -743,7 +747,6 @@ export default {
|
|||||||
'agents.detail.meta': 'Metadata',
|
'agents.detail.meta': 'Metadata',
|
||||||
'agents.detail.id': 'ID',
|
'agents.detail.id': 'ID',
|
||||||
'agents.detail.strength': 'Strength',
|
'agents.detail.strength': 'Strength',
|
||||||
'agents.detail.scope': 'Scope',
|
|
||||||
'agents.detail.pinned_model': 'Pinned model',
|
'agents.detail.pinned_model': 'Pinned model',
|
||||||
'agents.detail.memory_files': 'Memory files',
|
'agents.detail.memory_files': 'Memory files',
|
||||||
'agents.detail.model_order': 'Model resolution order',
|
'agents.detail.model_order': 'Model resolution order',
|
||||||
@@ -756,7 +759,6 @@ export default {
|
|||||||
'agents.table.strength': 'Strength',
|
'agents.table.strength': 'Strength',
|
||||||
'agents.table.name': 'Name',
|
'agents.table.name': 'Name',
|
||||||
'agents.table.model_id': 'Model ID',
|
'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.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.',
|
'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
@@ -177,11 +177,14 @@ export default {
|
|||||||
'config.error_save':'Erreur lors de l\'enregistrement de "{name}" : {msg}',
|
'config.error_save':'Erreur lors de l\'enregistrement de "{name}" : {msg}',
|
||||||
'config.enabled': 'Activé',
|
'config.enabled': 'Activé',
|
||||||
'config.disabled': 'Désactivé',
|
'config.disabled': 'Désactivé',
|
||||||
|
'config.llm_model.auto': 'auto',
|
||||||
|
|
||||||
'config.set.interface.name': 'Interface',
|
'config.set.interface.name': 'Interface',
|
||||||
'config.set.interface.desc': 'Aspect et style de l\'interface web.',
|
'config.set.interface.desc': 'Aspect et style de l\'interface web.',
|
||||||
'config.set.tic_agent.name': 'Agent TIC',
|
'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.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.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.',
|
'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__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.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.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 ────────────────────────────────────────────────────────────────
|
||||||
'projects.title': 'Projets',
|
'projects.title': 'Projets',
|
||||||
@@ -444,7 +449,6 @@ export default {
|
|||||||
'models.reasoning.thinking': 'Raisonnement (réflexion)',
|
'models.reasoning.thinking': 'Raisonnement (réflexion)',
|
||||||
'models.strength': 'Puissance',
|
'models.strength': 'Puissance',
|
||||||
'models.priority': 'Priorité',
|
'models.priority': 'Priorité',
|
||||||
'models.scope': 'Portée',
|
|
||||||
'models.default_model': 'Modèle par défaut',
|
'models.default_model': 'Modèle par défaut',
|
||||||
'models.extra_params': 'Paramètres supplémentaires',
|
'models.extra_params': 'Paramètres supplémentaires',
|
||||||
'models.extra_params_hint': '(JSON, facultatif)',
|
'models.extra_params_hint': '(JSON, facultatif)',
|
||||||
@@ -743,7 +747,6 @@ export default {
|
|||||||
'agents.detail.meta': 'Métadonnées',
|
'agents.detail.meta': 'Métadonnées',
|
||||||
'agents.detail.id': 'ID',
|
'agents.detail.id': 'ID',
|
||||||
'agents.detail.strength': 'Puissance',
|
'agents.detail.strength': 'Puissance',
|
||||||
'agents.detail.scope': 'Portée',
|
|
||||||
'agents.detail.pinned_model': 'Modèle épinglé',
|
'agents.detail.pinned_model': 'Modèle épinglé',
|
||||||
'agents.detail.memory_files': 'Fichiers mémoire',
|
'agents.detail.memory_files': 'Fichiers mémoire',
|
||||||
'agents.detail.model_order': 'Ordre de résolution des modèles',
|
'agents.detail.model_order': 'Ordre de résolution des modèles',
|
||||||
@@ -756,7 +759,6 @@ export default {
|
|||||||
'agents.table.strength': 'Puissance',
|
'agents.table.strength': 'Puissance',
|
||||||
'agents.table.name': 'Nom',
|
'agents.table.name': 'Nom',
|
||||||
'agents.table.model_id': 'ID du modèle',
|
'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.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.',
|
'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
@@ -201,11 +201,14 @@ export default {
|
|||||||
'config.error_save':'Errore durante il salvataggio di "{name}": {msg}',
|
'config.error_save':'Errore durante il salvataggio di "{name}": {msg}',
|
||||||
'config.enabled': 'Attivato',
|
'config.enabled': 'Attivato',
|
||||||
'config.disabled': 'Disattivato',
|
'config.disabled': 'Disattivato',
|
||||||
|
'config.llm_model.auto': 'automatico',
|
||||||
|
|
||||||
'config.set.interface.name': 'Interfaccia',
|
'config.set.interface.name': 'Interfaccia',
|
||||||
'config.set.interface.desc': 'Aspetto e stile dell\'interfaccia web.',
|
'config.set.interface.desc': 'Aspetto e stile dell\'interfaccia web.',
|
||||||
'config.set.tic_agent.name': 'Agente TIC',
|
'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.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.name': 'Lingua',
|
||||||
'config.prop.ui_locale.desc': 'Lingua predefinita per l\'intera istanza. Ogni utente può modificarla nel proprio profilo.',
|
'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__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.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.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 ────────────────────────────────────────────────────────────────
|
||||||
'projects.title': 'Progetti',
|
'projects.title': 'Progetti',
|
||||||
@@ -444,7 +449,6 @@ export default {
|
|||||||
'models.reasoning.thinking': 'Ragionamento (thinking)',
|
'models.reasoning.thinking': 'Ragionamento (thinking)',
|
||||||
'models.strength': 'Forza',
|
'models.strength': 'Forza',
|
||||||
'models.priority': 'Priorità',
|
'models.priority': 'Priorità',
|
||||||
'models.scope': 'Ambito',
|
|
||||||
'models.default_model': 'Modello predefinito',
|
'models.default_model': 'Modello predefinito',
|
||||||
'models.extra_params': 'Parametri extra',
|
'models.extra_params': 'Parametri extra',
|
||||||
'models.extra_params_hint': '(JSON, opzionale)',
|
'models.extra_params_hint': '(JSON, opzionale)',
|
||||||
@@ -743,7 +747,6 @@ export default {
|
|||||||
'agents.detail.meta': 'Metadati',
|
'agents.detail.meta': 'Metadati',
|
||||||
'agents.detail.id': 'ID',
|
'agents.detail.id': 'ID',
|
||||||
'agents.detail.strength': 'Forza',
|
'agents.detail.strength': 'Forza',
|
||||||
'agents.detail.scope': 'Ambito',
|
|
||||||
'agents.detail.pinned_model': 'Modello fissato',
|
'agents.detail.pinned_model': 'Modello fissato',
|
||||||
'agents.detail.memory_files': 'File di memoria',
|
'agents.detail.memory_files': 'File di memoria',
|
||||||
'agents.detail.model_order': 'Ordine di risoluzione modelli',
|
'agents.detail.model_order': 'Ordine di risoluzione modelli',
|
||||||
@@ -756,7 +759,6 @@ export default {
|
|||||||
'agents.table.strength': 'Forza',
|
'agents.table.strength': 'Forza',
|
||||||
'agents.table.name': 'Nome',
|
'agents.table.name': 'Nome',
|
||||||
'agents.table.model_id': 'ID modello',
|
'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.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.',
|
'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.',
|
||||||
|
|||||||
Reference in New Issue
Block a user