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
3 changes: 3 additions & 0 deletions pkg/script/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ func EchoSpec() registry.BuiltinSpec {
{Name: "text", Type: registry.StringT, Optional: true},
},
},
// echo is the one builtin available on BOTH backends: the
// in-process interpreter and Sibyl's registered Echo activity.
Backends: []registry.Backend{registry.BackendMemory, registry.BackendTemporal},
}
}

Expand Down
177 changes: 177 additions & 0 deletions pkg/script/catalog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// Package script — catalog.go defines the COMPLETE builtin catalog: every
// verb the AgentScript grammar has ever supported, so the vocabulary is
// complete and backwards-compatible. Resolve recognizes all of them.
//
// Availability is per-backend, not per-existence:
// - Every verb here is resolvable (no more "unknown builtin" for a real
// verb just because we're on the temporal path).
// - Each verb declares which backends it runs on. The full historical
// set runs on BackendMemory (the in-process interpreter implements
// them). Only verbs with a registered Sibyl activity also list
// BackendTemporal — today, just echo.
// - A known verb targeted at a backend it doesn't support yields a
// distinct NotImplementedOnBackendError (see resolve), NOT an
// UnknownBuiltinError. Genuinely unknown names (typos, hallucinations)
// still fail as unknown — the safety net is preserved.
//
// Arg schemas here are intentionally permissive (variadic strings): the
// registry's job for memory verbs is vocabulary recognition, while the
// in-process interpreter does its own argument handling. echo keeps a
// precise schema because it is the temporal-path builtin.
//
// Porting a verb to temporal later = add BackendTemporal to its spec and
// register the matching Sibyl activity. Until then it is a known,
// memory-only verb that reports "not implemented on temporal".
package script

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

// memoryVerbs is the complete historical verb set, all memory-backed.
// Sourced from the grammar's Action enum (the authoritative list).
var memoryVerbs = []string{
"agent",
"analyze",
"ask",
"audio_video_merge",
"calendar",
"claude",
"codereview",
"codereview_focus",
"confirm",
"contact_find",
"crypto",
"deploy",
"dns_lookup",
"doc_create",
"drive_save",
"email",
"emoji_style",
"exec",
"fmap",
"foreach",
"form_create",
"form_responses",
"gcp_check",
"github_pages_html",
"hf_classify",
"hf_embeddings",
"hf_fill_mask",
"hf_generate",
"hf_image_classify",
"hf_image_generate",
"hf_ner",
"hf_qa",
"hf_similarity",
"hf_speech_to_text",
"hf_summarize",
"hf_translate",
"hf_zero_shot",
"http_check",
"if",
"image_analyze",
"image_audio_merge",
"image_generate",
"images_to_video",
"job_search",
"kg_connect",
"kg_cypher",
"kg_extract",
"kg_hybrid",
"kg_ingest",
"kg_path",
"kg_query",
"kg_status",
"list",
"maps_trip",
"match",
"mcp",
"mcp_agent",
"mcp_connect",
"mcp_list",
"mcp_search",
"mcp_search_install",
"meet",
"merge",
"news",
"news_headlines",
"notify",
"ollama",
"pdf_fields",
"pdf_fill",
"perplexity",
"perplexity_domain",
"perplexity_pro",
"perplexity_recent",
"pfmap",
"ping",
"places_search",
"plug_agent",
"port_check",
"rag_connect",
"rag_index",
"rag_query",
"rag_schema",
"rag_status",
"read",
"reddit",
"render",
"rss",
"save",
"schedule",
"search",
"sheet_append",
"sheet_create",
"ssl_check",
"stdin",
"stock",
"summarize",
"table_render",
"task",
"text_to_speech",
"translate",
"twitter",
"undeploy",
"video_analyze",
"video_generate",
"video_script",
"weather",
"whatsapp",
"whois",
"youtube_search",
"youtube_shorts",
"youtube_upload",
}

// memorySpec builds a permissive, memory-only spec for a historical verb:
// any number of string args, recognized by Resolve, run by the in-process
// interpreter.
func memorySpec(name string) registry.BuiltinSpec {
return registry.BuiltinSpec{
Name: name,
AgentID: "agentscript/" + name,
ArgSchema: registry.ArgSchema{
Variadic: true,
VariadicType: registry.StringT,
},
Backends: []registry.Backend{registry.BackendMemory},
}
}

// CompleteRegistry returns a registry containing the entire historical
// verb vocabulary (all memory-backed) plus echo (memory + temporal). This
// is the backwards-compatible registry: every real verb resolves; the
// backend decides availability. Front ends that want the full grammar
// recognized use this; DefaultRegistry (echo only) remains for the
// minimal temporal-only case.
func CompleteRegistry() *registry.Registry {
r := registry.New()
// echo: the one verb available on BOTH backends.
echo := EchoSpec()
echo.Backends = []registry.Backend{registry.BackendMemory, registry.BackendTemporal}
r.MustRegister(echo)
// every historical verb, memory-backed.
for _, v := range memoryVerbs {
r.MustRegister(memorySpec(v))
}
return r
}
121 changes: 121 additions & 0 deletions pkg/script/catalog_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package script_test

import (
"context"
"errors"
"testing"

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

// The contract this PR establishes:
// - The registry is COMPLETE: every historical verb resolves (no
// "unknown builtin" for a real verb).
// - Availability is per-backend: historical verbs run on memory; a
// known verb under temporal that isn't ported yields a distinct
// NotImplementedOnBackendError — NEVER UnknownBuiltinError.
// - Genuinely unknown names still fail as UnknownBuiltinError (safety
// net preserved).

func TestCompleteRegistry_RecognizesAllHistoricalVerbs(t *testing.T) {
r := script.CompleteRegistry()
// A representative spread across the verb families.
for _, v := range []string{
"echo", "search", "summarize", "hf_summarize", "hf_translate",
"mcp", "mcp_agent", "mcp_connect", "perplexity", "perplexity_pro",
"weather", "stock", "crypto", "email", "calendar", "translate",
"image_generate", "video_analyze", "rag_query", "kg_extract",
"if", "foreach", "match", "fmap", "pfmap", "exec",
} {
if _, ok := r.Lookup(v); !ok {
t.Errorf("CompleteRegistry should recognize historical verb %q", v)
}
}
}

func TestCompleteRegistry_MemoryVerbResolvesOnMemory(t *testing.T) {
r := script.CompleteRegistry()
// hf_summarize is memory-only; under a memory block it RESOLVES clean
// (vocabulary + availability both pass). Note: memory *execution* is a
// later step — here we assert resolution succeeds, which is this PR's
// contract. Compile would invoke the temporal-only Finalize, so we
// stop at Resolve.
a, err := script.Parse(context.Background(), script.Source(`memory static ( hf_summarize "x" )`))
if err != nil {
t.Fatalf("parse: %v", err)
}
if _, err := script.Resolve(context.Background(), r, a); err != nil {
t.Fatalf("memory-backed verb should resolve under memory: %v", err)
}
}

func TestCompleteRegistry_MemoryVerbUnderTemporal_NotImplemented(t *testing.T) {
r := script.CompleteRegistry()
// hf_summarize under temporal: KNOWN verb, not available on temporal.
// This is caught at RESOLVE (availability check), before Finalize.
a, err := script.Parse(context.Background(), script.Source(`temporal static ( hf_summarize "x" )`))
if err != nil {
t.Fatalf("parse: %v", err)
}
_, err = script.Resolve(context.Background(), r, a)
if err == nil {
t.Fatal("memory-only verb under temporal should fail at resolve")
}
var notImpl *script.NotImplementedOnBackendError
if !errors.As(err, &notImpl) {
t.Fatalf("expected NotImplementedOnBackendError, got %T: %v", err, err)
}
var unknown *script.UnknownBuiltinError
if errors.As(err, &unknown) {
t.Error("a known verb must NOT produce UnknownBuiltinError")
}
if notImpl.Builtin != "hf_summarize" || notImpl.Backend != "temporal" {
t.Errorf("error fields = %q/%q, want hf_summarize/temporal", notImpl.Builtin, notImpl.Backend)
}
}

func TestCompleteRegistry_EchoRunsOnTemporal(t *testing.T) {
r := script.CompleteRegistry()
// echo is the one verb on BOTH backends. It compiles end-to-end on
// temporal (Finalize produces a Plan)...
if _, err := script.Compile(context.Background(), r, script.Source(`temporal static ( echo "hi" )`)); err != nil {
t.Errorf("echo should compile on temporal: %v", err)
}
// ...and resolves on memory (memory execution is a later step).
a, _ := script.Parse(context.Background(), script.Source(`memory static ( echo "hi" )`))
if _, err := script.Resolve(context.Background(), r, a); err != nil {
t.Errorf("echo should resolve on memory: %v", err)
}
}

func TestCompleteRegistry_SafetyNetUnknownStillUnknown(t *testing.T) {
r := script.CompleteRegistry()
// A genuinely unknown name (hallucination) must STILL be unknown,
// on either backend — the safety net is preserved.
for _, src := range []string{
`temporal static ( teleport "mars" )`,
`memory static ( teleport "mars" )`,
} {
_, err := script.Compile(context.Background(), r, script.Source(src))
if err == nil {
t.Fatalf("unknown verb should fail: %s", src)
}
var unknown *script.UnknownBuiltinError
if !errors.As(err, &unknown) {
t.Errorf("%s: expected UnknownBuiltinError, got %T", src, err)
}
}
}

func TestSupportsBackend_EmptyDefaultsToMemoryOnly(t *testing.T) {
// A spec with no declared backends must be memory-only — the safe
// default that prevents dispatching un-ported verbs to Temporal.
s := registry.BuiltinSpec{Name: "x"}
if !s.SupportsBackend(registry.BackendMemory) {
t.Error("empty Backends should support memory")
}
if s.SupportsBackend(registry.BackendTemporal) {
t.Error("empty Backends must NOT support temporal")
}
}
4 changes: 4 additions & 0 deletions pkg/script/lower_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ func TestCompile_CustomBuiltin(t *testing.T) {
ArgSchema: registry.ArgSchema{
Params: []registry.ParamSpec{{Name: "text", Type: registry.StringT}},
},
// This custom builtin is exercised under a temporal block, so it
// must declare temporal support (mirroring how a real ported verb
// is registered).
Backends: []registry.Backend{registry.BackendTemporal},
})
plan, err := script.Compile(context.Background(), r, script.Source(`temporal static ( shout "hey" )`))
if err != nil {
Expand Down
39 changes: 39 additions & 0 deletions pkg/script/registry/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,45 @@ type BuiltinSpec struct {
// AuthHint declares the builtin's default credential source. Advisory
// in the MVP.
AuthHint AuthSource
// Backends declares which execution backends this builtin can run on.
// A builtin is always resolvable (so the vocabulary is complete and
// backwards-compatible), but availability is per-backend: a verb in
// the registry that lists only BackendMemory resolves fine, runs on
// the memory interpreter, and yields a clear "not implemented on
// temporal" — distinct from an unknown-verb error. Empty is treated
// as "memory only" for safety (a verb with no declared temporal
// activity must not be dispatched to Temporal).
Backends []Backend
}

// Backend identifies an execution backend a builtin supports. It mirrors
// the ast.Backend keyword (memory | temporal) but lives here so the
// registry — the single source of truth for "what runs where" — has no
// dependency on the ast package.
type Backend int

const (
// BackendMemory is the in-process tree-walking interpreter, which
// implements the full historical verb set.
BackendMemory Backend = iota
// BackendTemporal is the durable Sibyl PlanWorkflow path; a verb is
// available here only once it has a registered Sibyl activity.
BackendTemporal
)

// SupportsBackend reports whether the builtin can run on b. An empty
// Backends list means memory-only (the safe default): a verb with no
// explicitly declared backend must never be dispatched to Temporal.
func (s BuiltinSpec) SupportsBackend(b Backend) bool {
if len(s.Backends) == 0 {
return b == BackendMemory
}
for _, x := range s.Backends {
if x == b {
return true
}
}
return false
}

// === Registry ==============================================================
Expand Down
Loading
Loading