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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions core/config/examples/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ listen = "127.0.0.1:4500"
# data_dir defaults to <AREAL_HARNESS_HOME>/state.

[model]
# 仅 Responses 且服务支持 WebSocket 时显式开启;默认 false。
# responses_websocket = true
# reasoning_effort = "xhigh"
# 仅选择 responses 且模型支持摘要时启用。
# reasoning_summary = "auto"
Expand Down
13 changes: 11 additions & 2 deletions core/config/src/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,18 +208,26 @@ fn walk(
| "max_tool_buffer_bytes"
| "context_window_bytes"
| "context_window_tokens"
| "context_target_tokens"
| "context_output_reserve_tokens"
| "context_recent_bytes"
| "max_completion_retries"
]
| ["model", "max_output_tokens" | "max_retries" | "top_k"]
| [
"model",
"max_output_tokens" | "summary_max_output_tokens" | "max_retries" | "top_k"
]
);
let string = matches!(
names.as_slice(),
["server", "listen" | "data_dir"]
| [
"model",
"provider" | "name" | "reasoning_effort" | "reasoning_summary"
"provider"
| "name"
| "reasoning_effort"
| "reasoning_summary"
| "summary_reasoning_effort"
]
| [
"model",
Expand All @@ -241,6 +249,7 @@ fn walk(
let boolean = matches!(
names.as_slice(),
["limits", "watchdog_disable" | "context_compaction_enabled"]
| ["model", "responses_websocket"]
);
if !numeric && !string && !decimal && !boolean {
return Err(error(
Expand Down
16 changes: 14 additions & 2 deletions core/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,13 @@ pub struct SelectedModelConfig {
pub protocol: ModelProtocolConfig,
pub api_key_env: Option<String>,
pub reasoning_effort: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub summary_reasoning_effort: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub summary_max_output_tokens: Option<u64>,
pub reasoning_summary: Option<String>,
#[serde(default, skip_serializing_if = "is_false")]
pub responses_websocket: bool,
pub temperature: Option<f64>,
pub top_p: Option<f64>,
pub top_k: Option<i64>,
Expand All @@ -163,6 +169,10 @@ pub struct SelectedModelConfig {
pub max_retries: usize,
}

fn is_false(value: &bool) -> bool {
!*value
}

impl SelectedModelConfig {
/// 只比较配置和凭据引用;密钥值不进入登记或历史。
pub fn fingerprint(&self) -> String {
Expand Down Expand Up @@ -226,6 +236,7 @@ pub struct ResolvedCoreConfig {
pub context_window_bytes: usize,
pub context_compaction_enabled: bool,
pub context_window_tokens: usize,
pub context_target_tokens: usize,
pub context_output_reserve_tokens: usize,
pub context_recent_bytes: usize,
pub max_completion_retries: usize,
Expand Down Expand Up @@ -260,8 +271,9 @@ impl ResolvedCoreConfig {
"tools": { "extensions_file": self.tool_extensions_file },
"model": { "provider": self.model.provider, "name": self.model.name,
"endpoint": endpoint.as_str(), "protocol": self.model.protocol,
"api_key_env": self.model.api_key_env, "reasoning_effort": self.model.reasoning_effort,
"api_key_env": self.model.api_key_env, "reasoning_effort": self.model.reasoning_effort, "summary_reasoning_effort":self.model.summary_reasoning_effort,"summary_max_output_tokens":self.model.summary_max_output_tokens,
"reasoning_summary": self.model.reasoning_summary,
"responses_websocket": self.model.responses_websocket,
"temperature": self.model.temperature,
"top_p": self.model.top_p,
"top_k": self.model.top_k,
Expand All @@ -272,7 +284,7 @@ impl ResolvedCoreConfig {
"limits": { "max_active_turns": self.max_active_turns, "max_children_per_turn": self.max_children_per_turn, "max_agent_depth": self.max_agent_depth, "model_concurrency": self.model_concurrency, "max_threads": self.max_threads,
"stream_idle_timeout_seconds": self.stream_idle_timeout_seconds,
"max_history_bytes": self.max_history_bytes, "max_output_bytes": self.max_output_bytes, "max_tool_calls": self.max_tool_calls, "max_tool_buffer_bytes": self.max_tool_buffer_bytes,
"context_window_bytes": self.context_window_bytes, "context_compaction_enabled": self.context_compaction_enabled, "context_window_tokens":self.context_window_tokens, "context_output_reserve_tokens":self.context_output_reserve_tokens, "context_recent_bytes": self.context_recent_bytes,
"context_window_bytes": self.context_window_bytes, "context_compaction_enabled": self.context_compaction_enabled, "context_window_tokens":self.context_window_tokens,"context_target_tokens":self.context_target_tokens, "context_output_reserve_tokens":self.context_output_reserve_tokens, "context_recent_bytes": self.context_recent_bytes,
"max_completion_retries": self.max_completion_retries, "watchdog_disable": self.watchdog_disable },
"logging": { "filter": self.log_filter },
});
Expand Down
65 changes: 62 additions & 3 deletions core/config/src/resolve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,26 @@ use std::{
};

const ENV: &[(&str, &str, &str)] = &[
(
"AREAL_HARNESS_SUMMARY_REASONING_EFFORT",
"",
"model.summary_reasoning_effort",
),
(
"AREAL_HARNESS_SUMMARY_MAX_OUTPUT_TOKENS",
"",
"model.summary_max_output_tokens",
),
(
"AREAL_HARNESS_CONTEXT_TARGET_TOKENS",
"",
"limits.context_target_tokens",
),
(
"AREAL_HARNESS_RESPONSES_WEBSOCKET",
"",
"model.responses_websocket",
),
("AREAL_HARNESS_PERMISSION_MODE", "", "permissions.mode"),
("AREAL_HARNESS_LISTEN", "", "server.listen"),
("AREAL_HARNESS_DATA_DIR", "", "server.data_dir"),
Expand Down Expand Up @@ -273,7 +293,7 @@ fn valid(field: &str, entry: &Entry) -> Result<()> {
return Err(reject("unsupported reasoning summary"));
}
}
"reasoning_effort" => {
"reasoning_effort" | "summary_reasoning_effort" => {
if !matches!(
value.as_str(),
"none" | "minimal" | "low" | "medium" | "high" | "xhigh"
Expand Down Expand Up @@ -310,14 +330,14 @@ fn valid(field: &str, entry: &Entry) -> Result<()> {
return Err(reject("top_k must be -1 (disabled) or a positive integer"));
}
}
"context_window_tokens" | "context_output_reserve_tokens" => {
"context_window_tokens" | "context_output_reserve_tokens" | "context_target_tokens" => {
if value.parse::<usize>().ok().is_none_or(|v| v > 2_000_000) {
return Err(reject(
"token budget must be an integer between 0 and 2000000",
));
}
}
"watchdog_disable" | "context_compaction_enabled" => {
"watchdog_disable" | "context_compaction_enabled" | "responses_websocket" => {
if !matches!(value.as_str(), "0" | "1" | "false" | "true") {
return Err(reject("boolean must be 0/1 or false/true"));
}
Expand Down Expand Up @@ -360,6 +380,7 @@ fn valid(field: &str, entry: &Entry) -> Result<()> {
| "max_children_per_turn"
| "max_agent_depth"
| "max_output_tokens"
| "summary_max_output_tokens"
| "max_history_bytes"
| "max_output_bytes"
| "max_tool_calls"
Expand Down Expand Up @@ -509,9 +530,11 @@ fn load_mode(inputs: &ConfigInputs, management: bool) -> Result<ResolvedCoreConf
("limits.context_window_bytes", "524288"),
("limits.context_compaction_enabled", "true"),
("limits.context_window_tokens", "65536"),
("limits.context_target_tokens", "0"),
("limits.context_output_reserve_tokens", "8192"),
("limits.context_recent_bytes", "131072"),
("model.max_retries", "2"),
("model.responses_websocket", "false"),
("limits.max_completion_retries", "0"),
("limits.watchdog_disable", "false"),
("logging.filter", "info"),
Expand Down Expand Up @@ -734,6 +757,16 @@ fn load_mode(inputs: &ConfigInputs, management: bool) -> Result<ResolvedCoreConf
.get("tools.extensions_file")
.map(|v| PathBuf::from(&v.value)),
model: SelectedModelConfig {
summary_reasoning_effort: values
.get("model.summary_reasoning_effort")
.map(|e| e.value.clone()),
summary_max_output_tokens: values
.get("model.summary_max_output_tokens")
.map(|e| e.value.parse().unwrap()),
responses_websocket: matches!(
values["model.responses_websocket"].value.as_str(),
"true" | "1"
),
provider,
name: values["model.name"].value.clone(),
endpoint: values[&format!("{prefix}.endpoint")].value.clone(),
Expand Down Expand Up @@ -791,6 +824,10 @@ fn load_mode(inputs: &ConfigInputs, management: bool) -> Result<ResolvedCoreConf
values["limits.context_compaction_enabled"].value.as_str(),
"1" | "true"
),
context_target_tokens: values["limits.context_target_tokens"]
.value
.parse()
.unwrap(),
context_window_tokens: values["limits.context_window_tokens"]
.value
.parse()
Expand Down Expand Up @@ -821,6 +858,14 @@ fn load_mode(inputs: &ConfigInputs, management: bool) -> Result<ResolvedCoreConf
));
}
// Validate credentials here so callers cannot accidentally skip the check.
if result.model.responses_websocket && result.model.protocol != ModelProtocolConfig::Responses {
return Err(error(
ConfigErrorKind::InvalidValue,
"model.responses_websocket",
&result.sources["model.responses_websocket"],
"responses_websocket requires responses protocol",
));
}
if result.context_recent_bytes >= result.context_window_bytes {
return Err(error(
ConfigErrorKind::InvalidValue,
Expand Down Expand Up @@ -852,6 +897,20 @@ fn load_mode(inputs: &ConfigInputs, management: bool) -> Result<ResolvedCoreConf
}
}
}
if result.context_target_tokens > 0
&& (result.context_window_tokens == 0
|| result.context_target_tokens
>= result
.context_window_tokens
.saturating_sub(result.context_output_reserve_tokens))
{
return Err(error(
ConfigErrorKind::InvalidValue,
"limits.context_target_tokens",
&result.sources["limits.context_target_tokens"],
"compaction target must be below the input trigger",
));
}
if result.context_window_tokens > 0
&& result.context_output_reserve_tokens >= result.context_window_tokens
{
Expand Down
55 changes: 55 additions & 0 deletions core/config/tests/loading.rs
Original file line number Diff line number Diff line change
Expand Up @@ -761,3 +761,58 @@ fn removed_turn_timeout_requires_toml_and_env_migration() {
set(&mut i, "AREAL_HARNESS_TURN_TIMEOUT_SECONDS", "0");
assert_eq!(failure(&i).kind, ConfigErrorKind::UnknownField);
}

#[test]
fn responses_websocket_is_opt_in_and_rejects_chat_protocol() {
let temp = tempfile::tempdir().unwrap();
let mut i = inputs(temp.path());
assert!(!load_config(&i).unwrap().model.responses_websocket);
write(
&mut i,
"schema_version=1\n[model]\nresponses_websocket=true\n",
);
assert_eq!(failure(&i).kind, ConfigErrorKind::InvalidValue);
i.overrides.model_protocol = Some("responses".into());
assert!(load_config(&i).unwrap().model.responses_websocket);
set(&mut i, "AREAL_HARNESS_RESPONSES_WEBSOCKET", "false");
assert!(!load_config(&i).unwrap().model.responses_websocket);
}

#[test]
fn compaction_target_is_explicit_bounded_and_overridable() {
let temp = tempfile::tempdir().unwrap();
let mut i = inputs(temp.path());
assert_eq!(load_config(&i).unwrap().context_target_tokens, 0);
write(
&mut i,
"schema_version=1\n[limits]\ncontext_window_tokens=88000\ncontext_output_reserve_tokens=8192\ncontext_target_tokens=55000\n",
);
assert_eq!(load_config(&i).unwrap().context_target_tokens, 55000);
set(&mut i, "AREAL_HARNESS_CONTEXT_TARGET_TOKENS", "79808");
assert_eq!(failure(&i).kind, ConfigErrorKind::InvalidValue);
set(&mut i, "AREAL_HARNESS_CONTEXT_TARGET_TOKENS", "0");
assert_eq!(load_config(&i).unwrap().context_target_tokens, 0);
write(
&mut i,
"schema_version=1\n[limits]\ncontext_window_tokens=0\ncontext_target_tokens=1000\n",
);
i.env
.remove(std::ffi::OsStr::new("AREAL_HARNESS_CONTEXT_TARGET_TOKENS"));
assert_eq!(failure(&i).kind, ConfigErrorKind::InvalidValue);
}

#[test]
fn summary_options_do_not_change_solve_configuration() {
let temp = tempfile::tempdir().unwrap();
let mut i = inputs(temp.path());
write(
&mut i,
"schema_version=1\n[model]\nreasoning_effort='high'\nsummary_reasoning_effort='low'\nsummary_max_output_tokens=4096\n",
);
let c = load_config(&i).unwrap();
assert_eq!(c.model.reasoning_effort.as_deref(), Some("high"));
assert_eq!(c.model.summary_reasoning_effort.as_deref(), Some("low"));
assert_eq!(c.model.summary_max_output_tokens, Some(4096));
set(&mut i, "AREAL_HARNESS_SUMMARY_MAX_OUTPUT_TOKENS", "0");
assert_eq!(failure(&i).kind, ConfigErrorKind::InvalidValue);
}
3 changes: 3 additions & 0 deletions core/engine/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ sha2.workspace = true
thiserror.workspace = true
tempfile.workspace = true
tokio.workspace = true
tokio-tungstenite = { workspace = true, features = ["rustls-tls-native-roots"] }
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
rustls-native-certs = "0.8"
tokio-util.workspace = true
tracing.workspace = true
uuid.workspace = true
Expand Down
Loading
Loading