mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-11 10:01:30 -10:00
tasks: caller-given run names (APPS-153)
NewTaskRequest/TaskRecord/TaskSummary gain an optional name, stored as-given and omitted when absent so queues fall back to prompt-derived titles. Automation-triggered runs inherit the automation name; the tasks tool accepts an optional name param (schema extended, backward compatible). Purely additive: legacy records decode via serde defaults. Evidence: new created_tasks_keep_their_given_name passes (create/get/ list round-trip + unnamed omission); task_ filter 181 passed / 0 failed; clippy clean on touched lines.
This commit is contained in:
1 parent
96f993a855
commit
08a4eec009
7 files changed
+85
No files matched your search
@@ -2046,6 +2046,7 @@ fn new_run_record(
|
||||
fn automation_task_request(automation: &AutomationRecord) -> NewTaskRequest {
|
||||
NewTaskRequest {
|
||||
prompt: automation.prompt.clone(),
|
||||
name: Some(automation.name.clone()),
|
||||
model: automation.model.clone(),
|
||||
model_provider: automation.model_provider.clone(),
|
||||
model_provider_id: automation.model_provider_id.clone(),
|
||||
@@ -2420,6 +2421,7 @@ where
|
||||
execution_scope: current.execution_scope.clone(),
|
||||
request: NewTaskRequest {
|
||||
prompt: current.message.clone(),
|
||||
name: None,
|
||||
model: None,
|
||||
model_provider: None,
|
||||
model_provider_id: None,
|
||||
|
||||
@@ -1585,6 +1585,63 @@ async fn health_and_tasks_endpoints_work() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Created tasks keep their caller-given name through get and list; unnamed
|
||||
/// tasks omit the field so queues can fall back to the prompt summary.
|
||||
#[tokio::test]
|
||||
async fn created_tasks_keep_their_given_name() -> Result<()> {
|
||||
let Some((addr, _runtime_threads, handle)) = spawn_test_server().await? else {
|
||||
return Ok(());
|
||||
};
|
||||
let client = crate::tls::reqwest_client();
|
||||
|
||||
let named: serde_json::Value = client
|
||||
.post(format!("http://{addr}/v1/tasks"))
|
||||
.json(&json!({ "prompt": "migrate the widget", "name": "Widget migration" }))
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?;
|
||||
assert_eq!(named["name"], "Widget migration");
|
||||
let id = named["id"].as_str().expect("task id").to_string();
|
||||
|
||||
let unnamed: serde_json::Value = client
|
||||
.post(format!("http://{addr}/v1/tasks"))
|
||||
.json(&json!({ "prompt": "unnamed work" }))
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?;
|
||||
assert!(unnamed.get("name").is_none());
|
||||
|
||||
let detail: serde_json::Value = client
|
||||
.get(format!("http://{addr}/v1/tasks/{id}"))
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?;
|
||||
assert_eq!(detail["name"], "Widget migration");
|
||||
|
||||
let listed: serde_json::Value = client
|
||||
.get(format!("http://{addr}/v1/tasks"))
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?;
|
||||
let rows = listed["tasks"].as_array().expect("task rows");
|
||||
let row = rows
|
||||
.iter()
|
||||
.find(|row| row["id"] == id)
|
||||
.expect("named row in list");
|
||||
assert_eq!(row["name"], "Widget migration");
|
||||
|
||||
handle.abort();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn omitted_runtime_models_use_the_active_provider_default() -> Result<()> {
|
||||
let _env = lock_test_env();
|
||||
|
||||
@@ -274,6 +274,8 @@ pub struct TaskRecord {
|
||||
pub schema_version: u32,
|
||||
pub id: String,
|
||||
pub prompt: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
pub model: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub model_provider: Option<String>,
|
||||
@@ -339,6 +341,8 @@ pub struct TaskSummary {
|
||||
pub id: String,
|
||||
pub status: TaskStatus,
|
||||
pub prompt_summary: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
pub model: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub model_provider: Option<String>,
|
||||
@@ -371,6 +375,7 @@ impl From<&TaskRecord> for TaskSummary {
|
||||
id: value.id.clone(),
|
||||
status: value.status,
|
||||
prompt_summary: summarize_text(&value.prompt, TIMELINE_SUMMARY_LIMIT),
|
||||
name: value.name.clone(),
|
||||
model: value.model.clone(),
|
||||
model_provider: value.model_provider.clone(),
|
||||
model_provider_id: value.model_provider_id.clone(),
|
||||
@@ -406,6 +411,10 @@ pub struct TaskCounts {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NewTaskRequest {
|
||||
pub prompt: String,
|
||||
/// Caller-given run name, stored as-given. Absent names stay absent —
|
||||
/// titles derived from the prompt are a presentation concern.
|
||||
#[serde(default)]
|
||||
pub name: Option<String>,
|
||||
pub model: Option<String>,
|
||||
#[serde(default)]
|
||||
pub model_provider: Option<String>,
|
||||
@@ -424,6 +433,7 @@ impl NewTaskRequest {
|
||||
pub(crate) fn from_task(task: &TaskRecord) -> Self {
|
||||
Self {
|
||||
prompt: task.prompt.clone(),
|
||||
name: task.name.clone(),
|
||||
model: Some(task.model.clone()),
|
||||
model_provider: task.model_provider.clone(),
|
||||
model_provider_id: task.model_provider_id.clone(),
|
||||
@@ -441,6 +451,7 @@ impl NewTaskRequest {
|
||||
pub fn from_prompt(prompt: impl Into<String>) -> Self {
|
||||
Self {
|
||||
prompt: prompt.into(),
|
||||
name: None,
|
||||
model: None,
|
||||
model_provider: None,
|
||||
model_provider_id: None,
|
||||
@@ -1669,6 +1680,10 @@ impl TaskManager {
|
||||
// work.
|
||||
id: task_id,
|
||||
prompt,
|
||||
name: req
|
||||
.name
|
||||
.map(|name| name.trim().to_string())
|
||||
.filter(|name| !name.is_empty()),
|
||||
model: req.model.unwrap_or_else(|| self.cfg.default_model.clone()),
|
||||
model_provider: req.model_provider,
|
||||
model_provider_id: req.model_provider_id,
|
||||
@@ -4199,6 +4214,7 @@ mod tests {
|
||||
schema_version: CURRENT_TASK_SCHEMA_VERSION,
|
||||
id: task_id.clone(),
|
||||
prompt: "long-running shell work".to_string(),
|
||||
name: None,
|
||||
model: "deepseek-v4-flash".to_string(),
|
||||
model_provider: None,
|
||||
model_provider_id: None,
|
||||
@@ -4400,6 +4416,7 @@ mod tests {
|
||||
|
||||
let req = NewTaskRequest {
|
||||
prompt: "fix TODOs and write a README".to_string(),
|
||||
name: None,
|
||||
model: None,
|
||||
model_provider: None,
|
||||
model_provider_id: None,
|
||||
@@ -4842,6 +4859,7 @@ mod tests {
|
||||
schema_version: CURRENT_TASK_SCHEMA_VERSION,
|
||||
id: "task_0123456789abcdef".to_string(),
|
||||
prompt: "bound timeline".to_string(),
|
||||
name: None,
|
||||
model: "deepseek-v4-flash".to_string(),
|
||||
model_provider: None,
|
||||
model_provider_id: None,
|
||||
|
||||
@@ -224,6 +224,10 @@ impl ToolSpec for TasksTool {
|
||||
"prompt".to_string(),
|
||||
json!({ "type": "string", "description": "Work prompt for the durable task (action=create)." }),
|
||||
);
|
||||
properties.insert(
|
||||
"name".to_string(),
|
||||
json!({ "type": "string", "description": "Short run name shown in queues; omit to derive from the prompt. (action=create)" }),
|
||||
);
|
||||
properties.insert(
|
||||
"model_provider".to_string(),
|
||||
json!({ "type": "string", "description": "Provider kind for the pinned model. Omit to inherit the configured provider." }),
|
||||
@@ -475,6 +479,7 @@ impl TasksTool {
|
||||
let prompt = required_str(input, "prompt")?.to_string();
|
||||
let req = NewTaskRequest {
|
||||
prompt: prompt.clone(),
|
||||
name: optional_str(input, "name")?.map(ToString::to_string),
|
||||
model: optional_str(input, "model")?.map(ToString::to_string),
|
||||
model_provider: optional_str(input, "model_provider")?.map(ToString::to_string),
|
||||
model_provider_id: optional_str(input, "model_provider_id")?.map(ToString::to_string),
|
||||
|
||||
@@ -1088,6 +1088,7 @@ mod tests {
|
||||
id: id.to_string(),
|
||||
status,
|
||||
prompt_summary: "Fix task list output".to_string(),
|
||||
name: None,
|
||||
model: "deepseek-v4-pro".to_string(),
|
||||
model_provider: None,
|
||||
model_provider_id: None,
|
||||
|
||||
@@ -2318,6 +2318,7 @@ pub(crate) async fn apply_command_result(
|
||||
app.current_session_id = Some(owner_session_id.clone());
|
||||
let request = NewTaskRequest {
|
||||
prompt: prompt.clone(),
|
||||
name: None,
|
||||
model: Some(app.model.clone()),
|
||||
model_provider: Some(app.api_provider.as_str().to_string()),
|
||||
model_provider_id: Some(app.provider_identity_for_persistence().to_string()),
|
||||
|
||||
@@ -25570,6 +25570,7 @@ mod work_sidebar_projection_tests {
|
||||
id: id.to_string(),
|
||||
status,
|
||||
prompt_summary: format!("task {id}"),
|
||||
name: None,
|
||||
model: "deepseek-v4-flash".to_string(),
|
||||
model_provider: None,
|
||||
model_provider_id: None,
|
||||
|
||||
Reference in new issue
Block a user