mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-11 01:51:34 -10:00
refactor(goal): delete the natural-language /goal prose parser (determinism audit batch 2, #3)
The host parsed ten prose phrasings plus a clause allow-list ("make it your
/goal to ...") into durable goals before the provider call. Per the founder
direction the model decides when a goal is useful: prose asks now reach the
model, which calls create_goal; the deterministic user path is the leading
/goals <objective> command, unchanged. Same philosophy as 4de9e9e28, which
deleted the Operate verb-list promotion.
Tests: tools::goal 29 passed 0 failed; engine goal suites (rewritten: prose
never activates, seeded-goal flows intact) 12 passed 0 failed. Clippy
--all-targets, fmt, dead-code budget clean.
This commit is contained in:
1 parent
72b028e5ee
commit
27ae676972
3 files changed
+64
-246
No files matched your search
@@ -40,8 +40,7 @@ use crate::route_runtime::{
|
||||
ResolvedRuntimeRoute, ValidatedRuntimeRoute, resolve_runtime_route_for_identity,
|
||||
};
|
||||
use crate::tools::goal::{
|
||||
GoalPauseReason, GoalSnapshot, GoalStatus, SharedGoalState, explicit_goal_directive,
|
||||
new_shared_goal_state,
|
||||
GoalPauseReason, GoalSnapshot, GoalStatus, SharedGoalState, new_shared_goal_state,
|
||||
};
|
||||
use crate::tools::plan::{SharedPlanState, new_shared_plan_state};
|
||||
use crate::tools::shell::{SharedShellManager, new_shared_shell_manager};
|
||||
@@ -4853,66 +4852,20 @@ impl Engine {
|
||||
if autonomous && self.cancel_token.is_cancelled() {
|
||||
return SendMessageOutcome::NotStarted { error: None };
|
||||
}
|
||||
let mut goal_objective = goal_objective;
|
||||
let mut goal_token_budget = goal_token_budget;
|
||||
let mut goal_status = goal_status;
|
||||
let initial_usage_owner = compaction.runtime_cost_owner.clone();
|
||||
|
||||
// A literal natural-language `/goal` declaration is control-plane
|
||||
// intent, not a suggestion that each provider may acknowledge or
|
||||
// ignore. Activate it through the same GoalState::create path as the
|
||||
// model-visible create_goal tool before constructing any provider
|
||||
// request. Only structurally external user input can authorize this;
|
||||
// runtime text, recalled memory, handoffs, and pasted multi-line
|
||||
// transcripts cannot create a goal.
|
||||
// Goals are created by the model (`create_goal`) or by the leading
|
||||
// `/goal <objective>` command; the host never infers one from
|
||||
// wording (docs/design/TUI_DECONSTRUCTION.md — founder clarification
|
||||
// 2026-09-09: the model decides when a goal is useful). The
|
||||
// natural-language `/goal` prose parser that used to recognize
|
||||
// "make it your /goal to ..." is gone with the #6290 rework — a
|
||||
// prose ask reaches the model, which calls `create_goal` when a goal
|
||||
// is actually useful.
|
||||
//
|
||||
// Goals are created by the model (`create_goal`) or by this literal
|
||||
// user declaration; the host never infers one from wording. The
|
||||
// verb-list promotion that used to turn ordinary Operate prompts into
|
||||
// goals is gone (docs/design/TUI_DECONSTRUCTION.md — founder
|
||||
// clarification 2026-09-09: the model decides when a goal is useful).
|
||||
// `GoalState::create` still refuses while an unfinished goal exists,
|
||||
// so a paused or blocked goal is never silently replaced.
|
||||
//
|
||||
// KV-cache effect: this only selects the already-existing volatile
|
||||
// <session_goal> contributor. It adds no new stable-prefix text.
|
||||
let goal_request = if provenance.can_authorize_work() {
|
||||
explicit_goal_directive(&content)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(directive) = goal_request {
|
||||
let result = self
|
||||
.config
|
||||
.goal_state
|
||||
.lock()
|
||||
.map_err(|_| "goal state lock poisoned".to_string())
|
||||
.and_then(|mut state| {
|
||||
state
|
||||
.create(directive.objective, None)
|
||||
.map_err(str::to_string)?;
|
||||
Ok(state.snapshot())
|
||||
});
|
||||
match result {
|
||||
Ok(snapshot) => {
|
||||
goal_objective.clone_from(&snapshot.objective);
|
||||
goal_token_budget = snapshot.token_budget;
|
||||
goal_status = GoalStatus::Active;
|
||||
// Publish before TurnStarted/provider dispatch so the TUI
|
||||
// and durable runtime host observe the real goal action,
|
||||
// even when this model would otherwise reply only in prose.
|
||||
let _ = self.tx_event.send(Event::GoalUpdated { snapshot }).await;
|
||||
}
|
||||
Err(error) => {
|
||||
let _ = self
|
||||
.tx_event
|
||||
.send(Event::status(format!(
|
||||
"Requested /goal was not created: {error}"
|
||||
)))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
// KV-cache effect: none. Goal state still flows through the existing
|
||||
// volatile <session_goal> contributor; nothing here touches the
|
||||
// stable prefix.
|
||||
|
||||
let effective_provider = route.identity.provider;
|
||||
let provider_identity = route.identity.key.clone();
|
||||
|
||||
@@ -3160,7 +3160,7 @@ fn goal_custom_route_config() -> Config {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn explicit_natural_goal_activates_before_provider_request() {
|
||||
async fn ordinary_prose_never_activates_a_goal() {
|
||||
let request_entered = std::sync::Arc::new(tokio::sync::Notify::new());
|
||||
let release_request = std::sync::Arc::new(tokio::sync::Notify::new());
|
||||
let model = std::sync::Arc::new(FirstRequestGatedGoalModelClient {
|
||||
@@ -3213,24 +3213,25 @@ async fn explicit_natural_goal_activates_before_provider_request() {
|
||||
.await
|
||||
.expect("send explicit natural goal turn");
|
||||
|
||||
let mut saw_goal_before_turn = false;
|
||||
// #6290 rework: the natural-language `/goal` prose parser is gone. The
|
||||
// same wording that used to activate a goal is now an ordinary turn;
|
||||
// only the model (`create_goal`) or the `/goal` command creates one.
|
||||
let mut saw_goal = false;
|
||||
loop {
|
||||
let event = tokio::time::timeout(model_turn_event_timeout(), async {
|
||||
handle.rx_event.write().await.recv().await
|
||||
})
|
||||
.await
|
||||
.expect("explicit goal event timeout")
|
||||
.expect("explicit goal event");
|
||||
.expect("prose goal event timeout")
|
||||
.expect("prose goal event");
|
||||
match event {
|
||||
Event::GoalUpdated { snapshot } => {
|
||||
assert_eq!(snapshot.objective.as_deref(), Some("solve navier stokes"));
|
||||
assert_eq!(snapshot.status, "active");
|
||||
saw_goal_before_turn = true;
|
||||
Event::GoalUpdated { .. } => {
|
||||
saw_goal = true;
|
||||
}
|
||||
Event::TurnStarted { .. } => {
|
||||
assert!(
|
||||
saw_goal_before_turn,
|
||||
"durable goal must be published before provider work starts"
|
||||
!saw_goal,
|
||||
"ordinary prose must not publish a goal before provider work starts"
|
||||
);
|
||||
break;
|
||||
}
|
||||
@@ -3241,29 +3242,15 @@ async fn explicit_natural_goal_activates_before_provider_request() {
|
||||
tokio::time::timeout(model_turn_event_timeout(), request_entered.notified())
|
||||
.await
|
||||
.expect("provider request was never entered");
|
||||
let snapshot = goal_state.lock().expect("goal lock").snapshot();
|
||||
assert_eq!(snapshot.objective.as_deref(), Some("solve navier stokes"));
|
||||
assert!(snapshot.is_active());
|
||||
|
||||
// Stop autonomous continuation after the one provider-boundary receipt.
|
||||
handle
|
||||
.send(Op::SetGoalStatus {
|
||||
goal_id: None,
|
||||
status: crate::tools::goal::GoalStatus::Paused,
|
||||
clear: false,
|
||||
})
|
||||
.await
|
||||
.expect("queue goal pause");
|
||||
release_request.notify_one();
|
||||
let _ = tokio::time::timeout(model_turn_event_timeout(), handle.get_session_snapshot())
|
||||
.await
|
||||
.expect("goal pause did not settle")
|
||||
.expect("post-goal session snapshot");
|
||||
.expect("turn did not settle")
|
||||
.expect("post-turn session snapshot");
|
||||
let snapshot = goal_state.lock().expect("goal lock").snapshot();
|
||||
assert_eq!(snapshot.objective.as_deref(), None);
|
||||
assert!(!snapshot.is_active());
|
||||
assert_eq!(model.calls.load(std::sync::atomic::Ordering::SeqCst), 1);
|
||||
assert_eq!(
|
||||
goal_state.lock().expect("goal lock").snapshot().status,
|
||||
"paused"
|
||||
);
|
||||
|
||||
handle.send(Op::Shutdown).await.expect("shutdown engine");
|
||||
run_task.await.expect("engine task");
|
||||
@@ -3395,11 +3382,16 @@ async fn operate_never_promotes_wording_to_a_goal() {
|
||||
assert!(!active);
|
||||
assert_eq!(contracts, 0, "Work never sees the Operate contract");
|
||||
|
||||
// An explicit declaration still creates one, through the same path.
|
||||
// #6290 rework: even an explicit-looking declaration is ordinary
|
||||
// prose now — the host never parses it, and the model decides goals
|
||||
// through `create_goal`.
|
||||
let (objective, active, contracts) =
|
||||
operate_goal_probe(AppMode::Operate, "Please set /goal to ship the release").await;
|
||||
assert_eq!(objective.as_deref(), Some("ship the release"));
|
||||
assert!(active, "an explicit declaration must create the goal");
|
||||
assert_eq!(
|
||||
objective, None,
|
||||
"prose asking for a goal must not create one host-side"
|
||||
);
|
||||
assert!(!active);
|
||||
assert_eq!(contracts, 1);
|
||||
}
|
||||
|
||||
@@ -3507,9 +3499,20 @@ async fn operate_contract_is_appended_once_and_an_existing_goal_is_never_replace
|
||||
|
||||
let first_objective =
|
||||
"Migrate the settings loader to the new config crate and keep the old keys readable";
|
||||
let first = format!("Please set /goal to {first_objective}");
|
||||
// #6290 rework: prose no longer creates goals, so the unfinished goal
|
||||
// this test needs is seeded directly — the same `GoalState::create` path
|
||||
// the `/goal` command and the model's `create_goal` tool use.
|
||||
goal_state
|
||||
.lock()
|
||||
.expect("goal lock")
|
||||
.create(first_objective.to_string(), None)
|
||||
.expect("seed unfinished goal");
|
||||
handle
|
||||
.send(send(&first, None, crate::tools::goal::GoalStatus::Active))
|
||||
.send(send(
|
||||
first_objective,
|
||||
Some(first_objective.to_string()),
|
||||
crate::tools::goal::GoalStatus::Active,
|
||||
))
|
||||
.await
|
||||
.expect("send first Operate turn");
|
||||
tokio::time::timeout(model_turn_event_timeout(), first_entered.notified())
|
||||
@@ -12372,10 +12375,10 @@ async fn operate_model_shell_uses_normal_approval_and_workspace_sandbox() {
|
||||
"\"finish_reason\":\"stop\"}]}\n\n",
|
||||
"data: [DONE]\n\n",
|
||||
);
|
||||
// Operate no longer infers a goal from wording, so this fixture declares
|
||||
// one explicitly; after the approved shell runs, the model seals it
|
||||
// through the same `update_goal` tool a live Operate turn uses, and only
|
||||
// then does the final "done" arrive.
|
||||
// The goal this fixture seals is seeded directly (prose no longer
|
||||
// creates goals since the #6290 rework); after the approved shell runs,
|
||||
// the model seals it through the same `update_goal` tool a live Operate
|
||||
// turn uses, and only then does the final "done" arrive.
|
||||
let goal_seal_marker = "goal-seal-receipt-0902";
|
||||
let goal_sse = concat!(
|
||||
"data: {\"id\":\"chatcmpl-operate-goal\",\"choices\":[{\"index\":0,\"delta\":{\"tool_calls\":[",
|
||||
@@ -12455,19 +12458,26 @@ async fn operate_model_shell_uses_normal_approval_and_workspace_sandbox() {
|
||||
},
|
||||
&api_config,
|
||||
);
|
||||
engine
|
||||
.config
|
||||
.goal_state
|
||||
.lock()
|
||||
.expect("goal lock")
|
||||
.create("write the requested local fixture".to_string(), None)
|
||||
.expect("seed fixture goal");
|
||||
let handle_for_approval = handle.clone();
|
||||
let run_task = tokio::spawn(engine.run());
|
||||
|
||||
handle
|
||||
.send(Op::SendMessage(TurnSpec {
|
||||
max_output_tokens: None,
|
||||
content: "Please set /goal to write the requested local fixture".to_string(),
|
||||
content: "Write the requested local fixture to the workspace".to_string(),
|
||||
images: Vec::new(),
|
||||
mode: AppMode::Operate,
|
||||
route: resolved_route_for_test(&api_config, crate::config::DEFAULT_TEXT_MODEL),
|
||||
compaction: Box::new(CompactionConfig::default()),
|
||||
initial_routed_usage: Box::default(),
|
||||
goal_objective: None,
|
||||
goal_objective: Some("write the requested local fixture".to_string()),
|
||||
goal_token_budget: None,
|
||||
goal_status: crate::tools::goal::GoalStatus::Active,
|
||||
reasoning_effort: None,
|
||||
@@ -12499,9 +12509,8 @@ async fn operate_model_shell_uses_normal_approval_and_workspace_sandbox() {
|
||||
Event::ApprovalRequired { id, tool_name, .. } => {
|
||||
saw_approval = true;
|
||||
assert_eq!(tool_name, "Bash");
|
||||
// No goal is created for this prompt: goals are model-decided
|
||||
// (`create_goal`), so there is no goal-continuation loop to
|
||||
// pause in this mock.
|
||||
// The seeded fixture goal is orthogonal to this gate:
|
||||
// Operate uses the normal approval flow either way.
|
||||
handle_for_approval
|
||||
.approve_tool_call(id)
|
||||
.await
|
||||
|
||||
@@ -44,116 +44,6 @@ pub fn new_shared_goal_state_from_snapshot(snapshot: &GoalSnapshot) -> SharedGoa
|
||||
Arc::new(Mutex::new(GoalState::from_snapshot(snapshot)))
|
||||
}
|
||||
|
||||
/// A goal declaration stated in ordinary user prose rather than as a leading
|
||||
/// `/goal <objective>` command.
|
||||
///
|
||||
/// This intentionally recognizes only a narrow, explicit `/goal` directive.
|
||||
/// Ordinary long-running requests are not silently promoted to goals, and a
|
||||
/// quoted multi-line transcript cannot authorize one through a later line.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ExplicitGoalDirective {
|
||||
pub objective: String,
|
||||
}
|
||||
|
||||
/// Extract an explicit natural-language `/goal` declaration from the user's
|
||||
/// first non-empty line.
|
||||
///
|
||||
/// The provider never participates in this decision. That makes an explicit
|
||||
/// declaration behave the same on DeepSeek, Zai, and every compatible route,
|
||||
/// while keeping ordinary tasks and discussion *about* `/goal` untouched.
|
||||
#[must_use]
|
||||
pub fn explicit_goal_directive(input: &str) -> Option<ExplicitGoalDirective> {
|
||||
let line = input.lines().find(|line| !line.trim().is_empty())?.trim();
|
||||
let lower = line.to_ascii_lowercase();
|
||||
|
||||
// Objective follows the directive.
|
||||
for pattern in [
|
||||
"make it your /goal to",
|
||||
"make this your /goal to",
|
||||
"make that your /goal to",
|
||||
"set your /goal to",
|
||||
"set my /goal to",
|
||||
"set the /goal to",
|
||||
"set /goal to",
|
||||
"your /goal is",
|
||||
"my /goal is",
|
||||
"the /goal is",
|
||||
] {
|
||||
let Some(index) = lower.find(pattern) else {
|
||||
continue;
|
||||
};
|
||||
if !is_direct_goal_clause(&lower[..index]) {
|
||||
continue;
|
||||
}
|
||||
let objective = normalize_explicit_goal_objective(&line[index + pattern.len()..])?;
|
||||
return Some(ExplicitGoalDirective { objective });
|
||||
}
|
||||
|
||||
// Objective precedes the marker: "make solving X your /goal".
|
||||
for marker in [" your /goal", " my /goal", " the /goal"] {
|
||||
let Some(marker_index) = lower.find(marker) else {
|
||||
continue;
|
||||
};
|
||||
let before_marker = &lower[..marker_index];
|
||||
let Some(make_index) = before_marker.rfind("make ") else {
|
||||
continue;
|
||||
};
|
||||
if !is_direct_goal_clause(&lower[..make_index]) {
|
||||
continue;
|
||||
}
|
||||
let objective =
|
||||
normalize_explicit_goal_objective(&line[make_index + "make ".len()..marker_index])?;
|
||||
if matches!(
|
||||
objective.to_ascii_lowercase().as_str(),
|
||||
"it" | "this" | "that"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
return Some(ExplicitGoalDirective { objective });
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn is_direct_goal_clause(prefix: &str) -> bool {
|
||||
let prefix = prefix.trim_end();
|
||||
if prefix.is_empty() {
|
||||
return true;
|
||||
}
|
||||
[
|
||||
"please",
|
||||
"and",
|
||||
"then",
|
||||
"also",
|
||||
"now",
|
||||
"you to",
|
||||
"need to",
|
||||
"can you",
|
||||
"could you",
|
||||
"would you",
|
||||
"can we",
|
||||
"could we",
|
||||
"should",
|
||||
"must",
|
||||
"-",
|
||||
";",
|
||||
":",
|
||||
",",
|
||||
]
|
||||
.iter()
|
||||
.any(|ending| prefix.ends_with(ending))
|
||||
}
|
||||
|
||||
fn normalize_explicit_goal_objective(raw: &str) -> Option<String> {
|
||||
let objective = raw
|
||||
.trim()
|
||||
.trim_start_matches([':', '-', '–', '—'])
|
||||
.trim()
|
||||
.trim_end_matches(['.', '!', '?'])
|
||||
.trim();
|
||||
(!objective.is_empty()).then(|| objective.to_string())
|
||||
}
|
||||
|
||||
/// Runtime status for a goal.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub enum GoalStatus {
|
||||
@@ -1301,40 +1191,6 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn explicit_goal_directive_extracts_user_requested_objective() {
|
||||
let request = explicit_goal_directive(
|
||||
"hello - take over and make it your /goal to solve navier stokes",
|
||||
)
|
||||
.expect("explicit /goal request");
|
||||
assert_eq!(request.objective, "solve navier stokes");
|
||||
|
||||
let request = explicit_goal_directive("Please make ship the release your /goal.")
|
||||
.expect("objective-before-marker request");
|
||||
assert_eq!(request.objective, "ship the release");
|
||||
|
||||
let request = explicit_goal_directive("Could you set /goal to repair provider routing?")
|
||||
.expect("set /goal request");
|
||||
assert_eq!(request.objective, "repair provider routing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_goal_directive_rejects_ordinary_or_quoted_goal_discussion() {
|
||||
for input in [
|
||||
"solve navier stokes",
|
||||
"why didn't you make it your /goal to solve navier stokes?",
|
||||
"what does /goal do?",
|
||||
"review the /goal implementation",
|
||||
"see this transcript where /goal was ignored:\nhello - take over and make it your /goal to solve navier stokes",
|
||||
] {
|
||||
assert_eq!(
|
||||
explicit_goal_directive(input),
|
||||
None,
|
||||
"must not activate from: {input}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_goal_rejects_objective_knob_instead_of_ignoring_it() {
|
||||
// #5123-class: `objective` used to return a success receipt with no
|
||||
|
||||
Reference in new issue
Block a user