Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

2 Embedding Models

Final exposes two public Go packages for embedding. Both boot the runtime on import, own a long-lived Session, and run scripts with Send. They differ in how much of the host you expose into Final.

PackageRoleChoose when
finalgoMinimal Session / Send / Close API; optional FinalTool for LLM tool callingAI agents, chat backends, sandboxed script runners, most “run this Final string” hosts
finalembedSame Session/Send model plus host type registration (RegisterTypes) and re-exported Final cell/collection typesEmbedding Final as a scripting language for your Go types (UI, domain objects, generators)

For AI agentic support, prefer the simple model (finalgo). Agents need a stable interpret tool, captured stdout, and a restricted Session — not a catalog of host widgets. Reach for finalembed only when scripts must mint and drive types that live in your application.


Concepts: Session, Send, and async

Session

A Session is a long-lived Final execution context:

  • Private module dictionary (Session-local DC) where colon words you define with : persist across Sends
  • One dedicated VM worker goroutine that runs script batches
  • Optional Security Restrictions disabled-set (see Security Restrictions)

Create one Session per logical agent conversation, web client, or embed unit. Close it when that unit ends.

Send

out, err := sess.GetSender().Send(script, timeoutS)
Argument / resultMeaning
scriptFinal source (interpret and/or compile). Empty script is an error.
timeoutSAdvisory run budget in whole seconds (0 = none). Used for inward I/O defaults; not a hard wall-clock kill of blocking natives.
outCaptured stdout text from the batch ($, .s, stream writes to stdout, …)
errFault or interpret error; may be accompanied by partial out

Default Sessions use ephemeral stacks: each Send starts with empty DS/RS/… Dictionary and compiled words persist. That matches request/batch and agent-tool semantics. Persistent stacks (REPL-like) exist for the TUI path (finalembed.NewPersistentSession / finalgo.NewREPLSession) — not the usual agent default.

Send blocks until the worker finishes the batch. Concurrent Send calls on the same Session serialize.

Call sess.SetSecurityRestrictions(mask) as a Session-wide floor, or sender.SetSecurityRestrictions(mask) on the handle from GetSender() before that Send (those bits are ORed with the floor — you can tighten, not lift). Bits set deny the matching SR themes.

Making Send asynchronous

The VM already runs on a worker goroutine inside the Session. Your host call to Send is the synchronous rendezvous with that worker.

To keep an HTTP handler, chat turn, or UI thread responsive, run Send on your own goroutine (or job queue) and deliver out/err when ready:

go func() {
    out, err := sess.GetSender().Send(script, 30)
    // deliver out/err on your completion channel, websocket, etc.
    _ = out
    _ = err
}()

Do not start multiple overlapping Sends and expect parallel VM progress on one Session — they queue. For cancel, call sess.Break(); the VM stops at the next opcode boundary (natives may still finish first).

LockSession (exclusive agent turns)

finalgo.NewLockSession() / finalembed.NewLockSession() returns a LockSession. Its only Send path is GetLockedSender(), which holds until that handle’s Unlock(). A second GetLockedSender blocks until Unlock. final-ai uses this so a Dev console cannot interleave Sends in the middle of an AI turn.

You may Send several times on the same locked sender (compile helpers, then a follow-up) and then Unlock. After Unlock the handle is invalid (ErrLockedSenderInvalid).


Simple model: finalgo

Import path: github.com/androticus/final-go/finalgo.

Core API

APIEffect
NewSession() (*Session, error)Local module Session; ephemeral stacks; unrestricted by default
NewLockSession() (*LockSession, error)Same, but Send only via GetLockedSender
(*Session) GetSender() *SessionSenderHandle for one or more Sends; optional SetSecurityRestrictions
(*LockSession) GetLockedSender() *LockedSenderExclusive hold until Unlock()
(*SessionSender) Send(script string, timeoutS int) (string, error)Run one batch; return stdout
(*Session) Break()Cooperative stop of in-flight VM run
(*Session) Close() errorStop worker; idempotent
NewFinalTool() *FinalToolOpenAI-compat tool wrapper (Name / Description / ParametersJSON / Dispatch)
Sender interfaceSetSecurityRestrictions + Send — what FinalTool.Dispatch uses

Minimal embed (agent-friendly)

package main

import (
	"fmt"
	"log"

	"github.com/androticus/final-go/finalgo"
)

func main() {
	sess, err := finalgo.NewSession()
	if err != nil {
		log.Fatal(err)
	}
	defer sess.Close()

	snd := sess.GetSender()

	// Interpret a postfix expression; $ prints the TOS number to stdout.
	out, err := snd.Send(`3 4 + $`, 0)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Print(out) // 7

	// Definitions persist for later Sends in this Session.
	if _, err := snd.Send(`: sq dup * ;`, 0); err != nil {
		log.Fatal(err)
	}
	out, err = snd.Send(`5 sq $`, 0)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Print(out) // 25
}

That is the whole loop for most agentic hosts: NewSession → Send → Close.

Final as an LLM tool

FinalTool packages the Session for providers that accept OpenAI-style tools. Dispatch parses {"script":"…"}, calls Sender.Send, and returns captured stdout (or an [error] … content string).

sess, err := finalgo.NewSession()
if err != nil {
	log.Fatal(err)
}
defer sess.Close()

ft := finalgo.NewFinalTool()
// Register ft.Name(), ft.Description(), ft.ParametersJSON() with your AI client.
// On tool_calls:
content, err := ft.Dispatch(sess.GetSender(), toolCallID, ft.Name(), argumentsJSON)

Sample chat application

This repo ships cmd/final-ai: a small web chat UI that registers Final as a tool the model can call. Early tests showed the agent successfully calling Final for mathematical calculations (postfix scripts via the final tool). Run it with your provider API key (see cmd/final-ai); use -dev for the tabbed AI Chat + Final Dev console that shares one Session per browser cookie.


Extensive model: finalembed

Import path: github.com/androticus/final-go/finalembed.

Use this when Final scripts must create and operate on your Go types. You register each host type once after NewSession, before any Send. Registration is app-global for that Session’s module.

Core API (beyond finalgo)

APIEffect
NewSession() (*Session, error)Local module; ephemeral stacks
NewLockSession() (*LockSession, error)Exclusive Send via GetLockedSender
NewPersistentSession() (*Session, error)Same, but stacks survive across Sends (REPL / TUI style)
(*Session) RegisterTypes([]TypeReg) errorInstall host types; call once before Send
NewTypeReg[T](name string, reg func(*Type)) TypeRegBuild a registration entry for Go type T
(*Type) RegisterProperty / RegisterNativeMethod / RegisterNativeClassMethod / …Describe members on the Final type
Re-exports in types.go / cells.goInt, Float, String, AnyList, ToCell, … for DS-safe boundaries

Host type sketch

sess, err := finalembed.NewSession()
if err != nil {
	return err
}
defer sess.Close()

err = sess.RegisterTypes([]finalembed.TypeReg{
	finalembed.NewTypeReg[*Widget]("Widget", func(t *finalembed.Type) {
		t.RegisterNativeClassMethod("new", func(f *finalembed.RunFrame, ths *finalembed.TypeObjPair) {
			f.RP.DataStack.Push(NewWidget())
		})
		t.RegisterProperty("title", "String",
			func(inst any) any { return finalembed.String(inst.(*Widget).Title) },
			func(inst any, v any) { inst.(*Widget).Title = string(v.(finalembed.String)) },
			false,
		)
		t.RegisterNativeMethod("render", func(f *finalembed.RunFrame, ths *finalembed.TypeObjPair) {
			ths.Obj.(*Widget).Render()
		})
	}),
})
if err != nil {
	return err
}

_, err = sess.GetSender().Send(`Widget.new^ >ts "Hi" t:.title! t:.render^ t:drop`, 0)

Cell rule: values that cross the data stack (property get/set, method args/results) must be Final cells — finalembed.Int, Float, String, … — or pass through finalembed.ToCell. Raw Go int32/float32 will eventually panic under arithmetic.

A fuller worked example lives in cmd/lissajous (register a generator type, drive it from a Final script).

Tradeoffs vs finalgo

Concernfinalgofinalembed
Lines to first useful SendVery fewRegistration boilerplate per host type
API for an LLM toolExcellent (FinalTool)Possible, but usually unnecessary
Script your own Go structsNoYes
Re-use Final Lists/Maps from Go nativesLimitedFirst-class via re-exported types
Typical security postureTight SR mask; math / pure logicOften needs the affordances your types wrap (files, UI, …)

Recommendation: start with finalgo for agents. Graduate to finalembed only when a script must say YourType.new^ and call methods on host objects.


  • Security Restrictions — deny file I/O, HTTP dial, threading for sandboxed Sessions
  • Features — embedding, LockSession, and SRs listed in the survey
  • AI Tool Guidelines — sandboxing and exclusive turns for LLM tools
  • Design depth (not required for host authors): docs/003-design-notes/134-embedding-final.md