diff --git a/pkg/script/grammar.go b/pkg/script/grammar.go new file mode 100644 index 0000000..7694d33 --- /dev/null +++ b/pkg/script/grammar.go @@ -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"}, + } +} diff --git a/pkg/script/grammar_test.go b/pkg/script/grammar_test.go new file mode 100644 index 0000000..99a6cc8 --- /dev/null +++ b/pkg/script/grammar_test.go @@ -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, ¬Impl) { + t.Errorf("expected NotImplementedOnBackendError, got %T: %v", err, err) + } +} diff --git a/pkg/script/submit.go b/pkg/script/submit.go index b9116ae..2145056 100644 --- a/pkg/script/submit.go +++ b/pkg/script/submit.go @@ -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. //