use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Tool {
Send,
Peers,
Report,
Help,
MemoryRead,
MemoryWrite,
Learn,
LogbookAppend,
Status,
Wait,
}
impl Tool {
pub const ALL: [Self; 10] = [
Self::Send,
Self::Peers,
Self::Report,
Self::Help,
Self::MemoryRead,
Self::MemoryWrite,
Self::Learn,
Self::LogbookAppend,
Self::Status,
Self::Wait,
];
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Send => "layover_send",
Self::Peers => "layover_peers",
Self::Report => "layover_report",
Self::Help => "layover_help",
Self::MemoryRead => "layover_memory_read",
Self::MemoryWrite => "layover_memory_write",
Self::Learn => "layover_learn",
Self::LogbookAppend => "layover_logbook_append",
Self::Status => "layover_status",
Self::Wait => "layover_wait",
}
}
#[must_use]
pub const fn description(self) -> &'static str {
match self {
Self::Send => {
"Send work to another agent. This is the only way work moves, and sending is what \
starts the agent you send to -- there is no separate spawn. You may only send to \
agents `layover_peers` lists."
}
Self::Peers => {
"List the agents you may send to and what each one is for. Call this before \
deciding where work goes rather than guessing at names."
}
Self::Report => {
"Say what you concluded. One headline, then the detail. Write it for somebody who \
was not watching and will read only the headline -- this is the account of your \
run that survives."
}
Self::Help => {
"Say that something is in the way and you could not get past it. Use this instead \
of producing a plausible answer you do not believe: a wrong answer nobody flags \
travels downstream, and that is the failure this exists to prevent."
}
Self::MemoryRead => {
"Read your own notes in full. Every run starts fresh, so this is the only thing \
you remember. The most recent part is already in your instructions."
}
Self::MemoryWrite => {
"Add to your own notes, for future runs of you. Nothing else carries over, so \
anything worth remembering has to be written here deliberately."
}
Self::Learn => {
"Propose something future runs of you should know. It applies immediately and \
lapses unless later runs arrive at it independently."
}
Self::LogbookAppend => {
"Add to the factory's shared memory, which every agent can read. For things the \
whole factory needs, not for your own notes."
}
Self::Status => {
"Ask what this chain has left: how many more messages it may send, and how much \
budget remains. Worth checking before fanning out to several agents."
}
Self::Wait => {
"Set this work down and have it picked up later, when something you are waiting \
for has happened. Nothing is kept running in the meantime."
}
}
}
#[must_use]
pub fn from_name(name: &str) -> Option<Self> {
Self::ALL.into_iter().find(|tool| tool.name() == name)
}
#[must_use]
pub const fn writes(self) -> bool {
matches!(
self,
Self::Send | Self::MemoryWrite | Self::LogbookAppend | Self::Learn | Self::Wait
)
}
}
impl fmt::Display for Tool {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
#[must_use]
pub fn unknown_tools_in(text: &str) -> Vec<String> {
let mut found = Vec::new();
for (index, _) in text.match_indices("layover_") {
let rest = &text[index..];
let end = rest
.find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
.unwrap_or(rest.len());
let name = &rest[..end];
if Tool::from_name(name).is_none() && !found.iter().any(|seen| seen == name) {
found.push(name.to_owned());
}
}
found.sort();
found
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_tool_has_a_distinct_name() {
let mut names: Vec<_> = Tool::ALL.iter().map(|tool| tool.name()).collect();
names.sort_unstable();
let before = names.len();
names.dedup();
assert_eq!(names.len(), before, "two tools answer to the same name");
}
#[test]
fn every_name_round_trips() {
for tool in Tool::ALL {
assert_eq!(Tool::from_name(tool.name()), Some(tool));
}
}
#[test]
fn every_tool_is_named_for_layover() {
for tool in Tool::ALL {
assert!(tool.name().starts_with("layover_"), "{}", tool.name());
}
}
#[test]
fn every_tool_says_what_it_is_for() {
for tool in Tool::ALL {
assert!(
tool.description().len() > 60,
"`{}` needs a description an agent can act on",
tool.name()
);
}
}
#[test]
fn there_is_no_spawn_tool() {
assert_eq!(Tool::from_name("layover_spawn"), None);
}
#[test]
fn a_prompt_naming_a_tool_that_does_not_exist_is_caught() {
let prompt = "Investigate, then call layover_publish to ship it.";
assert_eq!(unknown_tools_in(prompt), vec!["layover_publish"]);
}
#[test]
fn a_prompt_naming_real_tools_is_clean() {
let prompt = "Call layover_peers, then layover_send. If stuck, layover_help.";
assert!(unknown_tools_in(prompt).is_empty());
}
#[test]
fn the_same_wrong_name_twice_is_reported_once() {
let prompt = "use layover_ship. then layover_ship again.";
assert_eq!(unknown_tools_in(prompt), vec!["layover_ship"]);
}
#[test]
fn a_tool_name_followed_by_punctuation_is_still_recognised() {
for prompt in [
"call layover_send(...)",
"call `layover_send`",
"call layover_send.",
"call layover_send, then wait",
] {
assert!(unknown_tools_in(prompt).is_empty(), "{prompt}");
}
}
#[test]
fn a_read_only_agent_can_be_told_which_tools_would_change_things() {
assert!(Tool::Send.writes());
assert!(Tool::MemoryWrite.writes());
assert!(!Tool::Peers.writes());
assert!(!Tool::MemoryRead.writes());
assert!(
!Tool::Report.writes(),
"a report is an account of a run, not an effect on anything else"
);
}
}