Add await-signal molecule step type with backoff support (gt-l6ro3.3)

Adds Type and Backoff fields to MoleculeStep for patrol agents to implement
cost-saving await-signal patterns:

- Type field: "task" (default), "wait" (await-signal), etc.
- BackoffConfig: base interval, multiplier, max cap
- Parsing for "Type:" and "Backoff:" lines in step definitions
- Comprehensive tests for new parsing functionality

Step definition format:
  ## Step: await-signal
  Type: wait
  Backoff: base=30s, multiplier=2, max=10m

Agents interpret these declaratively, implementing backoff behavior at runtime.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-29 18:04:14 -08:00
parent 03775c6fc7
commit 0085353056
2 changed files with 229 additions and 6 deletions

View File

@@ -642,3 +642,148 @@ Needs: b`,
t.Error("ValidateMolecule() = nil, want error for cycle in subgraph")
}
}
func TestParseMoleculeSteps_WithType(t *testing.T) {
desc := `## Step: await-signal
Wait for a wake signal before proceeding.
Type: wait
## Step: check-reality
Check for work to do.
Type: task
Needs: await-signal
## Step: work
Do the actual work (default type).
Needs: check-reality`
steps, err := ParseMoleculeSteps(desc)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(steps) != 3 {
t.Fatalf("expected 3 steps, got %d", len(steps))
}
// await-signal has type wait
if steps[0].Type != "wait" {
t.Errorf("step[0].Type = %q, want wait", steps[0].Type)
}
// check-reality has explicit type task
if steps[1].Type != "task" {
t.Errorf("step[1].Type = %q, want task", steps[1].Type)
}
// work has no type specified (empty string, default)
if steps[2].Type != "" {
t.Errorf("step[2].Type = %q, want empty (default)", steps[2].Type)
}
}
func TestParseMoleculeSteps_WithBackoff(t *testing.T) {
desc := `## Step: await-signal
Wait for a wake signal with exponential backoff.
Type: wait
Backoff: base=30s, multiplier=2, max=10m
## Step: check-reality
Check for work.
Needs: await-signal`
steps, err := ParseMoleculeSteps(desc)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(steps) != 2 {
t.Fatalf("expected 2 steps, got %d", len(steps))
}
// await-signal has backoff config
if steps[0].Backoff == nil {
t.Fatal("step[0].Backoff is nil, want BackoffConfig")
}
if steps[0].Backoff.Base != "30s" {
t.Errorf("step[0].Backoff.Base = %q, want 30s", steps[0].Backoff.Base)
}
if steps[0].Backoff.Multiplier != 2 {
t.Errorf("step[0].Backoff.Multiplier = %d, want 2", steps[0].Backoff.Multiplier)
}
if steps[0].Backoff.Max != "10m" {
t.Errorf("step[0].Backoff.Max = %q, want 10m", steps[0].Backoff.Max)
}
// check-reality has no backoff
if steps[1].Backoff != nil {
t.Errorf("step[1].Backoff = %+v, want nil", steps[1].Backoff)
}
}
func TestParseMoleculeSteps_BackoffDefaultMultiplier(t *testing.T) {
desc := `## Step: wait-step
Simple wait.
Type: wait
Backoff: base=1m, max=30m`
steps, err := ParseMoleculeSteps(desc)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(steps) != 1 {
t.Fatalf("expected 1 step, got %d", len(steps))
}
if steps[0].Backoff == nil {
t.Fatal("step[0].Backoff is nil, want BackoffConfig")
}
// Default multiplier is 2
if steps[0].Backoff.Multiplier != 2 {
t.Errorf("step[0].Backoff.Multiplier = %d, want 2 (default)", steps[0].Backoff.Multiplier)
}
}
func TestParseMoleculeSteps_BackoffIncomplete(t *testing.T) {
desc := `## Step: bad-backoff
Missing base.
Backoff: multiplier=3, max=1h`
steps, err := ParseMoleculeSteps(desc)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(steps) != 1 {
t.Fatalf("expected 1 step, got %d", len(steps))
}
// Backoff without base should be nil
if steps[0].Backoff != nil {
t.Errorf("step[0].Backoff = %+v, want nil (missing base)", steps[0].Backoff)
}
}
func TestParseMoleculeSteps_TypeCaseInsensitive(t *testing.T) {
desc := `## Step: step1
First step.
TYPE: WAIT
## Step: step2
Second step.
type: Task
Needs: step1`
steps, err := ParseMoleculeSteps(desc)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(steps) != 2 {
t.Fatalf("expected 2 steps, got %d", len(steps))
}
// Type is normalized to lowercase
if steps[0].Type != "wait" {
t.Errorf("step[0].Type = %q, want wait", steps[0].Type)
}
if steps[1].Type != "task" {
t.Errorf("step[1].Type = %q, want task", steps[1].Type)
}
}