-
Notifications
You must be signed in to change notification settings - Fork 4k
fix(agent): user's model pick wins over orchestrator coding pin; stop offering fleet tools the parent lacks #6372
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ba70819
613ad69
34fba84
8c1cf9a
8534173
4982888
a50df86
2daac34
8ba9335
1f4d8e8
245abbb
96767a4
1ea6772
d825c41
0a2155a
1717945
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,114 @@ | ||||||||||||||
| //! Which fleet-control tools a parent agent can actually call. | ||||||||||||||
| //! | ||||||||||||||
| //! The `[async_subagent_ref]` envelope and the ambient `[active_subagents]` | ||||||||||||||
| //! roster used to hard-code a full fleet vocabulary — `wait_subagent`, | ||||||||||||||
| //! `steer_subagent`, `wait_loop`, `close_subagent` — while the orchestrator's | ||||||||||||||
| //! definition deliberately dropped most of it (#5701: a sub-agent result is | ||||||||||||||
| //! delivered back automatically on a later turn, so nothing needs to block). | ||||||||||||||
| //! The model was told to call tools it did not have, spent an iteration | ||||||||||||||
| //! reasoning about the mismatch, and improvised (`shell echo "waiting for | ||||||||||||||
| //! subagent"`). Every delegation paid a full extra model call for nothing. | ||||||||||||||
| //! | ||||||||||||||
| //! This module reads the parent's definition once per render and answers | ||||||||||||||
| //! "does this parent see tool X?", so both texts only ever name tools that | ||||||||||||||
| //! are in the caller's belt. | ||||||||||||||
|
|
||||||||||||||
| use crate::agent::harness::definition::{AgentDefinitionRegistry, ToolScope}; | ||||||||||||||
|
|
||||||||||||||
| /// The fleet-control tools whose availability shapes the delegation texts. | ||||||||||||||
| const FLEET_TOOLS: &[&str] = &[ | ||||||||||||||
| "steer_subagent", | ||||||||||||||
| "wait_subagent", | ||||||||||||||
| "wait", | ||||||||||||||
| "wait_loop", | ||||||||||||||
| "close_subagent", | ||||||||||||||
| "continue_subagent", | ||||||||||||||
| "list_subagents", | ||||||||||||||
| ]; | ||||||||||||||
|
|
||||||||||||||
| /// The subset of [`FLEET_TOOLS`] a given parent definition exposes. | ||||||||||||||
| #[derive(Debug, Clone, PartialEq, Eq)] | ||||||||||||||
| pub(crate) struct FleetToolSet { | ||||||||||||||
| available: Vec<&'static str>, | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| impl FleetToolSet { | ||||||||||||||
| /// Every fleet tool — the pre-#5701 assumption, used when the parent's | ||||||||||||||
| /// definition cannot be resolved so the texts degrade to their old shape | ||||||||||||||
| /// rather than to silence. | ||||||||||||||
| pub(crate) fn all() -> Self { | ||||||||||||||
| Self { | ||||||||||||||
| available: FLEET_TOOLS.to_vec(), | ||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /// Resolve the set for `agent_definition_id` from the global registry. | ||||||||||||||
| pub(crate) fn for_parent(agent_definition_id: &str) -> Self { | ||||||||||||||
| let Some(registry) = AgentDefinitionRegistry::global() else { | ||||||||||||||
| return Self::all(); | ||||||||||||||
| }; | ||||||||||||||
| let Some(definition) = registry.get(agent_definition_id) else { | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not advertise the full fleet for unknown parents When the registry exists but [RULE] tool-availability-mismatch · |
||||||||||||||
| log::debug!( | ||||||||||||||
| "[fleet_tools] parent definition '{}' not in registry; assuming full fleet vocabulary", | ||||||||||||||
| agent_definition_id | ||||||||||||||
| ); | ||||||||||||||
| return Self::all(); | ||||||||||||||
| }; | ||||||||||||||
| Self::from_scope(&definition.tools, &definition.disallowed_tools) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /// Resolve the set from a turn's *effective* visible tool names — the | ||||||||||||||
| /// live, already-filtered membership set (hides, named restrictions, and | ||||||||||||||
| /// policy narrowing all applied), as opposed to [`Self::for_parent`]'s | ||||||||||||||
| /// static read of the parent's registered definition. Prefer this | ||||||||||||||
| /// whenever the caller already has that snapshot: a hide or restriction | ||||||||||||||
| /// applied mid-session can narrow a turn's real tool surface below what | ||||||||||||||
| /// the definition alone would suggest, and offering a control absent | ||||||||||||||
| /// from this snapshot invites a denied tool call. | ||||||||||||||
| pub(crate) fn from_visible_tool_names(names: &std::collections::HashSet<String>) -> Self { | ||||||||||||||
| let available = FLEET_TOOLS | ||||||||||||||
| .iter() | ||||||||||||||
| .copied() | ||||||||||||||
| .filter(|name| names.contains(*name)) | ||||||||||||||
| .collect(); | ||||||||||||||
| Self { available } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /// Derive the set from a definition's tool scope and denylist. A | ||||||||||||||
| /// `Named` scope exposes exactly the fleet tools it lists; `Wildcard` | ||||||||||||||
| /// exposes all of them. `disallowed_tools` (exact or trailing-`*` | ||||||||||||||
| /// prefix) removes entries from either. | ||||||||||||||
| pub(crate) fn from_scope(scope: &ToolScope, disallowed: &[String]) -> Self { | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Constrain wildcard fleet tools to the actual parent belt The [RULE] tool-availability-mismatch · |
||||||||||||||
| let denied = |name: &str| { | ||||||||||||||
| disallowed | ||||||||||||||
| .iter() | ||||||||||||||
| .any(|entry| match entry.strip_suffix('*') { | ||||||||||||||
| Some(prefix) => name.starts_with(prefix), | ||||||||||||||
| None => entry == name, | ||||||||||||||
| }) | ||||||||||||||
| }; | ||||||||||||||
| let available = FLEET_TOOLS | ||||||||||||||
| .iter() | ||||||||||||||
| .copied() | ||||||||||||||
| .filter(|name| match scope { | ||||||||||||||
| ToolScope::Wildcard => true, | ||||||||||||||
| ToolScope::Named(named) => named.iter().any(|n| n == name), | ||||||||||||||
| }) | ||||||||||||||
| .filter(|name| !denied(name)) | ||||||||||||||
| .collect(); | ||||||||||||||
| Self { available } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| pub(crate) fn has(&self, tool: &str) -> bool { | ||||||||||||||
| self.available.contains(&tool) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| /// Whether the parent can block on or poll a worker at all. | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Treat delayed wait tools as polling capabilities
[RULE] incomplete-capability-check · |
||||||||||||||
| pub(crate) fn can_wait(&self) -> bool { | ||||||||||||||
| self.has("wait_subagent") | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+107
to
+109
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Treat delayed wait tools as polling capabilities
Suggested change
[RULE] incomplete-capability-check · |
||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| #[cfg(test)] | ||||||||||||||
| #[path = "fleet_tools_tests.rs"] | ||||||||||||||
| mod tests; | ||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| use super::FleetToolSet; | ||
| use crate::agent::harness::definition::ToolScope; | ||
|
|
||
| fn named(tools: &[&str]) -> ToolScope { | ||
| ToolScope::Named(tools.iter().map(|t| t.to_string()).collect()) | ||
| } | ||
|
|
||
| #[test] | ||
| fn named_scope_exposes_only_listed_fleet_tools() { | ||
| let set = FleetToolSet::from_scope( | ||
| &named(&[ | ||
| "spawn_async_subagent", | ||
| "list_subagents", | ||
| "continue_subagent", | ||
| "shell", | ||
| ]), | ||
| &[], | ||
| ); | ||
| assert!(set.has("list_subagents")); | ||
| assert!(set.has("continue_subagent")); | ||
| assert!(!set.has("wait_subagent")); | ||
| assert!(!set.has("steer_subagent")); | ||
| assert!(!set.has("wait_loop")); | ||
| assert!(!set.can_wait()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn wildcard_scope_exposes_every_fleet_tool_minus_denylist() { | ||
| let set = FleetToolSet::from_scope(&ToolScope::Wildcard, &["wait*".to_string()]); | ||
| assert!(set.has("steer_subagent")); | ||
| assert!(set.has("close_subagent")); | ||
| assert!(!set.has("wait")); | ||
| assert!(!set.has("wait_loop")); | ||
| assert!(!set.has("wait_subagent")); | ||
| assert!(!set.can_wait()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn all_is_the_full_vocabulary() { | ||
| let set = FleetToolSet::all(); | ||
| for tool in [ | ||
| "steer_subagent", | ||
| "wait_subagent", | ||
| "wait", | ||
| "wait_loop", | ||
| "close_subagent", | ||
| "continue_subagent", | ||
| "list_subagents", | ||
| ] { | ||
| assert!(set.has(tool), "{tool}"); | ||
| } | ||
| assert!(set.can_wait()); | ||
| } | ||
|
|
||
| /// The shipped orchestrator definition is the case that motivated this | ||
| /// module: it must not be told about wait/steer/close tools. | ||
| #[test] | ||
| fn builtin_orchestrator_has_no_wait_or_steer() { | ||
| let registry = crate::agent::harness::definition::AgentDefinitionRegistry::builtins_only(); | ||
| let def = registry.get("orchestrator").expect("built-in orchestrator"); | ||
| let set = FleetToolSet::from_scope(&def.tools, &def.disallowed_tools); | ||
| assert!(set.has("list_subagents")); | ||
| assert!(set.has("continue_subagent")); | ||
| assert!(!set.can_wait()); | ||
| assert!(!set.has("steer_subagent")); | ||
| assert!(!set.has("close_subagent")); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wire FleetToolSet into the fleet prompt renderers
This pull request adds the resolver but does not connect it to either the
[async_subagent_ref]envelope or the[active_subagents]roster. The production prompts therefore continue using the hard-coded fleet vocabulary, so agents can still be instructed to call controls absent from their tool surface. Pass the effectiveFleetToolSetinto both renderers and use it when generating their guidance.[RULE] missing-feature-wiring ·