mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-11 10:01:30 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af2ac21b36 |
No files matched your search
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- **Creator skills:** Codewhale now ships DeepSeek Harness's four plugin-authoring skills (`cordis-plugin-development`, `editing-cordis-compositions`, `cordis-composition-reference`, `agent-experience`) unmodified, with their reference and template files. A bundled skill can now carry files: `load_skill` prints the skill's base directory and a short Codewhale host-notes block after DSH's text. A copy you edited is never overwritten on upgrade. Source-level only: the Creator plugin switch and agent entry that make these useful land in later slices.
|
||||
- **Creator skills:** Codewhale now ships DeepSeek Harness's four plugin-authoring skills (`cordis-plugin-development`, `editing-cordis-compositions`, `cordis-composition-reference`, `agent-experience`) bundled, unmodified, with their reference and template files and DeepSeek's MIT notice beside each. They stay hidden from the model until Creator mode. A bundled skill can now carry files; `load_skill` prints its base directory and a short Codewhale host-notes block, and a file you edited is never overwritten on upgrade.
|
||||
- **Website home page, rebuilt around one first screen:** the headline stays "The model can be anyone's. The agent should be yours.", centred, with one plain sentence under it that says what Codewhale is (an open-source coding agent for your terminal, written in Rust, working with any model and able to split a task across several agents), the install command, and one wide picture of a real terminal session that runs off the bottom of the first screen. A row of facts under the picture takes its counts of provider routes and models from the published release, and the page says several agents share a task only while the release has it. The route of where the product is going moves lower and smaller, and the home page names no other tools. The new text in the 17 non-English languages is machine-drafted and is labelled as such in the footer; the Arabic page now reads the opening sentence right to left.
|
||||
- **Conversation modes (Engine):** `GET /v1/modes` lists two built-in modes, `standard` (default) and `creator`, each with `id`, `name`, `description`, `source` and `available` (plus `reason` when not; Creator is listed unavailable, "not ready in this build", until it loads its own tools, and then also when no Node.js or Bun plugin runtime is found). A thread's mode is `agent_preset`, set on `POST /v1/threads` or by `PATCH /v1/threads/{id}` until the thread has a turn, saved on the thread record and the terminal session file and shown in `GET /v1/threads/summary`; records without it load as `standard`. An unknown or unavailable id, a change after the first turn, or a mode together with another vendor's agent is refused with `error.code` `unknown_mode`, `mode_unavailable`, `mode_locked` or `mode_not_supported_with_executor`. Source only: Creator loads nothing extra yet and no menu uses it.
|
||||
- **Website home page:** the page is reorganized around what a visitor does: a dated OrcaRouter offer strip with a visible referral tag, one proof strip (release, stars, contributors), a models section with the newest first-party models, one capabilities section that includes Computer Use, an "Everything is a plugin" teaser and a community panel whose live GitHub numbers fill in from the browser and hide when unavailable. Unused home copy was removed from all 18 site languages.
|
||||
- **Website home page, rebuilt around one first screen:** the headline stays "The model can be anyone's. The agent should be yours.", centred, with one plain sentence under it that says what Codewhale is (an open-source coding agent for your terminal, written in Rust, working with any model and able to split a task across several agents), the install command, and one wide picture of a real terminal session that runs off the bottom of the first screen. A row of facts under the picture takes its counts of provider routes and models from the published release, and the page says several agents share a task only while the release has it. The route of where the product is going moves lower and smaller, and the home page names no other tools. This replaces the earlier reorganization of the home page, so the dated OrcaRouter offer strip and the community panel whose GitHub numbers filled in from the browser are no longer on it; the models and contribute pages keep their own content. The new text in the 17 non-English languages is machine-drafted and is labelled as such in the footer; the Arabic page now reads the opening sentence right to left.
|
||||
- **Website home page, rebuilt around one first screen:** the headline stays "The model can be anyone's. The agent should be yours.", centred, with one plain sentence under it that says what Codewhale is (an open-source coding agent for your terminal, written in Rust, working with any model and able to split a task across several agents), the install command, and one wide picture of a real terminal session that runs off the bottom of the first screen. A row of facts under the picture takes its counts of provider routes and models from the published release, and the facts row lists many agents on one task only while the release has it. The route of where the product is going moves lower and smaller, and the home page names no other coding agents. This replaces the earlier reorganization of the home page, so the community panel whose GitHub numbers filled in from the browser is no longer on it, and the dated OrcaRouter offer strip, English only, with its referral disclosure, stays below the first screen until it ends; the models and contribute pages keep their own content. The new text in the 17 non-English languages is machine-drafted and is labelled as such in the footer; the Arabic page now reads the opening sentence right to left.
|
||||
|
||||
@@ -1323,6 +1323,7 @@ struct ThreadSummary {
|
||||
preview: String,
|
||||
model: String,
|
||||
mode: String,
|
||||
agent_preset: String,
|
||||
workspace: PathBuf,
|
||||
branch: Option<String>,
|
||||
head: Option<String>,
|
||||
@@ -3314,6 +3315,7 @@ pub fn build_router(state: RuntimeApiState) -> Router {
|
||||
.route("/v1/tasks/{id}", get(get_task))
|
||||
.route("/v1/tasks/{id}/cancel", post(cancel_task))
|
||||
.route("/v1/skills", get(list_skills))
|
||||
.route("/v1/modes", get(list_modes))
|
||||
.route("/v1/commands", get(list_commands))
|
||||
.route("/v1/threads/{id}/commands", post(run_thread_command))
|
||||
.route(
|
||||
@@ -3935,7 +3937,7 @@ async fn create_thread(
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
if let Some(refusal) = external_agent_refusal(&e) {
|
||||
if let Some(refusal) = external_agent_refusal(&e).or_else(|| mode_refusal(&e)) {
|
||||
refusal
|
||||
} else if e
|
||||
.is::<codewhale_engine::runtime_threads::manager::RuntimeThreadCreateConflict>()
|
||||
@@ -4184,6 +4186,10 @@ async fn list_threads_summary(
|
||||
preview,
|
||||
model: thread.model,
|
||||
mode: thread.mode,
|
||||
agent_preset: codewhale_engine::conversation_modes::effective_id(
|
||||
thread.agent_preset.as_deref(),
|
||||
)
|
||||
.to_string(),
|
||||
branch: workspace_git.and_then(|git| git.branch.clone()),
|
||||
head: workspace_git.and_then(|git| git.head.clone()),
|
||||
dirty: workspace_git.is_some_and(|git| git.dirty),
|
||||
@@ -6248,6 +6254,17 @@ async fn list_hooks(
|
||||
}))
|
||||
}
|
||||
|
||||
async fn list_modes(
|
||||
State(state): State<RuntimeApiState>,
|
||||
) -> Json<codewhale_engine::conversation_modes::ConversationModeTable> {
|
||||
Json(
|
||||
codewhale_engine::conversation_modes::current_table(
|
||||
state.runtime_threads.extension_host_options(),
|
||||
)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
async fn list_skills(
|
||||
State(state): State<RuntimeApiState>,
|
||||
) -> Result<Json<SkillsResponse>, ApiError> {
|
||||
@@ -14829,6 +14846,20 @@ fn map_automation_err(err: anyhow::Error) -> ApiError {
|
||||
}
|
||||
}
|
||||
|
||||
fn mode_refusal(err: &anyhow::Error) -> Option<ApiError> {
|
||||
use codewhale_engine::conversation_modes::ModeRefusal;
|
||||
let error = err.downcast_ref::<ModeRefusal>()?;
|
||||
Some(ApiError {
|
||||
status: match error.code {
|
||||
ModeRefusal::MODE_UNAVAILABLE => StatusCode::FAILED_DEPENDENCY,
|
||||
ModeRefusal::MODE_LOCKED => StatusCode::CONFLICT,
|
||||
_ => StatusCode::BAD_REQUEST,
|
||||
},
|
||||
message: error.message.clone(),
|
||||
code: Some(error.code),
|
||||
})
|
||||
}
|
||||
|
||||
fn external_agent_refusal(err: &anyhow::Error) -> Option<ApiError> {
|
||||
use codewhale_engine::fleet::agents::ExecutorUnavailable;
|
||||
let error = err.downcast_ref::<ExecutorUnavailable>()?;
|
||||
@@ -14844,7 +14875,7 @@ fn external_agent_refusal(err: &anyhow::Error) -> Option<ApiError> {
|
||||
}
|
||||
|
||||
fn map_thread_err(err: anyhow::Error) -> ApiError {
|
||||
if let Some(refusal) = external_agent_refusal(&err) {
|
||||
if let Some(refusal) = external_agent_refusal(&err).or_else(|| mode_refusal(&err)) {
|
||||
return refusal;
|
||||
}
|
||||
if let Some(error) = err.downcast_ref::<codewhale_engine::runtime_threads::manager::RuntimeTurnOperationLookupError>() {
|
||||
|
||||
@@ -18,6 +18,7 @@ use uuid::Uuid;
|
||||
mod command_catalog;
|
||||
#[cfg(unix)]
|
||||
mod control_socket_selection;
|
||||
mod conversation_modes;
|
||||
#[cfg(unix)]
|
||||
mod external_agent_options;
|
||||
#[cfg(unix)]
|
||||
@@ -423,6 +424,7 @@ fn saved_session_with_blocks(blocks: Vec<codewhale_models::ContentBlock>) -> Sav
|
||||
composer_color: None,
|
||||
last_auto_route: None,
|
||||
turn_outcomes: Vec::new(),
|
||||
agent_preset: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -563,6 +565,7 @@ fn messages_from_thread_detail_batches_tool_results() {
|
||||
last_read: None,
|
||||
executor: None,
|
||||
environment: None,
|
||||
agent_preset: None,
|
||||
};
|
||||
let turn = TurnRecord {
|
||||
source_usage_origin_turn_id: None,
|
||||
@@ -795,6 +798,7 @@ fn legacy_exact_thread_export_normalizes_provider_kind_and_id() {
|
||||
last_read: None,
|
||||
executor: None,
|
||||
environment: None,
|
||||
agent_preset: None,
|
||||
},
|
||||
turns: Vec::new(),
|
||||
items: Vec::new(),
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
use super::*;
|
||||
use codewhale_engine::conversation_modes::CreatorAvailabilityGuard;
|
||||
|
||||
#[tokio::test]
|
||||
async fn modes_route_lists_the_table_and_threads_carry_and_refuse_modes() -> Result<()> {
|
||||
let _unavailable = CreatorAvailabilityGuard::unforced();
|
||||
let Some((addr, _threads, handle)) = spawn_test_server().await? else {
|
||||
return Ok(());
|
||||
};
|
||||
let client = codewhale_release::tls::reqwest_client();
|
||||
let base = format!("http://{addr}");
|
||||
|
||||
let table: Value = client
|
||||
.get(format!("{base}/v1/modes"))
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?;
|
||||
assert_eq!(table["default"], "standard");
|
||||
assert_eq!(table["modes"][0]["id"], "standard");
|
||||
assert_eq!(table["modes"][0]["source"], "builtin");
|
||||
assert_eq!(table["modes"][0]["available"], true);
|
||||
assert!(table["modes"][0].get("reason").is_none());
|
||||
assert_eq!(table["modes"][1]["id"], "creator");
|
||||
assert_eq!(table["modes"][1]["available"], false);
|
||||
assert_eq!(
|
||||
table["modes"][1]["reason"],
|
||||
codewhale_engine::conversation_modes::CREATOR_NOT_READY_REASON
|
||||
);
|
||||
|
||||
let unavailable = client
|
||||
.post(format!("{base}/v1/threads"))
|
||||
.json(&json!({"agent_preset": "creator"}))
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(unavailable.status(), reqwest::StatusCode::FAILED_DEPENDENCY);
|
||||
let body: Value = unavailable.json().await?;
|
||||
assert_eq!(body["error"]["code"], "mode_unavailable");
|
||||
|
||||
let unknown = client
|
||||
.post(format!("{base}/v1/threads"))
|
||||
.json(&json!({"agent_preset": "minimal"}))
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(unknown.status(), reqwest::StatusCode::BAD_REQUEST);
|
||||
let body: Value = unknown.json().await?;
|
||||
assert_eq!(body["error"]["code"], "unknown_mode");
|
||||
|
||||
let created: Value = client
|
||||
.post(format!("{base}/v1/threads"))
|
||||
.json(&json!({"agent_preset": "standard"}))
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?;
|
||||
let id = created["id"].as_str().expect("thread id").to_string();
|
||||
let summary: Value = client
|
||||
.get(format!("{base}/v1/threads/summary"))
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?;
|
||||
let row = summary
|
||||
.as_array()
|
||||
.and_then(|rows| rows.iter().find(|row| row["id"] == id.as_str()))
|
||||
.expect("summary row");
|
||||
assert_eq!(row["agent_preset"], "standard");
|
||||
|
||||
let patched = client
|
||||
.patch(format!("{base}/v1/threads/{id}"))
|
||||
.json(&json!({"agent_preset": "creator"}))
|
||||
.send()
|
||||
.await?;
|
||||
assert_eq!(patched.status(), reqwest::StatusCode::FAILED_DEPENDENCY);
|
||||
|
||||
handle.abort();
|
||||
let _ = handle.await;
|
||||
Ok(())
|
||||
}
|
||||
@@ -559,6 +559,7 @@ impl RuntimeChatRelayHost {
|
||||
parent_thread_id: None,
|
||||
executor: None,
|
||||
environment: None,
|
||||
agent_preset: None,
|
||||
operation_key: None,
|
||||
model: Some(command.model.clone()),
|
||||
model_provider: Some(command.model_provider.clone()),
|
||||
@@ -721,6 +722,7 @@ impl RuntimeChatRelayHost {
|
||||
parent_thread_id: None,
|
||||
executor: None,
|
||||
environment: None,
|
||||
agent_preset: None,
|
||||
dynamic_tools: Vec::new(),
|
||||
environments: Vec::new(),
|
||||
})
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
use std::sync::Mutex;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::extension_host::ExtensionHostOptions;
|
||||
|
||||
pub const STANDARD: &str = "standard";
|
||||
pub const CREATOR: &str = "creator";
|
||||
pub const SOURCE_BUILTIN: &str = "builtin";
|
||||
|
||||
pub const CREATOR_LOADS_ITS_OWN_TOOLS: bool = false;
|
||||
pub const CREATOR_NOT_READY_REASON: &str = "Creator mode is not ready in this build.";
|
||||
|
||||
const AVAILABILITY_TTL: Duration = Duration::from_secs(30);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
#[error("{message}")]
|
||||
pub struct ModeRefusal {
|
||||
pub code: &'static str,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl ModeRefusal {
|
||||
pub const UNKNOWN_MODE: &'static str = "unknown_mode";
|
||||
pub const MODE_UNAVAILABLE: &'static str = "mode_unavailable";
|
||||
pub const MODE_LOCKED: &'static str = "mode_locked";
|
||||
pub const MODE_NOT_SUPPORTED_WITH_EXECUTOR: &'static str = "mode_not_supported_with_executor";
|
||||
|
||||
pub fn new(code: &'static str, message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
code,
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn locked() -> Self {
|
||||
Self::new(
|
||||
Self::MODE_LOCKED,
|
||||
"The conversation mode is fixed once the conversation has its first turn; start a new conversation to use another mode.",
|
||||
)
|
||||
}
|
||||
|
||||
pub fn with_executor() -> Self {
|
||||
Self::new(
|
||||
Self::MODE_NOT_SUPPORTED_WITH_EXECUTOR,
|
||||
"A session with another agent uses that agent's own tools and instructions; conversation modes are unavailable.",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||
pub struct ConversationMode {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub source: &'static str,
|
||||
pub available: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||
pub struct ConversationModeTable {
|
||||
pub default: &'static str,
|
||||
pub modes: Vec<ConversationMode>,
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn effective_id(stored: Option<&str>) -> &str {
|
||||
stored.unwrap_or(STANDARD)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn stored_form(id: &str) -> Option<String> {
|
||||
(id != STANDARD).then(|| id.to_string())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn table(creator_unavailable: Option<String>) -> ConversationModeTable {
|
||||
ConversationModeTable {
|
||||
default: STANDARD,
|
||||
modes: vec![
|
||||
ConversationMode {
|
||||
id: STANDARD.to_string(),
|
||||
name: "Standard".to_string(),
|
||||
description: "The everyday Codewhale agent with its normal tools and skills."
|
||||
.to_string(),
|
||||
source: SOURCE_BUILTIN,
|
||||
available: true,
|
||||
reason: None,
|
||||
},
|
||||
ConversationMode {
|
||||
id: CREATOR.to_string(),
|
||||
name: "Creator".to_string(),
|
||||
description:
|
||||
"Build, inspect and test Codewhale plugins in this conversation, with the plugin authoring tools and guides loaded."
|
||||
.to_string(),
|
||||
source: SOURCE_BUILTIN,
|
||||
available: creator_unavailable.is_none(),
|
||||
reason: creator_unavailable,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
type Cached = Option<(Instant, Option<String>)>;
|
||||
|
||||
static CREATOR_PROBE: Mutex<Cached> = Mutex::new(None);
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
static CREATOR_PROBE_OVERRIDE: Mutex<Option<Option<String>>> = Mutex::new(None);
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
static CREATOR_PROBE_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub struct CreatorAvailabilityGuard {
|
||||
_serial: std::sync::MutexGuard<'static, ()>,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl CreatorAvailabilityGuard {
|
||||
pub fn set(unavailable_reason: Option<&str>) -> Self {
|
||||
Self::hold(Some(unavailable_reason.map(str::to_string)))
|
||||
}
|
||||
|
||||
pub fn unforced() -> Self {
|
||||
Self::hold(None)
|
||||
}
|
||||
|
||||
fn hold(forced: Option<Option<String>>) -> Self {
|
||||
let serial = CREATOR_PROBE_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
*CREATOR_PROBE_OVERRIDE
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = forced;
|
||||
Self { _serial: serial }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl Drop for CreatorAvailabilityGuard {
|
||||
fn drop(&mut self) {
|
||||
*CREATOR_PROBE_OVERRIDE
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn probe_host_runtime(options: &ExtensionHostOptions) -> Option<String> {
|
||||
let resolution = codewhale_dependencies::resolve_extension_host_runtime(
|
||||
options.runtime,
|
||||
options.node_override.as_deref(),
|
||||
options.bun_override.as_deref(),
|
||||
crate::extension_host::bundle_sha256(),
|
||||
);
|
||||
if resolution.selected.is_some() {
|
||||
None
|
||||
} else {
|
||||
Some(format!(
|
||||
"Creator mode needs a plugin runtime (Node.js or Bun), and none was found. {}",
|
||||
resolution.failure()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
async fn creator_unavailable_reason(options: ExtensionHostOptions) -> Option<String> {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
if let Some(forced) = CREATOR_PROBE_OVERRIDE
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.clone()
|
||||
{
|
||||
return forced;
|
||||
}
|
||||
if !CREATOR_LOADS_ITS_OWN_TOOLS {
|
||||
return Some(CREATOR_NOT_READY_REASON.to_string());
|
||||
}
|
||||
if let Some((at, reason)) = CREATOR_PROBE.lock().expect("probe cache").clone()
|
||||
&& at.elapsed() < AVAILABILITY_TTL
|
||||
{
|
||||
return reason;
|
||||
}
|
||||
let reason = tokio::task::spawn_blocking(move || probe_host_runtime(&options))
|
||||
.await
|
||||
.unwrap_or_else(|_| Some("Creator mode could not check for a plugin runtime.".to_string()));
|
||||
*CREATOR_PROBE.lock().expect("probe cache") = Some((Instant::now(), reason.clone()));
|
||||
reason
|
||||
}
|
||||
|
||||
pub async fn current_table(options: ExtensionHostOptions) -> ConversationModeTable {
|
||||
table(creator_unavailable_reason(options).await)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ModeSelection {
|
||||
pub id: String,
|
||||
pub unavailable: Option<String>,
|
||||
}
|
||||
|
||||
impl ModeSelection {
|
||||
pub fn require_available(&self) -> Result<(), ModeRefusal> {
|
||||
match &self.unavailable {
|
||||
Some(reason) => Err(ModeRefusal::new(
|
||||
ModeRefusal::MODE_UNAVAILABLE,
|
||||
reason.clone(),
|
||||
)),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn select(id: &str, options: ExtensionHostOptions) -> Result<ModeSelection, ModeRefusal> {
|
||||
let table = current_table(options).await;
|
||||
match table.modes.into_iter().find(|mode| mode.id == id) {
|
||||
Some(mode) => Ok(ModeSelection {
|
||||
id: mode.id,
|
||||
unavailable: if mode.available {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
mode.reason
|
||||
.unwrap_or_else(|| "This mode is unavailable.".to_string()),
|
||||
)
|
||||
},
|
||||
}),
|
||||
None => Err(ModeRefusal::new(
|
||||
ModeRefusal::UNKNOWN_MODE,
|
||||
format!("`{id}` is not a conversation mode; GET /v1/modes lists them."),
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ pub mod codex_model_cache;
|
||||
pub mod command_catalog;
|
||||
pub mod compaction;
|
||||
pub mod compaction_archive;
|
||||
pub mod conversation_modes;
|
||||
pub mod core;
|
||||
pub mod cost_status;
|
||||
pub mod diagnostics_messages;
|
||||
|
||||
@@ -430,6 +430,8 @@ pub struct ThreadRecord {
|
||||
pub executor: Option<ExecutorRecord>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub environment: Option<ThreadEnvironment>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub agent_preset: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
|
||||
@@ -1465,6 +1465,8 @@ pub struct CreateThreadRequest {
|
||||
pub executor: Option<CreateThreadExecutor>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub environment: Option<ThreadEnvironment>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub agent_preset: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
@@ -1514,6 +1516,8 @@ pub struct UpdateThreadRequest {
|
||||
/// route name in `model_provider`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub model_provider_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub agent_preset: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -3966,6 +3970,12 @@ impl RuntimeThreadManager {
|
||||
self.config.read()
|
||||
}
|
||||
|
||||
pub fn extension_host_options(&self) -> crate::extension_host::ExtensionHostOptions {
|
||||
crate::extension_host::ExtensionHostOptions::from_config(
|
||||
self.read_config().extension_host.as_ref(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn resolved_route_for_thread(
|
||||
&self,
|
||||
config: &Config,
|
||||
@@ -7171,6 +7181,18 @@ impl RuntimeThreadManager {
|
||||
if let Some(environment) = req.environment.as_ref() {
|
||||
return Err(environment.refusal().into());
|
||||
}
|
||||
if req.executor.is_some() && req.agent_preset.is_some() {
|
||||
return Err(crate::conversation_modes::ModeRefusal::with_executor().into());
|
||||
}
|
||||
let agent_preset = match req.agent_preset.as_deref() {
|
||||
Some(id) => {
|
||||
let selection =
|
||||
crate::conversation_modes::select(id, self.extension_host_options()).await?;
|
||||
selection.require_available()?;
|
||||
crate::conversation_modes::stored_form(&selection.id)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let executor = match req.executor.as_ref() {
|
||||
Some(requested) => {
|
||||
anyhow::ensure!(
|
||||
@@ -7311,6 +7333,7 @@ impl RuntimeThreadManager {
|
||||
last_read: None,
|
||||
executor,
|
||||
environment: None,
|
||||
agent_preset,
|
||||
};
|
||||
if let Some(operation_key) = req.operation_key {
|
||||
let key_fingerprint = runtime_turn_operation_key_fingerprint(
|
||||
@@ -7318,28 +7341,32 @@ impl RuntimeThreadManager {
|
||||
":thread-create:",
|
||||
&operation_key,
|
||||
)?;
|
||||
let mut fingerprint_body = json!({
|
||||
"version": 1,
|
||||
"model": thread.model,
|
||||
"model_provider": thread.model_provider,
|
||||
"model_provider_id": thread.model_provider_id,
|
||||
"reasoning_effort": thread.reasoning_effort,
|
||||
"allowed_tools": thread.allowed_tools,
|
||||
"add_dirs": thread.add_dirs,
|
||||
"workspace": thread.workspace,
|
||||
"mode": thread.mode,
|
||||
"permission_posture": thread.permission_posture,
|
||||
"allow_shell": thread.allow_shell,
|
||||
"trust_mode": thread.trust_mode,
|
||||
"auto_approve": thread.auto_approve,
|
||||
"archived": thread.archived,
|
||||
"system_prompt": thread.system_prompt,
|
||||
"task_id": thread.task_id,
|
||||
"dynamic_tools": req.dynamic_tools,
|
||||
"environments": req.environments,
|
||||
"executor": thread.executor,
|
||||
});
|
||||
if let Some(preset) = thread.agent_preset.as_ref() {
|
||||
fingerprint_body["agent_preset"] = json!(preset);
|
||||
}
|
||||
let request_fingerprint =
|
||||
codewhale_runtime::hashing::sha256_hex(serde_json::to_vec(&json!({
|
||||
"version": 1,
|
||||
"model": thread.model,
|
||||
"model_provider": thread.model_provider,
|
||||
"model_provider_id": thread.model_provider_id,
|
||||
"reasoning_effort": thread.reasoning_effort,
|
||||
"allowed_tools": thread.allowed_tools,
|
||||
"add_dirs": thread.add_dirs,
|
||||
"workspace": thread.workspace,
|
||||
"mode": thread.mode,
|
||||
"permission_posture": thread.permission_posture,
|
||||
"allow_shell": thread.allow_shell,
|
||||
"trust_mode": thread.trust_mode,
|
||||
"auto_approve": thread.auto_approve,
|
||||
"archived": thread.archived,
|
||||
"system_prompt": thread.system_prompt,
|
||||
"task_id": thread.task_id,
|
||||
"dynamic_tools": req.dynamic_tools,
|
||||
"environments": req.environments,
|
||||
"executor": thread.executor,
|
||||
}))?);
|
||||
codewhale_runtime::hashing::sha256_hex(serde_json::to_vec(&fingerprint_body)?);
|
||||
let _emit_order = self.event_emit.lock().await;
|
||||
let store = self.store.clone();
|
||||
let (thread, event) = tokio::task::spawn_blocking(move || {
|
||||
@@ -8400,9 +8427,26 @@ impl RuntimeThreadManager {
|
||||
&& req.workspace.is_none()
|
||||
&& req.model_provider.is_none()
|
||||
&& req.model_provider_id.is_none()
|
||||
&& req.agent_preset.is_none()
|
||||
{
|
||||
bail!("At least one thread field is required");
|
||||
}
|
||||
let preset_selection = match req.agent_preset.as_deref() {
|
||||
Some(preset_id) => {
|
||||
if self
|
||||
.store
|
||||
.load_thread(id)
|
||||
.is_ok_and(|thread| thread.executor.is_some())
|
||||
{
|
||||
return Err(crate::conversation_modes::ModeRefusal::with_executor().into());
|
||||
}
|
||||
Some(
|
||||
crate::conversation_modes::select(preset_id, self.extension_host_options())
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
if (req.model.is_some()
|
||||
|| req.model_provider.is_some()
|
||||
|| req.model_provider_id.is_some()
|
||||
@@ -8571,6 +8615,24 @@ impl RuntimeThreadManager {
|
||||
thread.trust_mode = trust_mode;
|
||||
changes.insert("trust_mode".to_string(), json!(trust_mode));
|
||||
}
|
||||
if let Some(selection) = preset_selection.as_ref() {
|
||||
let current =
|
||||
crate::conversation_modes::effective_id(thread.agent_preset.as_deref());
|
||||
if selection.id != current {
|
||||
if thread.latest_turn_id.is_some()
|
||||
|| active
|
||||
.engines
|
||||
.get(id)
|
||||
.and_then(|state| state.active_turn.as_ref())
|
||||
.is_some()
|
||||
{
|
||||
return Err(crate::conversation_modes::ModeRefusal::locked().into());
|
||||
}
|
||||
selection.require_available()?;
|
||||
thread.agent_preset = crate::conversation_modes::stored_form(&selection.id);
|
||||
changes.insert("agent_preset".to_string(), json!(selection.id));
|
||||
}
|
||||
}
|
||||
let requested_model = match provider_switch {
|
||||
Some((model_provider, model_provider_id, model)) => {
|
||||
if thread.model_provider.as_deref() != Some(model_provider.as_str())
|
||||
@@ -8639,7 +8701,9 @@ impl RuntimeThreadManager {
|
||||
}
|
||||
|
||||
let workspace_changed = changes.contains_key("workspace");
|
||||
let rebuild_engine = workspace_changed || changes.contains_key("system_prompt");
|
||||
let rebuild_engine = workspace_changed
|
||||
|| changes.contains_key("system_prompt")
|
||||
|| changes.contains_key("agent_preset");
|
||||
if rebuild_engine
|
||||
&& active
|
||||
.engines
|
||||
|
||||
@@ -2668,6 +2668,7 @@ fn sample_thread(thread_id: &str) -> ThreadRecord {
|
||||
last_read: None,
|
||||
executor: None,
|
||||
environment: None,
|
||||
agent_preset: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19967,6 +19968,7 @@ fn opening_manager_recovers_stale_queued_and_in_progress_work() -> Result<()> {
|
||||
last_read: None,
|
||||
executor: None,
|
||||
environment: None,
|
||||
agent_preset: None,
|
||||
};
|
||||
manager.store.save_thread(&thread)?;
|
||||
|
||||
@@ -21758,6 +21760,7 @@ async fn report_store_failure_names_the_item_file_without_terminalizing() -> Res
|
||||
Ok(())
|
||||
}
|
||||
|
||||
mod conversation_modes;
|
||||
mod external_agent;
|
||||
mod task_ownership;
|
||||
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
use super::*;
|
||||
use crate::conversation_modes::{CREATOR, CreatorAvailabilityGuard, ModeRefusal, STANDARD};
|
||||
|
||||
fn refusal_code(error: &anyhow::Error) -> &'static str {
|
||||
error
|
||||
.downcast_ref::<ModeRefusal>()
|
||||
.expect("typed refusal")
|
||||
.code
|
||||
}
|
||||
|
||||
fn create_request(preset: Option<&str>) -> CreateThreadRequest {
|
||||
CreateThreadRequest {
|
||||
agent_preset: preset.map(str::to_string),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_record_without_agent_preset_loads_as_standard() -> Result<()> {
|
||||
let thread = sample_thread("thr_modes_legacy");
|
||||
let mut raw = serde_json::to_value(&thread)?;
|
||||
assert!(raw.get("agent_preset").is_none());
|
||||
let legacy: ThreadRecord = serde_json::from_value(raw.clone())?;
|
||||
assert_eq!(legacy.agent_preset, None);
|
||||
assert_eq!(
|
||||
crate::conversation_modes::effective_id(legacy.agent_preset.as_deref()),
|
||||
STANDARD
|
||||
);
|
||||
raw["agent_preset"] = json!(CREATOR);
|
||||
let creator: ThreadRecord = serde_json::from_value(raw)?;
|
||||
assert_eq!(creator.agent_preset.as_deref(), Some(CREATOR));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mode_is_set_at_creation_and_refused_when_unknown_or_unavailable() -> Result<()> {
|
||||
let tmp = tempfile::tempdir()?;
|
||||
let manager = test_manager(tmp.path().to_path_buf())?;
|
||||
{
|
||||
let _available = CreatorAvailabilityGuard::set(None);
|
||||
let created = manager.create_thread(create_request(Some(CREATOR))).await?;
|
||||
assert_eq!(created.agent_preset.as_deref(), Some(CREATOR));
|
||||
let standard = manager
|
||||
.create_thread(create_request(Some(STANDARD)))
|
||||
.await?;
|
||||
assert_eq!(standard.agent_preset, None);
|
||||
let unknown = manager
|
||||
.create_thread(create_request(Some("minimal")))
|
||||
.await
|
||||
.expect_err("unknown id");
|
||||
assert_eq!(refusal_code(&unknown), ModeRefusal::UNKNOWN_MODE);
|
||||
}
|
||||
let _missing = CreatorAvailabilityGuard::set(Some("no runtime"));
|
||||
let unavailable = manager
|
||||
.create_thread(create_request(Some(CREATOR)))
|
||||
.await
|
||||
.expect_err("unavailable");
|
||||
assert_eq!(refusal_code(&unavailable), ModeRefusal::MODE_UNAVAILABLE);
|
||||
assert_eq!(unavailable.to_string(), "no runtime");
|
||||
let table = crate::conversation_modes::current_table(manager.extension_host_options()).await;
|
||||
assert!(table.modes[0].available && table.modes[0].reason.is_none());
|
||||
assert!(!table.modes[1].available);
|
||||
assert_eq!(table.modes[1].reason.as_deref(), Some("no runtime"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn mode_changes_only_before_the_first_turn_and_never_with_an_executor() -> Result<()> {
|
||||
let tmp = tempfile::tempdir()?;
|
||||
let manager = test_manager(tmp.path().to_path_buf())?;
|
||||
let _available = CreatorAvailabilityGuard::set(None);
|
||||
let thread = manager.create_thread(create_request(None)).await?;
|
||||
let patch = |preset: &str| UpdateThreadRequest {
|
||||
agent_preset: Some(preset.to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let updated = manager.update_thread(&thread.id, patch(CREATOR)).await?;
|
||||
assert_eq!(updated.agent_preset.as_deref(), Some(CREATOR));
|
||||
let unknown = manager
|
||||
.update_thread(&thread.id, patch("nope"))
|
||||
.await
|
||||
.expect_err("unknown id");
|
||||
assert_eq!(refusal_code(&unknown), ModeRefusal::UNKNOWN_MODE);
|
||||
|
||||
let mut with_turn = manager.store.load_thread(&thread.id)?;
|
||||
with_turn.latest_turn_id = Some("turn_first".to_string());
|
||||
manager.store.save_thread(&with_turn)?;
|
||||
let locked = manager
|
||||
.update_thread(&thread.id, patch(STANDARD))
|
||||
.await
|
||||
.expect_err("locked");
|
||||
assert_eq!(refusal_code(&locked), ModeRefusal::MODE_LOCKED);
|
||||
manager.update_thread(&thread.id, patch(CREATOR)).await?;
|
||||
|
||||
let with_executor = CreateThreadRequest {
|
||||
agent_preset: Some(CREATOR.to_string()),
|
||||
executor: Some(CreateThreadExecutor {
|
||||
kind: "acp".to_string(),
|
||||
agent_id: "opencode".to_string(),
|
||||
config: CreateThreadExecutorConfig::default(),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let refused = manager
|
||||
.create_thread(with_executor)
|
||||
.await
|
||||
.expect_err("executor");
|
||||
assert_eq!(
|
||||
refusal_code(&refused),
|
||||
ModeRefusal::MODE_NOT_SUPPORTED_WITH_EXECUTOR
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn creator_is_listed_unavailable_until_it_loads_its_tools() -> Result<()> {
|
||||
let _real = CreatorAvailabilityGuard::unforced();
|
||||
let tmp = tempfile::tempdir()?;
|
||||
let manager = test_manager(tmp.path().to_path_buf())?;
|
||||
let table = crate::conversation_modes::current_table(manager.extension_host_options()).await;
|
||||
assert!(table.modes[0].available);
|
||||
assert!(!table.modes[1].available);
|
||||
assert_eq!(
|
||||
table.modes[1].reason.as_deref(),
|
||||
Some(crate::conversation_modes::CREATOR_NOT_READY_REASON)
|
||||
);
|
||||
let refused = manager
|
||||
.create_thread(create_request(Some(CREATOR)))
|
||||
.await
|
||||
.expect_err("not ready");
|
||||
assert_eq!(refusal_code(&refused), ModeRefusal::MODE_UNAVAILABLE);
|
||||
Ok(())
|
||||
}
|
||||
@@ -2090,6 +2090,8 @@ pub struct SavedSession {
|
||||
/// record resume, export, and the Runtime API read back.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub turn_outcomes: Vec<SavedTurnOutcome>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub agent_preset: Option<String>,
|
||||
}
|
||||
impl SavedSession {
|
||||
pub fn capture_publication(
|
||||
@@ -2302,6 +2304,7 @@ impl SavedSession {
|
||||
composer_color: None,
|
||||
last_auto_route: None,
|
||||
turn_outcomes: Vec::new(),
|
||||
agent_preset: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -7079,6 +7082,7 @@ fn create_saved_session_inner(
|
||||
composer_color: None,
|
||||
last_auto_route: None,
|
||||
turn_outcomes: Vec::new(),
|
||||
agent_preset: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
composer_color: None,
|
||||
last_auto_route: None,
|
||||
turn_outcomes: Vec::new(),
|
||||
agent_preset: None,
|
||||
};
|
||||
manager.save_session(&session).expect("save");
|
||||
}
|
||||
@@ -96,6 +97,7 @@
|
||||
composer_color: None,
|
||||
last_auto_route: None,
|
||||
turn_outcomes: Vec::new(),
|
||||
agent_preset: None,
|
||||
};
|
||||
manager.save_session(&session).expect("save empty");
|
||||
}
|
||||
|
||||
@@ -346,3 +346,19 @@
|
||||
primary.join("sessions")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn saved_session_without_agent_preset_loads_and_round_trips_the_mode() {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
let manager = SessionManager::new(tmp.path().join("sessions")).expect("new");
|
||||
let session = create_saved_session(&[], "test-model", tmp.path(), 0, None);
|
||||
let mut raw = serde_json::to_value(&session).expect("serialize");
|
||||
assert!(raw.get("agent_preset").is_none());
|
||||
let legacy: SavedSession = serde_json::from_value(raw.clone()).expect("legacy load");
|
||||
assert_eq!(legacy.agent_preset, None);
|
||||
raw["agent_preset"] = serde_json::json!("creator");
|
||||
let saved: SavedSession = serde_json::from_value(raw).expect("mode load");
|
||||
manager.save_session(&saved).expect("save");
|
||||
let loaded = manager.load_session(&saved.metadata.id).expect("load");
|
||||
assert_eq!(loaded.agent_preset.as_deref(), Some("creator"));
|
||||
}
|
||||
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- **Creator skills:** Codewhale now ships DeepSeek Harness's four plugin-authoring skills (`cordis-plugin-development`, `editing-cordis-compositions`, `cordis-composition-reference`, `agent-experience`) unmodified, with their reference and template files. A bundled skill can now carry files: `load_skill` prints the skill's base directory and a short Codewhale host-notes block after DSH's text. A copy you edited is never overwritten on upgrade. Source-level only: the Creator plugin switch and agent entry that make these useful land in later slices.
|
||||
- **Creator skills:** Codewhale now ships DeepSeek Harness's four plugin-authoring skills (`cordis-plugin-development`, `editing-cordis-compositions`, `cordis-composition-reference`, `agent-experience`) bundled, unmodified, with their reference and template files and DeepSeek's MIT notice beside each. They stay hidden from the model until Creator mode. A bundled skill can now carry files; `load_skill` prints its base directory and a short Codewhale host-notes block, and a file you edited is never overwritten on upgrade.
|
||||
- **Website home page, rebuilt around one first screen:** the headline stays "The model can be anyone's. The agent should be yours.", centred, with one plain sentence under it that says what Codewhale is (an open-source coding agent for your terminal, written in Rust, working with any model and able to split a task across several agents), the install command, and one wide picture of a real terminal session that runs off the bottom of the first screen. A row of facts under the picture takes its counts of provider routes and models from the published release, and the page says several agents share a task only while the release has it. The route of where the product is going moves lower and smaller, and the home page names no other tools. The new text in the 17 non-English languages is machine-drafted and is labelled as such in the footer; the Arabic page now reads the opening sentence right to left.
|
||||
- **Conversation modes (Engine):** `GET /v1/modes` lists two built-in modes, `standard` (default) and `creator`, each with `id`, `name`, `description`, `source` and `available` (plus `reason` when not; Creator is listed unavailable, "not ready in this build", until it loads its own tools, and then also when no Node.js or Bun plugin runtime is found). A thread's mode is `agent_preset`, set on `POST /v1/threads` or by `PATCH /v1/threads/{id}` until the thread has a turn, saved on the thread record and the terminal session file and shown in `GET /v1/threads/summary`; records without it load as `standard`. An unknown or unavailable id, a change after the first turn, or a mode together with another vendor's agent is refused with `error.code` `unknown_mode`, `mode_unavailable`, `mode_locked` or `mode_not_supported_with_executor`. Source only: Creator loads nothing extra yet and no menu uses it.
|
||||
- **Website home page:** the page is reorganized around what a visitor does: a dated OrcaRouter offer strip with a visible referral tag, one proof strip (release, stars, contributors), a models section with the newest first-party models, one capabilities section that includes Computer Use, an "Everything is a plugin" teaser and a community panel whose live GitHub numbers fill in from the browser and hide when unavailable. Unused home copy was removed from all 18 site languages.
|
||||
- **Website home page, rebuilt around one first screen:** the headline stays "The model can be anyone's. The agent should be yours.", centred, with one plain sentence under it that says what Codewhale is (an open-source coding agent for your terminal, written in Rust, working with any model and able to split a task across several agents), the install command, and one wide picture of a real terminal session that runs off the bottom of the first screen. A row of facts under the picture takes its counts of provider routes and models from the published release, and the page says several agents share a task only while the release has it. The route of where the product is going moves lower and smaller, and the home page names no other tools. This replaces the earlier reorganization of the home page, so the dated OrcaRouter offer strip and the community panel whose GitHub numbers filled in from the browser are no longer on it; the models and contribute pages keep their own content. The new text in the 17 non-English languages is machine-drafted and is labelled as such in the footer; the Arabic page now reads the opening sentence right to left.
|
||||
- **Website home page, rebuilt around one first screen:** the headline stays "The model can be anyone's. The agent should be yours.", centred, with one plain sentence under it that says what Codewhale is (an open-source coding agent for your terminal, written in Rust, working with any model and able to split a task across several agents), the install command, and one wide picture of a real terminal session that runs off the bottom of the first screen. A row of facts under the picture takes its counts of provider routes and models from the published release, and the facts row lists many agents on one task only while the release has it. The route of where the product is going moves lower and smaller, and the home page names no other coding agents. This replaces the earlier reorganization of the home page, so the community panel whose GitHub numbers filled in from the browser is no longer on it, and the dated OrcaRouter offer strip, English only, with its referral disclosure, stays below the first screen until it ends; the models and contribute pages keep their own content. The new text in the 17 non-English languages is machine-drafted and is labelled as such in the footer; the Arabic page now reads the opening sentence right to left.
|
||||
|
||||
@@ -9572,6 +9572,7 @@ fn saved_session_with_messages(messages: Vec<Message>) -> SavedSession {
|
||||
composer_color: None,
|
||||
last_auto_route: None,
|
||||
turn_outcomes: Vec::new(),
|
||||
agent_preset: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1481,6 +1481,7 @@ plus workspace metadata:
|
||||
"preview": "The TUI footer should count project MCP servers...",
|
||||
"model": "deepseek-v4-pro",
|
||||
"mode": "agent",
|
||||
"agent_preset": "standard",
|
||||
"branch": "feature/runtime-api",
|
||||
"head": "abc1234",
|
||||
"dirty": false,
|
||||
@@ -1518,6 +1519,38 @@ Thread list and summary responses remain flat in v0.8.40, so clients that need
|
||||
a graph should reconstruct it from events instead of assuming list order is a
|
||||
complete tree.
|
||||
|
||||
### Conversation modes
|
||||
|
||||
`GET /v1/modes` lists the conversation modes a client can offer when it starts
|
||||
a conversation:
|
||||
|
||||
```json
|
||||
{
|
||||
"default": "standard",
|
||||
"modes": [
|
||||
{"id": "standard", "name": "Standard", "description": "The everyday Codewhale agent with its normal tools and skills.", "source": "builtin", "available": true},
|
||||
{"id": "creator", "name": "Creator", "description": "Build, inspect and test Codewhale plugins in this conversation, with the plugin authoring tools and guides loaded.", "source": "builtin", "available": false, "reason": "Creator mode is not ready in this build."}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`reason` appears only when `available` is `false`; a client lists the row
|
||||
disabled and shows it. A thread's mode is its `agent_preset` string. Set it on
|
||||
`POST /v1/threads`, or with `PATCH /v1/threads/{id}` while the thread has no
|
||||
turn. A thread record with no `agent_preset` is `standard`, and the thread
|
||||
record omits the field for `standard`; rows from `GET /v1/threads/summary`
|
||||
always carry it. This field stores and reports the mode; what a mode loads is
|
||||
not part of it.
|
||||
|
||||
Refusals carry `error.code`:
|
||||
|
||||
| `error.code` | Status | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `unknown_mode` | `400` | The id is not in `GET /v1/modes`. Never replaced by `standard`. |
|
||||
| `mode_unavailable` | `424` | The row is listed with `available: false`; the message is its `reason`. |
|
||||
| `mode_locked` | `409` | The thread already has a turn, so its mode cannot change. |
|
||||
| `mode_not_supported_with_executor` | `400` | `agent_preset` with `executor` (another vendor's agent). |
|
||||
|
||||
### Read markers
|
||||
|
||||
`POST /v1/threads/{id}/read` with `{"item_id": "<item id>"}` records that a
|
||||
@@ -2256,6 +2289,7 @@ This persisted project decision is separate from a thread’s `trust_mode` / Ful
|
||||
- `GET /v1/workspace/files/search?query=<partial>&limit=<1-100>` (see workspace file suggestions above)
|
||||
- `GET /v1/workspace/files?path=<dir>&limit=<1-2000>`, `GET /v1/workspace/files/read?path=<file>&offset=&limit=`
|
||||
and `PUT /v1/workspace/files` (see workspace files and session artifacts above)
|
||||
- `GET /v1/modes` (see Conversation modes)
|
||||
- `GET /v1/skills`
|
||||
- `GET /v1/skills/{name}` — one skill's routing metadata (`source`,
|
||||
`invocation`, `aliases`, `bundled_tier`, `enabled`) plus its full
|
||||
|
||||
@@ -129,6 +129,7 @@ registration index.
|
||||
| GET | `/v1/lsp` |
|
||||
| DELETE / GET / POST | `/v1/memory` |
|
||||
| GET | `/v1/memory/{id}` |
|
||||
| GET | `/v1/modes` |
|
||||
| GET / PATCH / POST | `/v1/operate` |
|
||||
| POST | `/v1/operate/auto-merge/check` |
|
||||
| POST | `/v1/operate/cancel` |
|
||||
@@ -262,4 +263,4 @@ registration index.
|
||||
| GET / POST | `/v1/worktrees` |
|
||||
| POST | `/v1/worktrees/remove` |
|
||||
|
||||
The source contains 242 distinct literal paths in this table.
|
||||
The source contains 243 distinct literal paths in this table.
|
||||
@@ -1056,6 +1056,7 @@ Runtime 前缀恢复验证与后续写入所需的完整文档条件保持独立
|
||||
"preview": "The TUI footer should count project MCP servers...",
|
||||
"model": "deepseek-v4-pro",
|
||||
"mode": "agent",
|
||||
"agent_preset": "standard",
|
||||
"branch": "feature/runtime-api",
|
||||
"head": "abc1234",
|
||||
"dirty": false,
|
||||
@@ -1087,6 +1088,35 @@ fork 还可能包含 `backtrack_depth_from_tail` 与 `dropped_turn_id`,而
|
||||
图的客户端应当从事件重建它,而不是假定列表顺序就是
|
||||
一棵完整的树。
|
||||
|
||||
### 对话模式
|
||||
|
||||
`GET /v1/modes` 列出客户端在开始对话时可提供的对话模式:
|
||||
|
||||
```json
|
||||
{
|
||||
"default": "standard",
|
||||
"modes": [
|
||||
{"id": "standard", "name": "Standard", "description": "The everyday Codewhale agent with its normal tools and skills.", "source": "builtin", "available": true},
|
||||
{"id": "creator", "name": "Creator", "description": "Build, inspect and test Codewhale plugins in this conversation, with the plugin authoring tools and guides loaded.", "source": "builtin", "available": false, "reason": "Creator mode is not ready in this build."}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
仅当 `available` 为 `false` 时才有 `reason`;客户端应将该行显示为禁用并展示原因。
|
||||
线程的模式是其 `agent_preset` 字符串,可在 `POST /v1/threads` 时设置,或在线程尚无
|
||||
任何回合时通过 `PATCH /v1/threads/{id}` 修改。没有 `agent_preset` 的线程记录即
|
||||
`standard`(线程记录对 `standard` 省略该字段;`GET /v1/threads/summary` 的每一行总是
|
||||
带有它)。该字段只存储并报告模式,不决定模式加载什么。
|
||||
|
||||
拒绝响应带有 `error.code`:
|
||||
|
||||
| `error.code` | 状态 | 含义 |
|
||||
| --- | --- | --- |
|
||||
| `unknown_mode` | `400` | 该 id 不在 `GET /v1/modes` 中;绝不会静默改为 `standard`。 |
|
||||
| `mode_unavailable` | `424` | 该行以 `available: false` 列出;消息即其 `reason`。 |
|
||||
| `mode_locked` | `409` | 线程已有回合,模式不可再更改。 |
|
||||
| `mode_not_supported_with_executor` | `400` | `agent_preset` 与 `executor`(其他厂商的代理)同时出现。 |
|
||||
|
||||
### 已读标记
|
||||
|
||||
`POST /v1/threads/{id}/read` 携带 `{"item_id": "<item id>"}`,记录客户端已把对话
|
||||
@@ -1660,6 +1690,7 @@ direction 为空、等待 lead 计划、缺少凭据或有
|
||||
- `GET /v1/workspace/files/search?query=<partial>&limit=<1-100>`(参见上文的工作区文件建议)
|
||||
- `GET /v1/workspace/files?path=<dir>&limit=<1-2000>`、`GET /v1/workspace/files/read?path=<file>&offset=&limit=`
|
||||
与 `PUT /v1/workspace/files`(参见上文的工作区文件与会话工件)
|
||||
- `GET /v1/modes`(参见对话模式)
|
||||
- `GET /v1/skills`
|
||||
- `GET /v1/skills/{name}` — 返回路由元数据(`source`、`invocation`、`aliases`、
|
||||
`bundled_tier`、`enabled`)及完整 `SKILL.md`,让客户端为自己的下一回合组成激活指令。
|
||||
|
||||
@@ -334,8 +334,8 @@
|
||||
"sources": [
|
||||
{
|
||||
"path": "docs/RUNTIME_API.md",
|
||||
"sha256": "d2d2d6211c35fe02ce6ec48c881879fe7dd9a45f1bd4ba1b7d64b9df663ef3ec",
|
||||
"content": "# Codewhale Engine Runtime API & Integration Contract\n\n> 阅读简体中文版:[zh_hans/RUNTIME_API.md](zh_hans/RUNTIME_API.md)。\n\n`codewhale app-server --http` exposes the [Codewhale Engine](ARCHITECTURE.md) through\nits canonical local Runtime API and control plane.\nLocal SDKs, mobile/remote-control clients, and editor integrations talk to it\ninstead of screen-scraping terminal output. It serves the full HTTP/SSE runtime\nAPI (`/v1/*`), a JSON-RPC control transport over stdio, and the phone-friendly\nmobile page. `codewhale doctor --json` provides machine-readable health, and\n`codewhale serve --acp` speaks the Agent Client Protocol over stdio for editors\nsuch as Zed.\n\n`codewhale serve --http` / `serve --mobile` remain as **compatibility aliases**\nfor `codewhale app-server --http` / `--mobile`; both launch the identical\nserver. Desktop, web, SDK and custom integrations should target\n`app-server --http`. `--stdio` and `--socket` expose compatibility controls.\nBare `app-server` selects initialized JSONL control over stdio. Local stdio,\nsocket and ACP owners do not bind a hidden TCP listener. Explicit `--http`\nretains HTTP/SSE and compatibility routes on the same Runtime owner.\n\n`codewhale exec` is the separate one-shot headless worker path (stream-json,\nfleet worker subprocess, CI primitive). It is not part of this API, but it\nshares the same runtime, provider/model resolution, permission profiles, and\nevent vocabulary.\n\nThis document is the stable integration contract for native workbench\napplications (and other local supervisors) that embed the Codewhale Engine.\n\n## Architecture\n\n```\nlocal supervisor / SDK / automation harness\n │\n ├─ codewhale app-server --http → HTTP/SSE runtime API (/v1/*) [canonical]\n ├─ codewhale app-server --mobile → runtime API + mobile control page\n ├─ codewhale app-server --stdio → JSON-RPC control transport over stdio\n ├─ codewhale app-server --socket → same JSON-RPC over a unix domain socket (desktop daemon)\n ├─ codewhale doctor --json → machine-readable health & capability\n ├─ codewhale serve --acp → ACP stdio agent for editors such as Zed\n ├─ codewhale serve --mcp → MCP stdio server\n ├─ codewhale serve --http/--mobile → legacy aliases for `app-server --http/--mobile`\n └─ codewhale exec [args] → one-shot headless worker (stream-json)\n```\n\nThe engine runs as a local-only process. All APIs bind to `localhost` by\ndefault. No hosted relay, no provider-token custody, no secret leakage.\n\nFor the read-only record of what a thread or turn did, see\n[`docs/RECEIPTS.md`](RECEIPTS.md): `codewhale receipts` on the CLI and the\n`/receipt` routes under **Threads** below.\n\n## Runtime API entrypoints\n\n| Entry | Transport | Use |\n|---|---|---|\n| `codewhale web [--port 7878]` | HTTP/SSE on `127.0.0.1:7878` + embedded client | First-class loopback-only browser client; opens the default browser |\n| `codewhale app-server --http` | HTTP/SSE on `127.0.0.1:7878` | Full `/v1/*` runtime API (canonical) |\n| `codewhale app-server --mobile` | HTTP/SSE on loopback + `/mobile` | Runtime API + local mobile control page |\n| `codewhale app-server --stdio` | Initialized JSONL over stdio | Compatibility controls and explicit tool-free turns through the held manager; no hidden TCP listener or full Codex interaction channel. |\n| `codewhale app-server --socket [--socket-path P]` | RPC text messages over WebSocket on a `0600` Unix domain socket (macOS/Linux) or an owner-only named pipe (Windows) | Desktop daemon: multi-client, kernel peer identity checked, `daemon/attach` claim handshake. Windows native-app qualification remains open. |\n| `codewhale app-server` | Initialized JSONL over stdio | Same selection as `--stdio`; HTTP requires explicit `--http`. |\n| `codewhale serve --http` / `--mobile` | same server as `app-server --http`/`--mobile` | Compatibility aliases |\n\n`app-server --http` and `--mobile` launch the same mature runtime API server\nhistorically reached through `serve --http` — no routes or behavior changed, so\nevery endpoint documented below is identical across both entrypoints. The\nruntime API token is read from `--auth-token`, then `CODEWHALE_RUNTIME_TOKEN`,\nthen `DEEPSEEK_RUNTIME_TOKEN`; use `--insecure-no-auth` only with a loopback\nbind. The `serve` compatibility aliases keep their `--insecure` flag.\nBare `app-server` does not open an HTTP port. Selected attached HTTP listeners\nretain their explicit bearer and non-loopback security checks.\n\nInitialized stdio/private-socket controls support `thread/loaded/list` with\n`{cursor?: string | null, limit?: number | null}`. The result is\n`{data: string[], nextCursor: string | null}` from the held manager's loaded\nengine snapshot, including idle and archived engines still in memory. It dLine truncated
|
||||
"sha256": "aa431fd8e623203508227c20dd0d34c577fee4d33e1a4d9bb69161c8c5df6ce7",
|
||||
"content": "# Codewhale Engine Runtime API & Integration Contract\n\n> 阅读简体中文版:[zh_hans/RUNTIME_API.md](zh_hans/RUNTIME_API.md)。\n\n`codewhale app-server --http` exposes the [Codewhale Engine](ARCHITECTURE.md) through\nits canonical local Runtime API and control plane.\nLocal SDKs, mobile/remote-control clients, and editor integrations talk to it\ninstead of screen-scraping terminal output. It serves the full HTTP/SSE runtime\nAPI (`/v1/*`), a JSON-RPC control transport over stdio, and the phone-friendly\nmobile page. `codewhale doctor --json` provides machine-readable health, and\n`codewhale serve --acp` speaks the Agent Client Protocol over stdio for editors\nsuch as Zed.\n\n`codewhale serve --http` / `serve --mobile` remain as **compatibility aliases**\nfor `codewhale app-server --http` / `--mobile`; both launch the identical\nserver. Desktop, web, SDK and custom integrations should target\n`app-server --http`. `--stdio` and `--socket` expose compatibility controls.\nBare `app-server` selects initialized JSONL control over stdio. Local stdio,\nsocket and ACP owners do not bind a hidden TCP listener. Explicit `--http`\nretains HTTP/SSE and compatibility routes on the same Runtime owner.\n\n`codewhale exec` is the separate one-shot headless worker path (stream-json,\nfleet worker subprocess, CI primitive). It is not part of this API, but it\nshares the same runtime, provider/model resolution, permission profiles, and\nevent vocabulary.\n\nThis document is the stable integration contract for native workbench\napplications (and other local supervisors) that embed the Codewhale Engine.\n\n## Architecture\n\n```\nlocal supervisor / SDK / automation harness\n │\n ├─ codewhale app-server --http → HTTP/SSE runtime API (/v1/*) [canonical]\n ├─ codewhale app-server --mobile → runtime API + mobile control page\n ├─ codewhale app-server --stdio → JSON-RPC control transport over stdio\n ├─ codewhale app-server --socket → same JSON-RPC over a unix domain socket (desktop daemon)\n ├─ codewhale doctor --json → machine-readable health & capability\n ├─ codewhale serve --acp → ACP stdio agent for editors such as Zed\n ├─ codewhale serve --mcp → MCP stdio server\n ├─ codewhale serve --http/--mobile → legacy aliases for `app-server --http/--mobile`\n └─ codewhale exec [args] → one-shot headless worker (stream-json)\n```\n\nThe engine runs as a local-only process. All APIs bind to `localhost` by\ndefault. No hosted relay, no provider-token custody, no secret leakage.\n\nFor the read-only record of what a thread or turn did, see\n[`docs/RECEIPTS.md`](RECEIPTS.md): `codewhale receipts` on the CLI and the\n`/receipt` routes under **Threads** below.\n\n## Runtime API entrypoints\n\n| Entry | Transport | Use |\n|---|---|---|\n| `codewhale web [--port 7878]` | HTTP/SSE on `127.0.0.1:7878` + embedded client | First-class loopback-only browser client; opens the default browser |\n| `codewhale app-server --http` | HTTP/SSE on `127.0.0.1:7878` | Full `/v1/*` runtime API (canonical) |\n| `codewhale app-server --mobile` | HTTP/SSE on loopback + `/mobile` | Runtime API + local mobile control page |\n| `codewhale app-server --stdio` | Initialized JSONL over stdio | Compatibility controls and explicit tool-free turns through the held manager; no hidden TCP listener or full Codex interaction channel. |\n| `codewhale app-server --socket [--socket-path P]` | RPC text messages over WebSocket on a `0600` Unix domain socket (macOS/Linux) or an owner-only named pipe (Windows) | Desktop daemon: multi-client, kernel peer identity checked, `daemon/attach` claim handshake. Windows native-app qualification remains open. |\n| `codewhale app-server` | Initialized JSONL over stdio | Same selection as `--stdio`; HTTP requires explicit `--http`. |\n| `codewhale serve --http` / `--mobile` | same server as `app-server --http`/`--mobile` | Compatibility aliases |\n\n`app-server --http` and `--mobile` launch the same mature runtime API server\nhistorically reached through `serve --http` — no routes or behavior changed, so\nevery endpoint documented below is identical across both entrypoints. The\nruntime API token is read from `--auth-token`, then `CODEWHALE_RUNTIME_TOKEN`,\nthen `DEEPSEEK_RUNTIME_TOKEN`; use `--insecure-no-auth` only with a loopback\nbind. The `serve` compatibility aliases keep their `--insecure` flag.\nBare `app-server` does not open an HTTP port. Selected attached HTTP listeners\nretain their explicit bearer and non-loopback security checks.\n\nInitialized stdio/private-socket controls support `thread/loaded/list` with\n`{cursor?: string | null, limit?: number | null}`. The result is\n`{data: string[], nextCursor: string | null}` from the held manager's loaded\nengine snapshot, including idle and archived engines still in memory. It dLine truncated
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -403,13 +403,13 @@
|
||||
"sources": [
|
||||
{
|
||||
"path": "docs/RUNTIME_API.md",
|
||||
"sha256": "d2d2d6211c35fe02ce6ec48c881879fe7dd9a45f1bd4ba1b7d64b9df663ef3ec",
|
||||
"content": "# Codewhale Engine Runtime API & Integration Contract\n\n> 阅读简体中文版:[zh_hans/RUNTIME_API.md](zh_hans/RUNTIME_API.md)。\n\n`codewhale app-server --http` exposes the [Codewhale Engine](ARCHITECTURE.md) through\nits canonical local Runtime API and control plane.\nLocal SDKs, mobile/remote-control clients, and editor integrations talk to it\ninstead of screen-scraping terminal output. It serves the full HTTP/SSE runtime\nAPI (`/v1/*`), a JSON-RPC control transport over stdio, and the phone-friendly\nmobile page. `codewhale doctor --json` provides machine-readable health, and\n`codewhale serve --acp` speaks the Agent Client Protocol over stdio for editors\nsuch as Zed.\n\n`codewhale serve --http` / `serve --mobile` remain as **compatibility aliases**\nfor `codewhale app-server --http` / `--mobile`; both launch the identical\nserver. Desktop, web, SDK and custom integrations should target\n`app-server --http`. `--stdio` and `--socket` expose compatibility controls.\nBare `app-server` selects initialized JSONL control over stdio. Local stdio,\nsocket and ACP owners do not bind a hidden TCP listener. Explicit `--http`\nretains HTTP/SSE and compatibility routes on the same Runtime owner.\n\n`codewhale exec` is the separate one-shot headless worker path (stream-json,\nfleet worker subprocess, CI primitive). It is not part of this API, but it\nshares the same runtime, provider/model resolution, permission profiles, and\nevent vocabulary.\n\nThis document is the stable integration contract for native workbench\napplications (and other local supervisors) that embed the Codewhale Engine.\n\n## Architecture\n\n```\nlocal supervisor / SDK / automation harness\n │\n ├─ codewhale app-server --http → HTTP/SSE runtime API (/v1/*) [canonical]\n ├─ codewhale app-server --mobile → runtime API + mobile control page\n ├─ codewhale app-server --stdio → JSON-RPC control transport over stdio\n ├─ codewhale app-server --socket → same JSON-RPC over a unix domain socket (desktop daemon)\n ├─ codewhale doctor --json → machine-readable health & capability\n ├─ codewhale serve --acp → ACP stdio agent for editors such as Zed\n ├─ codewhale serve --mcp → MCP stdio server\n ├─ codewhale serve --http/--mobile → legacy aliases for `app-server --http/--mobile`\n └─ codewhale exec [args] → one-shot headless worker (stream-json)\n```\n\nThe engine runs as a local-only process. All APIs bind to `localhost` by\ndefault. No hosted relay, no provider-token custody, no secret leakage.\n\nFor the read-only record of what a thread or turn did, see\n[`docs/RECEIPTS.md`](RECEIPTS.md): `codewhale receipts` on the CLI and the\n`/receipt` routes under **Threads** below.\n\n## Runtime API entrypoints\n\n| Entry | Transport | Use |\n|---|---|---|\n| `codewhale web [--port 7878]` | HTTP/SSE on `127.0.0.1:7878` + embedded client | First-class loopback-only browser client; opens the default browser |\n| `codewhale app-server --http` | HTTP/SSE on `127.0.0.1:7878` | Full `/v1/*` runtime API (canonical) |\n| `codewhale app-server --mobile` | HTTP/SSE on loopback + `/mobile` | Runtime API + local mobile control page |\n| `codewhale app-server --stdio` | Initialized JSONL over stdio | Compatibility controls and explicit tool-free turns through the held manager; no hidden TCP listener or full Codex interaction channel. |\n| `codewhale app-server --socket [--socket-path P]` | RPC text messages over WebSocket on a `0600` Unix domain socket (macOS/Linux) or an owner-only named pipe (Windows) | Desktop daemon: multi-client, kernel peer identity checked, `daemon/attach` claim handshake. Windows native-app qualification remains open. |\n| `codewhale app-server` | Initialized JSONL over stdio | Same selection as `--stdio`; HTTP requires explicit `--http`. |\n| `codewhale serve --http` / `--mobile` | same server as `app-server --http`/`--mobile` | Compatibility aliases |\n\n`app-server --http` and `--mobile` launch the same mature runtime API server\nhistorically reached through `serve --http` — no routes or behavior changed, so\nevery endpoint documented below is identical across both entrypoints. The\nruntime API token is read from `--auth-token`, then `CODEWHALE_RUNTIME_TOKEN`,\nthen `DEEPSEEK_RUNTIME_TOKEN`; use `--insecure-no-auth` only with a loopback\nbind. The `serve` compatibility aliases keep their `--insecure` flag.\nBare `app-server` does not open an HTTP port. Selected attached HTTP listeners\nretain their explicit bearer and non-loopback security checks.\n\nInitialized stdio/private-socket controls support `thread/loaded/list` with\n`{cursor?: string | null, limit?: number | null}`. The result is\n`{data: string[], nextCursor: string | null}` from the held manager's loaded\nengine snapshot, including idle and archived engines still in memory. It dLine truncated
|
||||
"sha256": "aa431fd8e623203508227c20dd0d34c577fee4d33e1a4d9bb69161c8c5df6ce7",
|
||||
"content": "# Codewhale Engine Runtime API & Integration Contract\n\n> 阅读简体中文版:[zh_hans/RUNTIME_API.md](zh_hans/RUNTIME_API.md)。\n\n`codewhale app-server --http` exposes the [Codewhale Engine](ARCHITECTURE.md) through\nits canonical local Runtime API and control plane.\nLocal SDKs, mobile/remote-control clients, and editor integrations talk to it\ninstead of screen-scraping terminal output. It serves the full HTTP/SSE runtime\nAPI (`/v1/*`), a JSON-RPC control transport over stdio, and the phone-friendly\nmobile page. `codewhale doctor --json` provides machine-readable health, and\n`codewhale serve --acp` speaks the Agent Client Protocol over stdio for editors\nsuch as Zed.\n\n`codewhale serve --http` / `serve --mobile` remain as **compatibility aliases**\nfor `codewhale app-server --http` / `--mobile`; both launch the identical\nserver. Desktop, web, SDK and custom integrations should target\n`app-server --http`. `--stdio` and `--socket` expose compatibility controls.\nBare `app-server` selects initialized JSONL control over stdio. Local stdio,\nsocket and ACP owners do not bind a hidden TCP listener. Explicit `--http`\nretains HTTP/SSE and compatibility routes on the same Runtime owner.\n\n`codewhale exec` is the separate one-shot headless worker path (stream-json,\nfleet worker subprocess, CI primitive). It is not part of this API, but it\nshares the same runtime, provider/model resolution, permission profiles, and\nevent vocabulary.\n\nThis document is the stable integration contract for native workbench\napplications (and other local supervisors) that embed the Codewhale Engine.\n\n## Architecture\n\n```\nlocal supervisor / SDK / automation harness\n │\n ├─ codewhale app-server --http → HTTP/SSE runtime API (/v1/*) [canonical]\n ├─ codewhale app-server --mobile → runtime API + mobile control page\n ├─ codewhale app-server --stdio → JSON-RPC control transport over stdio\n ├─ codewhale app-server --socket → same JSON-RPC over a unix domain socket (desktop daemon)\n ├─ codewhale doctor --json → machine-readable health & capability\n ├─ codewhale serve --acp → ACP stdio agent for editors such as Zed\n ├─ codewhale serve --mcp → MCP stdio server\n ├─ codewhale serve --http/--mobile → legacy aliases for `app-server --http/--mobile`\n └─ codewhale exec [args] → one-shot headless worker (stream-json)\n```\n\nThe engine runs as a local-only process. All APIs bind to `localhost` by\ndefault. No hosted relay, no provider-token custody, no secret leakage.\n\nFor the read-only record of what a thread or turn did, see\n[`docs/RECEIPTS.md`](RECEIPTS.md): `codewhale receipts` on the CLI and the\n`/receipt` routes under **Threads** below.\n\n## Runtime API entrypoints\n\n| Entry | Transport | Use |\n|---|---|---|\n| `codewhale web [--port 7878]` | HTTP/SSE on `127.0.0.1:7878` + embedded client | First-class loopback-only browser client; opens the default browser |\n| `codewhale app-server --http` | HTTP/SSE on `127.0.0.1:7878` | Full `/v1/*` runtime API (canonical) |\n| `codewhale app-server --mobile` | HTTP/SSE on loopback + `/mobile` | Runtime API + local mobile control page |\n| `codewhale app-server --stdio` | Initialized JSONL over stdio | Compatibility controls and explicit tool-free turns through the held manager; no hidden TCP listener or full Codex interaction channel. |\n| `codewhale app-server --socket [--socket-path P]` | RPC text messages over WebSocket on a `0600` Unix domain socket (macOS/Linux) or an owner-only named pipe (Windows) | Desktop daemon: multi-client, kernel peer identity checked, `daemon/attach` claim handshake. Windows native-app qualification remains open. |\n| `codewhale app-server` | Initialized JSONL over stdio | Same selection as `--stdio`; HTTP requires explicit `--http`. |\n| `codewhale serve --http` / `--mobile` | same server as `app-server --http`/`--mobile` | Compatibility aliases |\n\n`app-server --http` and `--mobile` launch the same mature runtime API server\nhistorically reached through `serve --http` — no routes or behavior changed, so\nevery endpoint documented below is identical across both entrypoints. The\nruntime API token is read from `--auth-token`, then `CODEWHALE_RUNTIME_TOKEN`,\nthen `DEEPSEEK_RUNTIME_TOKEN`; use `--insecure-no-auth` only with a loopback\nbind. The `serve` compatibility aliases keep their `--insecure` flag.\nBare `app-server` does not open an HTTP port. Selected attached HTTP listeners\nretain their explicit bearer and non-loopback security checks.\n\nInitialized stdio/private-socket controls support `thread/loaded/list` with\n`{cursor?: string | null, limit?: number | null}`. The result is\n`{data: string[], nextCursor: string | null}` from the held manager's loaded\nengine snapshot, including idle and archived engines still in memory. It dLine truncated
|
||||
},
|
||||
{
|
||||
"path": "docs/RUNTIME_ROUTES.md",
|
||||
"sha256": "6aa9cf20c05e9d155687a2b34f11d1ecd4d6a2c29c6295305f077c5a0671a4fe",
|
||||
"content": "# Runtime HTTP route inventory\n\nThis table is generated from literal route registrations in\n`crates/app-server/src/runtime_api.rs`. It names the paths and methods that\nthe current source registers, including native-client and compatibility\nsurfaces. A registered path is not a grant of permission or a promise that\na feature is enabled in your installed version.\n\nThe [Runtime API contract](RUNTIME_API.md) explains request fields, events,\nauthorization and compatibility. This inventory is a navigation index, not\na replacement for those schemas; routes used by the native client can have\nclient-specific contracts. Provider calls, file mutations and connected-service\nactions remain subject to their documented authority boundaries.\n\nRun `node web/scripts/check-runtime-routes.mjs --write` after changing a\nregistration; `npm run check:docs` refuses a stale inventory. Dynamic nested\nrouters, fallback handlers and static-asset mounts are outside this literal\nregistration index.\n\n| HTTP method | Path |\n| --- | --- |\n| GET | `/` |\n| GET | `/__codewhale/bootstrap/{nonce}` |\n| GET | `/__codewhale/mobile/bootstrap/{nonce}` |\n| POST | `/__codewhale/mobile/session` |\n| POST | `/__codewhale/mobile/stream-ticket` |\n| POST | `/__codewhale/web/stream-ticket` |\n| GET | `/assets/codewhale-192.png` |\n| GET | `/assets/codewhale-web.css` |\n| GET | `/assets/codewhale-web.js` |\n| GET | `/health` |\n| GET | `/mobile` |\n| GET | `/mobile/` |\n| DELETE / GET / PUT | `/v1/account/model-access` |\n| POST | `/v1/agent-mail` |\n| GET | `/v1/agent-runs` |\n| GET | `/v1/agent-runs/{run_id}` |\n| POST | `/v1/agent-runs/{run_id}/cancel` |\n| GET | `/v1/agents/external` |\n| GET | `/v1/agents/external/{id}/options` |\n| GET | `/v1/agents/external/quota` |\n| GET | `/v1/approvals` |\n| POST | `/v1/approvals/{approval_id}` |\n| GET | `/v1/apps/avatars` |\n| GET | `/v1/apps/avatars/{handle}/atlases/{page}` |\n| GET / POST | `/v1/apps/marketplaces` |\n| DELETE / GET | `/v1/apps/marketplaces/{name}` |\n| POST | `/v1/apps/marketplaces/{name}/install` |\n| POST | `/v1/apps/mcp/desktop-browser` |\n| GET / POST | `/v1/apps/mcp/imports` |\n| GET / POST | `/v1/apps/mcp/servers` |\n| DELETE / GET / PATCH | `/v1/apps/mcp/servers/{name}` |\n| POST | `/v1/apps/mcp/servers/{name}/disable` |\n| POST | `/v1/apps/mcp/servers/{name}/enable` |\n| POST | `/v1/apps/mcp/servers/{name}/reconnect` |\n| GET | `/v1/apps/mcp/tools` |\n| GET | `/v1/apps/plugins` |\n| DELETE / GET | `/v1/apps/plugins/{selector}` |\n| POST | `/v1/apps/plugins/{selector}/disable` |\n| POST | `/v1/apps/plugins/{selector}/enable` |\n| POST | `/v1/apps/plugins/{selector}/restage` |\n| POST | `/v1/apps/plugins/{selector}/revoke` |\n| ANY | `/v1/apps/plugins/{selector}/routes/{*path}` |\n| POST | `/v1/apps/plugins/{selector}/trust` |\n| GET | `/v1/apps/plugins/{selector}/ui` |\n| GET | `/v1/apps/plugins/{selector}/ui/{*path}` |\n| POST | `/v1/apps/plugins/{selector}/update` |\n| GET | `/v1/apps/plugins/catalog` |\n| POST | `/v1/apps/plugins/catalog/{id}/disable` |\n| POST | `/v1/apps/plugins/catalog/{id}/enable` |\n| PUT | `/v1/apps/plugins/catalog/{id}/settings` |\n| POST | `/v1/apps/plugins/import/dsh/preview` |\n| POST | `/v1/apps/plugins/install` |\n| GET / POST | `/v1/automations` |\n| DELETE / GET / PATCH | `/v1/automations/{id}` |\n| GET | `/v1/automations/{id}/missed` |\n| PUT | `/v1/automations/{id}/missed/policy` |\n| POST | `/v1/automations/{id}/missed/run` |\n| POST | `/v1/automations/{id}/missed/skip` |\n| POST | `/v1/automations/{id}/pause` |\n| POST | `/v1/automations/{id}/resume` |\n| POST | `/v1/automations/{id}/run` |\n| GET | `/v1/automations/{id}/runs` |\n| GET | `/v1/automations/missed` |\n| GET | `/v1/changes` |\n| GET / POST | `/v1/cloud` |\n| POST | `/v1/cloud/attach` |\n| GET | `/v1/commands` |\n| GET / POST | `/v1/config` |\n| POST | `/v1/config/reload` |\n| GET | `/v1/constitution` |\n| POST | `/v1/constitution/preview` |\n| GET | `/v1/crashes` |\n| GET | `/v1/crashes/{name}` |\n| GET | `/v1/definition` |\n| GET | `/v1/diagnostics` |\n| GET | `/v1/diff` |\n| POST | `/v1/fleet/placement` |\n| GET | `/v1/fleet/profiles` |\n| GET | `/v1/fleet/profiles/{id}/document` |\n| GET / POST | `/v1/fleet/runs` |\n| GET | `/v1/fleet/runs/{run_id}` |\n| GET | `/v1/fleet/runs/{run_id}/events` |\n| GET | `/v1/fleet/runs/{run_id}/events/replay` |\n| GET | `/v1/fleet/runs/{run_id}/receipts` |\n| GET | `/v1/fleet/runs/{run_id}/receipts/{task_id}` |\n| GET | `/v1/fleet/runs/{run_id}/receipts/{task_id}/evidence` |\n| POST | `/v1/fleet/runs/{run_id}/start` |\n| POST | `/v1/fleet/runs/{run_id}/stop` |\n| GET | `/v1/fleet/runs/{run_id}/workers` |\n| GET | `/v1/fleet/workers/{worker_id}` |\n| POST | `/v1/fleet/workers/{worker_id}/interrupt` |\n| POST | `/v1/fleet/workers/{worker_id}/restart` |\n| POST | `/v1/fleet/workers/{worker_id}/stop` |\n| GET | `/v1/git` |\n| POST | `/v1/git/branch` |\n| POST | `/v1/git/commit` |\n| POST | `/v1/git/discard` |\nLine truncated
|
||||
"sha256": "1a21c05b0249c2ff225f7b554e1e610fc12122ab3da52e6ac7ceb06c90448984",
|
||||
"content": "# Runtime HTTP route inventory\n\nThis table is generated from literal route registrations in\n`crates/app-server/src/runtime_api.rs`. It names the paths and methods that\nthe current source registers, including native-client and compatibility\nsurfaces. A registered path is not a grant of permission or a promise that\na feature is enabled in your installed version.\n\nThe [Runtime API contract](RUNTIME_API.md) explains request fields, events,\nauthorization and compatibility. This inventory is a navigation index, not\na replacement for those schemas; routes used by the native client can have\nclient-specific contracts. Provider calls, file mutations and connected-service\nactions remain subject to their documented authority boundaries.\n\nRun `node web/scripts/check-runtime-routes.mjs --write` after changing a\nregistration; `npm run check:docs` refuses a stale inventory. Dynamic nested\nrouters, fallback handlers and static-asset mounts are outside this literal\nregistration index.\n\n| HTTP method | Path |\n| --- | --- |\n| GET | `/` |\n| GET | `/__codewhale/bootstrap/{nonce}` |\n| GET | `/__codewhale/mobile/bootstrap/{nonce}` |\n| POST | `/__codewhale/mobile/session` |\n| POST | `/__codewhale/mobile/stream-ticket` |\n| POST | `/__codewhale/web/stream-ticket` |\n| GET | `/assets/codewhale-192.png` |\n| GET | `/assets/codewhale-web.css` |\n| GET | `/assets/codewhale-web.js` |\n| GET | `/health` |\n| GET | `/mobile` |\n| GET | `/mobile/` |\n| DELETE / GET / PUT | `/v1/account/model-access` |\n| POST | `/v1/agent-mail` |\n| GET | `/v1/agent-runs` |\n| GET | `/v1/agent-runs/{run_id}` |\n| POST | `/v1/agent-runs/{run_id}/cancel` |\n| GET | `/v1/agents/external` |\n| GET | `/v1/agents/external/{id}/options` |\n| GET | `/v1/agents/external/quota` |\n| GET | `/v1/approvals` |\n| POST | `/v1/approvals/{approval_id}` |\n| GET | `/v1/apps/avatars` |\n| GET | `/v1/apps/avatars/{handle}/atlases/{page}` |\n| GET / POST | `/v1/apps/marketplaces` |\n| DELETE / GET | `/v1/apps/marketplaces/{name}` |\n| POST | `/v1/apps/marketplaces/{name}/install` |\n| POST | `/v1/apps/mcp/desktop-browser` |\n| GET / POST | `/v1/apps/mcp/imports` |\n| GET / POST | `/v1/apps/mcp/servers` |\n| DELETE / GET / PATCH | `/v1/apps/mcp/servers/{name}` |\n| POST | `/v1/apps/mcp/servers/{name}/disable` |\n| POST | `/v1/apps/mcp/servers/{name}/enable` |\n| POST | `/v1/apps/mcp/servers/{name}/reconnect` |\n| GET | `/v1/apps/mcp/tools` |\n| GET | `/v1/apps/plugins` |\n| DELETE / GET | `/v1/apps/plugins/{selector}` |\n| POST | `/v1/apps/plugins/{selector}/disable` |\n| POST | `/v1/apps/plugins/{selector}/enable` |\n| POST | `/v1/apps/plugins/{selector}/restage` |\n| POST | `/v1/apps/plugins/{selector}/revoke` |\n| ANY | `/v1/apps/plugins/{selector}/routes/{*path}` |\n| POST | `/v1/apps/plugins/{selector}/trust` |\n| GET | `/v1/apps/plugins/{selector}/ui` |\n| GET | `/v1/apps/plugins/{selector}/ui/{*path}` |\n| POST | `/v1/apps/plugins/{selector}/update` |\n| GET | `/v1/apps/plugins/catalog` |\n| POST | `/v1/apps/plugins/catalog/{id}/disable` |\n| POST | `/v1/apps/plugins/catalog/{id}/enable` |\n| PUT | `/v1/apps/plugins/catalog/{id}/settings` |\n| POST | `/v1/apps/plugins/import/dsh/preview` |\n| POST | `/v1/apps/plugins/install` |\n| GET / POST | `/v1/automations` |\n| DELETE / GET / PATCH | `/v1/automations/{id}` |\n| GET | `/v1/automations/{id}/missed` |\n| PUT | `/v1/automations/{id}/missed/policy` |\n| POST | `/v1/automations/{id}/missed/run` |\n| POST | `/v1/automations/{id}/missed/skip` |\n| POST | `/v1/automations/{id}/pause` |\n| POST | `/v1/automations/{id}/resume` |\n| POST | `/v1/automations/{id}/run` |\n| GET | `/v1/automations/{id}/runs` |\n| GET | `/v1/automations/missed` |\n| GET | `/v1/changes` |\n| GET / POST | `/v1/cloud` |\n| POST | `/v1/cloud/attach` |\n| GET | `/v1/commands` |\n| GET / POST | `/v1/config` |\n| POST | `/v1/config/reload` |\n| GET | `/v1/constitution` |\n| POST | `/v1/constitution/preview` |\n| GET | `/v1/crashes` |\n| GET | `/v1/crashes/{name}` |\n| GET | `/v1/definition` |\n| GET | `/v1/diagnostics` |\n| GET | `/v1/diff` |\n| POST | `/v1/fleet/placement` |\n| GET | `/v1/fleet/profiles` |\n| GET | `/v1/fleet/profiles/{id}/document` |\n| GET / POST | `/v1/fleet/runs` |\n| GET | `/v1/fleet/runs/{run_id}` |\n| GET | `/v1/fleet/runs/{run_id}/events` |\n| GET | `/v1/fleet/runs/{run_id}/events/replay` |\n| GET | `/v1/fleet/runs/{run_id}/receipts` |\n| GET | `/v1/fleet/runs/{run_id}/receipts/{task_id}` |\n| GET | `/v1/fleet/runs/{run_id}/receipts/{task_id}/evidence` |\n| POST | `/v1/fleet/runs/{run_id}/start` |\n| POST | `/v1/fleet/runs/{run_id}/stop` |\n| GET | `/v1/fleet/runs/{run_id}/workers` |\n| GET | `/v1/fleet/workers/{worker_id}` |\n| POST | `/v1/fleet/workers/{worker_id}/interrupt` |\n| POST | `/v1/fleet/workers/{worker_id}/restart` |\n| POST | `/v1/fleet/workers/{worker_id}/stop` |\n| GET | `/v1/git` |\n| POST | `/v1/git/branch` |\n| POST | `/v1/git/commit` |\n| POST | `/v1/git/discard` |\nLine truncated
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user