mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-15 04:21:44 +00:00
Adopt Petri skill expansion boundaries
This commit is contained in:
parent
ac84eccfcc
commit
daf82929a0
3 changed files with 205 additions and 20 deletions
30
Cargo.lock
generated
30
Cargo.lock
generated
|
|
@ -5167,7 +5167,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
|||
[[package]]
|
||||
name = "petri-attractor-steps"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?branch=main#857a171bc984d578eeaedac574ff754431dc0002"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?branch=main#56ec3849ca693974ff778aa5ad4f9f9410d47d29"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"globset",
|
||||
|
|
@ -5198,7 +5198,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "petri-driver"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?branch=main#857a171bc984d578eeaedac574ff754431dc0002"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?branch=main#56ec3849ca693974ff778aa5ad4f9f9410d47d29"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"getrandom 0.3.4",
|
||||
|
|
@ -5218,7 +5218,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "petri-engine"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?branch=main#857a171bc984d578eeaedac574ff754431dc0002"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?branch=main#56ec3849ca693974ff778aa5ad4f9f9410d47d29"
|
||||
dependencies = [
|
||||
"petri-ir",
|
||||
"serde",
|
||||
|
|
@ -5230,7 +5230,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "petri-execution"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?branch=main#857a171bc984d578eeaedac574ff754431dc0002"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?branch=main#56ec3849ca693974ff778aa5ad4f9f9410d47d29"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"petri-driver",
|
||||
|
|
@ -5254,7 +5254,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "petri-executor"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?branch=main#857a171bc984d578eeaedac574ff754431dc0002"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?branch=main#56ec3849ca693974ff778aa5ad4f9f9410d47d29"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"libc",
|
||||
|
|
@ -5269,7 +5269,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "petri-executor-sandbox"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?branch=main#857a171bc984d578eeaedac574ff754431dc0002"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?branch=main#56ec3849ca693974ff778aa5ad4f9f9410d47d29"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"petri-executor",
|
||||
|
|
@ -5291,7 +5291,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "petri-frontend"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?branch=main#857a171bc984d578eeaedac574ff754431dc0002"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?branch=main#56ec3849ca693974ff778aa5ad4f9f9410d47d29"
|
||||
dependencies = [
|
||||
"marked-yaml",
|
||||
"petri-ir",
|
||||
|
|
@ -5305,7 +5305,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "petri-frontend-attractor"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?branch=main#857a171bc984d578eeaedac574ff754431dc0002"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?branch=main#56ec3849ca693974ff778aa5ad4f9f9410d47d29"
|
||||
dependencies = [
|
||||
"minijinja",
|
||||
"petri-frontend",
|
||||
|
|
@ -5322,7 +5322,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "petri-frontend-fabro"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?branch=main#857a171bc984d578eeaedac574ff754431dc0002"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?branch=main#56ec3849ca693974ff778aa5ad4f9f9410d47d29"
|
||||
dependencies = [
|
||||
"petri-frontend",
|
||||
"petri-frontend-attractor",
|
||||
|
|
@ -5338,7 +5338,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "petri-frontend-native"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?branch=main#857a171bc984d578eeaedac574ff754431dc0002"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?branch=main#56ec3849ca693974ff778aa5ad4f9f9410d47d29"
|
||||
dependencies = [
|
||||
"petri-frontend",
|
||||
"petri-ir",
|
||||
|
|
@ -5349,7 +5349,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "petri-ir"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?branch=main#857a171bc984d578eeaedac574ff754431dc0002"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?branch=main#56ec3849ca693974ff778aa5ad4f9f9410d47d29"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
|
|
@ -5362,7 +5362,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "petri-runtime"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?branch=main#857a171bc984d578eeaedac574ff754431dc0002"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?branch=main#56ec3849ca693974ff778aa5ad4f9f9410d47d29"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"petri-driver",
|
||||
|
|
@ -5383,7 +5383,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "petri-steps"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?branch=main#857a171bc984d578eeaedac574ff754431dc0002"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?branch=main#56ec3849ca693974ff778aa5ad4f9f9410d47d29"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"petri-executor",
|
||||
|
|
@ -5399,7 +5399,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "petri-store"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?branch=main#857a171bc984d578eeaedac574ff754431dc0002"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?branch=main#56ec3849ca693974ff778aa5ad4f9f9410d47d29"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"getrandom 0.3.4",
|
||||
|
|
@ -5414,7 +5414,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "petri-testkit"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?branch=main#857a171bc984d578eeaedac574ff754431dc0002"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?branch=main#56ec3849ca693974ff778aa5ad4f9f9410d47d29"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"petri-driver",
|
||||
|
|
|
|||
|
|
@ -31,20 +31,24 @@ Skills can be invoked in two ways.
|
|||
|
||||
### Slash syntax (user input)
|
||||
|
||||
Users can reference a skill with a `/` prefix in their input:
|
||||
On the native agent backend, start authored input with `/skill-name` to invoke a skill:
|
||||
|
||||
```
|
||||
/commit fix the off-by-one error in pagination
|
||||
```
|
||||
|
||||
Fabro detects the `/commit` token, finds the matching skill, and expands the input by replacing the skill reference with the full template. The remaining text (`fix the off-by-one error in pagination`) is substituted into the `{{user_input}}` placeholder.
|
||||
Fabro detects the leading `/commit` command, finds the matching skill, and expands the authored input using its template. The remaining text (`fix the off-by-one error in pagination`) is substituted into the `{{user_input}}` placeholder. A template without that placeholder replaces the authored input.
|
||||
|
||||
Rules for slash syntax:
|
||||
- The `/` must be preceded by whitespace or be at the start of the input
|
||||
- The command must be at the start of authored input, after any leading whitespace
|
||||
- The skill name must be followed by whitespace or end of input
|
||||
- Only one skill reference per input is allowed
|
||||
- Later slash tokens are ordinary arguments or text: `/commit mention /review` invokes only `commit`, while `Please run /commit` invokes no skill
|
||||
- File paths like `/usr/bin/bash` are not matched (the character after `/` must be a lowercase letter, and the name cannot contain `/`)
|
||||
|
||||
For native workflow agent stages, only the authored node prompt is expanded. Workflow preambles, prior-stage context, `for_each` item data, and output contracts remain outside expansion and are preserved even when the template has no `{{user_input}}` placeholder. For example, prior-stage text mentioning `/tmp` or `/commit` stays literal.
|
||||
|
||||
User-delivered follow-ups also support a leading command. Generated output-repair turns bypass expansion. ACP agents receive the assembled workflow input and own any command interpretation; one-shot prompt nodes do not expand skills.
|
||||
|
||||
### use_skill tool (agent-initiated)
|
||||
|
||||
When skills are available, Fabro adds a `use_skill` tool to the agent's toolset and lists the available skills in the system prompt:
|
||||
|
|
@ -69,5 +73,5 @@ Returns the skill's template text.
|
|||
|
||||
1. **Discovery** -- On session startup, Fabro scans the skill directories and parses all valid `SKILL.md` files
|
||||
2. **Registration** -- If any skills are found, the `use_skill` tool is registered and the skill list is added to the system prompt
|
||||
3. **Expansion** -- When user input contains a `/skill-name` reference, Fabro expands it before sending to the LLM
|
||||
3. **Expansion** -- On the native agent backend, a leading `/skill-name` command in authored input is expanded before sending to the LLM. Leading whitespace is allowed; later slash tokens stay literal. Workflow context and output requirements remain outside expansion, and generated output-repair turns bypass it
|
||||
4. **Tool use** -- The agent can also call `use_skill` at any time to load a skill's template mid-conversation
|
||||
|
|
|
|||
181
lib/components/fabro-petri/tests/skills.rs
Normal file
181
lib/components/fabro-petri/tests/skills.rs
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
//! Skills discovered through Fabro's runtime expand only authored native input.
|
||||
|
||||
mod support;
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use fabro_llm::test_support::{self, ScriptedAdapter};
|
||||
use fabro_petri::check::Launch;
|
||||
use fabro_petri::engine::{self, RunStatus};
|
||||
use fabro_petri::runtime::RuntimeSpec;
|
||||
use lithos_llm::Client;
|
||||
use lithos_llm::catalog::ProviderId;
|
||||
use lithos_llm::middleware::{Call, Observer, ObserverMiddleware};
|
||||
use lithos_llm::types::{ContentPart, Request};
|
||||
use petri_store::MemoryRunStore;
|
||||
use support::{Silent, no_questions, run_request};
|
||||
use tokio::fs;
|
||||
|
||||
const MODEL: &str = "gpt-5.4";
|
||||
const TEMPLATE: &str = "COMMIT TEMPLATE SENTINEL";
|
||||
const PRIOR: &str = "smoke-tested via a /tmp build; reviewer mentioned /commit";
|
||||
|
||||
struct Requests(Arc<Mutex<Vec<Request>>>);
|
||||
|
||||
impl Observer for Requests {
|
||||
fn on_start(&self, call: &Call) {
|
||||
self.0
|
||||
.lock()
|
||||
.expect("request capture lock")
|
||||
.push(call.request().clone());
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_native(workflow: &str, responses: &[&str]) -> Vec<Request> {
|
||||
let root = tempfile::tempdir().expect("temporary runtime home");
|
||||
let home = root.path().join("fabro-home");
|
||||
let skill = home.join("skills/commit");
|
||||
fs::create_dir_all(&skill).await.expect("skill directory");
|
||||
// No placeholder: expansion must replace only the authored input.
|
||||
fs::write(
|
||||
skill.join("SKILL.md"),
|
||||
format!("---\nname: commit\ndescription: Test commit instructions\n---\n{TEMPLATE}\n"),
|
||||
)
|
||||
.await
|
||||
.expect("skill template");
|
||||
let requests = Arc::new(Mutex::new(Vec::new()));
|
||||
let client = Client::builder()
|
||||
.catalog(test_support::test_catalog())
|
||||
.adapter(
|
||||
ProviderId::new("openai"),
|
||||
ScriptedAdapter::new(
|
||||
responses
|
||||
.iter()
|
||||
.map(|text| test_support::text_response("openai", MODEL, text))
|
||||
.collect(),
|
||||
),
|
||||
)
|
||||
.middleware(ObserverMiddleware::new(Requests(Arc::clone(&requests))))
|
||||
.build()
|
||||
.expect("scripted model client")
|
||||
.client;
|
||||
let runtime = RuntimeSpec {
|
||||
model_client: Some(client),
|
||||
fabro_home: Some(home),
|
||||
..RuntimeSpec::default()
|
||||
};
|
||||
let graphs = support::admit(
|
||||
&[
|
||||
("workflow.fabro", workflow),
|
||||
("workflow.toml", support::SETTINGS),
|
||||
],
|
||||
Launch {
|
||||
default_model: Some(format!("openai/{MODEL}")),
|
||||
..Launch::default()
|
||||
},
|
||||
&runtime,
|
||||
);
|
||||
let outcome = engine::run(run_request(
|
||||
"skills",
|
||||
&root.path().join("run"),
|
||||
graphs,
|
||||
Arc::new(MemoryRunStore::new()),
|
||||
runtime,
|
||||
no_questions(Arc::new(Silent)),
|
||||
))
|
||||
.await
|
||||
.expect("workflow run settles");
|
||||
assert_eq!(outcome.status, RunStatus::Success, "{outcome:?}");
|
||||
let requests = requests.lock().expect("request capture lock").clone();
|
||||
for request in &requests {
|
||||
for message in request.messages() {
|
||||
assert!(
|
||||
message
|
||||
.content()
|
||||
.iter()
|
||||
.all(|part| !matches!(part, ContentPart::Text { text } if text.is_empty())),
|
||||
"absent segments must not become invalid empty text blocks: {message:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
requests
|
||||
}
|
||||
|
||||
fn user_text(request: &Request) -> String {
|
||||
request
|
||||
.messages()
|
||||
.last()
|
||||
.expect("user input message")
|
||||
.content()
|
||||
.iter()
|
||||
.filter_map(|part| match part {
|
||||
ContentPart::Text { text } => Some(text.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn authored_skill_commands_preserve_workflow_context_and_output_requirements() {
|
||||
for (authored, expands) in [
|
||||
(" /commit authored arguments /tmp /commit", true),
|
||||
("Address the feedback about /commit and /tmp", false),
|
||||
] {
|
||||
let workflow = format!(
|
||||
r#"digraph Skills {{
|
||||
graph [backend="api", goal="Preserve workflow context", default_max_retries=0]
|
||||
start [shape=Mdiamond]
|
||||
prior [prompt="Report the previous result"]
|
||||
task [prompt="{authored}", fidelity="summary:high", output_schema="routing", output_retries=0]
|
||||
exit [shape=Msquare]
|
||||
start -> prior -> task -> exit
|
||||
}}"#
|
||||
);
|
||||
let requests = run_native(&workflow, &[PRIOR, r#"{"outcome":"succeeded"}"#]).await;
|
||||
assert_eq!(requests.len(), 2);
|
||||
let sent = user_text(&requests[1]);
|
||||
assert!(sent.contains("Preserve workflow context"), "{sent}");
|
||||
assert!(sent.contains(PRIOR), "{sent}");
|
||||
assert!(sent.contains("Fabro final-output contract"), "{sent}");
|
||||
assert_eq!(sent.contains(TEMPLATE), expands, "{sent}");
|
||||
if expands {
|
||||
assert!(!sent.contains(authored), "{sent}");
|
||||
assert!(sent.find(PRIOR) < sent.find(TEMPLATE), "{sent}");
|
||||
assert!(
|
||||
sent.find(TEMPLATE) < sent.find("Fabro final-output contract"),
|
||||
"{sent}"
|
||||
);
|
||||
} else {
|
||||
assert!(sent.contains(authored), "{sent}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn absent_native_prompt_segments_do_not_emit_empty_model_text_blocks() {
|
||||
for (fidelity, authored) in [
|
||||
("full", "Say done"),
|
||||
("compact", "Say done"),
|
||||
("compact", ""),
|
||||
] {
|
||||
let workflow = format!(
|
||||
r#"digraph Segments {{
|
||||
graph [backend="api", goal="Keep the nonempty preamble", default_max_retries=0]
|
||||
start [shape=Mdiamond]
|
||||
task [prompt="{authored}", fidelity="{fidelity}"]
|
||||
exit [shape=Msquare]
|
||||
start -> task -> exit
|
||||
}}"#
|
||||
);
|
||||
let requests = run_native(&workflow, &["done"]).await;
|
||||
assert_eq!(requests.len(), 1);
|
||||
let sent = user_text(&requests[0]);
|
||||
if fidelity == "full" {
|
||||
assert_eq!(sent, authored);
|
||||
} else {
|
||||
assert!(sent.contains("Keep the nonempty preamble"), "{sent}");
|
||||
assert!(sent.contains(authored), "{sent}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue