instructionファイルの定義・読み込みの実装
This commit is contained in:
@@ -58,7 +58,7 @@ pub struct ProviderConfigPartial {
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct WorkerManifestConfig {
|
||||
#[serde(default)]
|
||||
pub system_prompt: Option<String>,
|
||||
pub instruction: Option<String>,
|
||||
#[serde(default)]
|
||||
pub max_tokens: Option<u32>,
|
||||
#[serde(default)]
|
||||
@@ -179,7 +179,7 @@ impl ProviderConfigPartial {
|
||||
impl WorkerManifestConfig {
|
||||
fn merge(self, upper: Self) -> Self {
|
||||
Self {
|
||||
system_prompt: upper.system_prompt.or(self.system_prompt),
|
||||
instruction: upper.instruction.or(self.instruction),
|
||||
max_tokens: upper.max_tokens.or(self.max_tokens),
|
||||
max_turns: upper.max_turns.or(self.max_turns),
|
||||
temperature: upper.temperature.or(self.temperature),
|
||||
@@ -275,7 +275,10 @@ impl TryFrom<PodManifestConfig> for PodManifest {
|
||||
)?;
|
||||
|
||||
let worker = WorkerManifest {
|
||||
system_prompt: cfg.worker.system_prompt,
|
||||
instruction: cfg
|
||||
.worker
|
||||
.instruction
|
||||
.unwrap_or_else(|| defaults::DEFAULT_INSTRUCTION.to_string()),
|
||||
max_tokens: cfg.worker.max_tokens,
|
||||
max_turns: cfg.worker.max_turns,
|
||||
temperature: cfg.worker.temperature,
|
||||
|
||||
@@ -21,3 +21,8 @@ pub const PRUNE_MIN_SAVINGS: u64 = 4096;
|
||||
/// Number of most-recent turns retained after a compact. See
|
||||
/// [`crate::CompactionConfig::compact_retained_turns`].
|
||||
pub const COMPACT_RETAINED_TURNS: usize = 2;
|
||||
|
||||
/// Default instruction asset reference used when `worker.instruction`
|
||||
/// is omitted. See the `PromptLoader` prefix addressing scheme for the
|
||||
/// `$insomnia/` / `$user/` / `$workspace/` namespaces.
|
||||
pub const DEFAULT_INSTRUCTION: &str = "$insomnia/default";
|
||||
|
||||
@@ -78,8 +78,13 @@ impl ProviderKind {
|
||||
/// Worker-level configuration embedded in the manifest.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WorkerManifest {
|
||||
#[serde(default)]
|
||||
pub system_prompt: Option<String>,
|
||||
/// Reference to the instruction prompt asset used as the body of
|
||||
/// the worker's system prompt. Uses the `PromptLoader` prefix
|
||||
/// addressing scheme (`$insomnia/...`, `$user/...`,
|
||||
/// `$workspace/...`) and is always populated after resolution —
|
||||
/// unset manifests fall through to [`defaults::DEFAULT_INSTRUCTION`].
|
||||
#[serde(default = "default_instruction")]
|
||||
pub instruction: String,
|
||||
#[serde(default)]
|
||||
pub max_tokens: Option<u32>,
|
||||
#[serde(default)]
|
||||
@@ -115,6 +120,10 @@ fn default_tool_output_max_bytes() -> usize {
|
||||
defaults::TOOL_OUTPUT_MAX_BYTES
|
||||
}
|
||||
|
||||
fn default_instruction() -> String {
|
||||
defaults::DEFAULT_INSTRUCTION.to_string()
|
||||
}
|
||||
|
||||
impl Default for ToolOutputLimits {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@@ -270,7 +279,7 @@ permission = "write"
|
||||
assert!(manifest.provider.api_key_file.is_none());
|
||||
assert_eq!(manifest.scope.allow.len(), 1);
|
||||
assert!(manifest.scope.deny.is_empty());
|
||||
assert!(manifest.worker.system_prompt.is_none());
|
||||
assert_eq!(manifest.worker.instruction, defaults::DEFAULT_INSTRUCTION);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -286,7 +295,7 @@ model = "claude-sonnet-4-20250514"
|
||||
api_key_file = "~/.config/insomnia/keys/anthropic"
|
||||
|
||||
[worker]
|
||||
system_prompt = "You are a code reviewer."
|
||||
instruction = "$user/reviewer"
|
||||
max_tokens = 4096
|
||||
temperature = 0.3
|
||||
|
||||
@@ -310,10 +319,7 @@ permission = "write"
|
||||
manifest.provider.api_key_file.as_deref(),
|
||||
Some(std::path::Path::new("~/.config/insomnia/keys/anthropic"))
|
||||
);
|
||||
assert_eq!(
|
||||
manifest.worker.system_prompt.as_deref(),
|
||||
Some("You are a code reviewer.")
|
||||
);
|
||||
assert_eq!(manifest.worker.instruction, "$user/reviewer");
|
||||
assert_eq!(manifest.worker.max_tokens, Some(4096));
|
||||
assert_eq!(manifest.worker.temperature, Some(0.3));
|
||||
let allow = &manifest.scope.allow;
|
||||
|
||||
@@ -150,32 +150,42 @@ impl Scope {
|
||||
/// Human-readable grouping of allow rules, suitable for embedding in
|
||||
/// LLM system prompts. Deny rules are intentionally omitted — they
|
||||
/// only cap effective permission and surface them would mislead the
|
||||
/// reader about what paths are accessible.
|
||||
/// reader about what paths are accessible. Rules with
|
||||
/// `recursive = false` are tagged with a trailing `[non-recursive]`
|
||||
/// marker so the model does not assume child paths are included.
|
||||
///
|
||||
/// ```text
|
||||
/// Readable:
|
||||
/// - /abs/path1
|
||||
/// - /abs/path1 [non-recursive]
|
||||
/// Writable:
|
||||
/// - /abs/path2
|
||||
/// ```
|
||||
pub fn summary(&self) -> String {
|
||||
fn push_rule(out: &mut String, rule: &ResolvedRule) {
|
||||
out.push_str(" - ");
|
||||
out.push_str(&rule.target.display().to_string());
|
||||
if !rule.recursive {
|
||||
out.push_str(" [non-recursive]");
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
|
||||
let mut out = String::new();
|
||||
let readable: Vec<_> = self.readable_paths().collect();
|
||||
if !readable.is_empty() {
|
||||
if !self.allow.is_empty() {
|
||||
out.push_str("Readable:\n");
|
||||
for p in &readable {
|
||||
out.push_str(" - ");
|
||||
out.push_str(&p.display().to_string());
|
||||
out.push('\n');
|
||||
for rule in &self.allow {
|
||||
push_rule(&mut out, rule);
|
||||
}
|
||||
}
|
||||
let writable: Vec<_> = self.writable_paths().collect();
|
||||
let writable: Vec<&ResolvedRule> = self
|
||||
.allow
|
||||
.iter()
|
||||
.filter(|r| r.permission == Permission::Write)
|
||||
.collect();
|
||||
if !writable.is_empty() {
|
||||
out.push_str("Writable:\n");
|
||||
for p in &writable {
|
||||
out.push_str(" - ");
|
||||
out.push_str(&p.display().to_string());
|
||||
out.push('\n');
|
||||
for rule in &writable {
|
||||
push_rule(&mut out, rule);
|
||||
}
|
||||
}
|
||||
if out.ends_with('\n') {
|
||||
@@ -427,6 +437,41 @@ mod tests {
|
||||
assert!(!summary.contains("secret"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn summary_marks_non_recursive_rules() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
let docs = dir.path().join("docs");
|
||||
std::fs::create_dir(&docs).unwrap();
|
||||
let cfg = ScopeConfig {
|
||||
allow: vec![
|
||||
ScopeRule {
|
||||
target: docs.clone(),
|
||||
permission: Permission::Read,
|
||||
recursive: false,
|
||||
},
|
||||
ScopeRule {
|
||||
target: dir.path().to_path_buf(),
|
||||
permission: Permission::Write,
|
||||
recursive: true,
|
||||
},
|
||||
],
|
||||
deny: Vec::new(),
|
||||
};
|
||||
let scope = Scope::from_config(&cfg, dir.path()).unwrap();
|
||||
let summary = scope.summary();
|
||||
let docs_canon = docs.canonicalize().unwrap().display().to_string();
|
||||
let dir_canon = dir.path().canonicalize().unwrap().display().to_string();
|
||||
assert!(
|
||||
summary.contains(&format!("{docs_canon} [non-recursive]")),
|
||||
"expected non-recursive marker in: {summary}"
|
||||
);
|
||||
// Recursive rule must NOT carry the marker.
|
||||
assert!(
|
||||
!summary.contains(&format!("{dir_canon} [non-recursive]")),
|
||||
"recursive rule incorrectly marked: {summary}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readable_paths_includes_writable() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user