Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions pkg/script/grammar.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Package script — grammar.go is the discovery surface: a single entry
// point a front end calls to learn the complete grammar without knowing
// anything about how it is built or what is in it.
//
// The point is decoupling. A front end (loom, a CLI, anything) should not
// name a particular registry, hardcode a verb list, or change when the
// grammar grows. It calls Grammar(), pipes the result into Translate, and
// forwards results — never inspecting the contents. When AgentScript adds
// verbs, restructures registries, or changes the grammar, the front end
// reflects it automatically, because it fetches the grammar rather than
// embedding knowledge of it.
//
// AgentScript is the single source of truth for the grammar; the front
// end is a dumb pipe that asks "what can be done?" and forwards the
// answer.
package script

import "github.com/vinodhalaharvi/agentscript/pkg/script/registry"

// GrammarInfo is the self-describing result of discovery. It carries the
// registry a caller passes straight to Translate/Compile (without
// inspecting it) plus advisory, human-readable metadata that tooling
// (docs, a CLI, a help command) can surface. A front end that only wants
// to translate prose uses Registry and ignores the rest.
type GrammarInfo struct {
// Registry is the complete builtin catalog. Pass it to Translate and
// Compile. Callers are not expected to inspect it.
Registry *registry.Registry

// Verbs is the advisory list of available verb names (sorted),
// derived from the registry. For tooling/help surfaces; the
// authoritative validation always happens in Resolve against
// Registry.
Verbs []string

// Operators describes the composition operators the grammar supports,
// for help/discovery surfaces.
Operators []OperatorInfo

// Backends lists the execution backends the grammar can target.
Backends []string
}

// OperatorInfo describes a composition operator for discovery surfaces.
type OperatorInfo struct {
Symbol string
Name string
Desc string
}

// Grammar returns the complete grammar a front end can use: the full
// builtin catalog plus self-describing metadata. This is THE discovery
// entry point — a front end calls it, passes GrammarInfo.Registry into
// Translate, and never needs to know which registry backs it or when it
// changes.
//
// It is backed by CompleteRegistry (every historical verb, availability
// decided per-backend at Resolve), so discovery always reflects the full
// current vocabulary.
func Grammar() GrammarInfo {
reg := CompleteRegistry()
return GrammarInfo{
Registry: reg,
Verbs: reg.Names(),
Operators: []OperatorInfo{
{Symbol: ">=>", Name: "pipe", Desc: "sequential composition: left output feeds the right"},
{Symbol: "<*>", Name: "fanout", Desc: "parallel fanout: branches run on the same input"},
},
Backends: []string{"memory", "temporal"},
}
}
82 changes: 82 additions & 0 deletions pkg/script/grammar_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package script_test

import (
"context"
"errors"
"sort"
"testing"

"github.com/vinodhalaharvi/agentscript/pkg/script"
)

func TestGrammar_ReturnsCompleteVocabulary(t *testing.T) {
g := script.Grammar()
if g.Registry == nil {
t.Fatal("Grammar().Registry must not be nil")
}
if len(g.Verbs) < 100 {
t.Errorf("Grammar should expose the full vocabulary; got %d verbs", len(g.Verbs))
}
if !sort.StringsAreSorted(g.Verbs) {
t.Error("Grammar().Verbs should be sorted")
}
want := map[string]bool{"echo": false, "hf_summarize": false, "mcp_agent": false, "perplexity": false}
for _, v := range g.Verbs {
if _, ok := want[v]; ok {
want[v] = true
}
}
for v, found := range want {
if !found {
t.Errorf("Grammar should advertise %q", v)
}
}
}

func TestGrammar_DescribesOperatorsAndBackends(t *testing.T) {
g := script.Grammar()
syms := map[string]bool{}
for _, op := range g.Operators {
syms[op.Symbol] = true
}
if !syms[">=>"] || !syms["<*>"] {
t.Errorf("operators should include >=> and <*>, got %v", syms)
}
if len(g.Backends) != 2 {
t.Errorf("expected memory+temporal backends, got %v", g.Backends)
}
}

func TestGrammar_DiscoveryDrivenTranslateAndCompile(t *testing.T) {
g := script.Grammar()
llm := func(_ context.Context, _, _ string) (string, error) {
return `temporal static ( echo "hi" )`, nil
}
src, err := script.TranslateGrammar(context.Background(), llm, g, "say hi")
if err != nil {
t.Fatalf("TranslateGrammar: %v", err)
}
plan, err := script.CompileGrammar(context.Background(), g, src)
if err != nil {
t.Fatalf("CompileGrammar: %v", err)
}
if len(plan.Nodes) != 1 {
t.Errorf("nodes = %d, want 1", len(plan.Nodes))
}
}

func TestGrammar_HistoricalVerbIsKnownNotUnknown(t *testing.T) {
g := script.Grammar()
llm := func(_ context.Context, _, _ string) (string, error) {
return `temporal static ( hf_summarize "x" )`, nil
}
src, _ := script.TranslateGrammar(context.Background(), llm, g, "summarize")
_, err := script.CompileGrammar(context.Background(), g, src)
if err == nil {
t.Fatal("hf_summarize on temporal should fail (not implemented there yet)")
}
var notImpl *script.NotImplementedOnBackendError
if !errors.As(err, &notImpl) {
t.Errorf("expected NotImplementedOnBackendError, got %T: %v", err, err)
}
}
16 changes: 16 additions & 0 deletions pkg/script/submit.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,22 @@ import (
"github.com/vinodhalaharvi/agentscript/pkg/script/resolved"
)

// TranslateGrammar is the discovery-driven translate entry: it takes a
// GrammarInfo (from Grammar()) instead of a registry, so a front end can
// pipe discovery straight into translation without ever touching or
// naming a registry. This is the "dumb pipe" path — loom calls Grammar(),
// passes the result here, and forwards the output.
func TranslateGrammar(ctx context.Context, complete CompleteFunc, g GrammarInfo, prose string) (Source, error) {
return Translate(ctx, complete, g.Registry, prose)
}

// CompileGrammar compiles source against a GrammarInfo's registry. Pairs
// with TranslateGrammar so a discovery-driven front end never names a
// registry for either phase.
func CompileGrammar(ctx context.Context, g GrammarInfo, src Source) (sibyl.Plan, error) {
return Compile(ctx, g.Registry, src)
}

// Compile runs Parse >=> Resolve >=> Lower >=> Finalize >=> Validate,
// producing a validated sibyl.Plan. It does not submit.
//
Expand Down
Loading