codeact

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 30, 2026 License: MIT Imports: 2 Imported by: 3

README

codeact

In-process JS/Python execution for Go LLM agents. No Cgo. No subprocesses.

Powered by Ramune (JavaScriptCore or QuickJS) and pyffi (CPython).

Why codeact?

Existing Go AI agent frameworks (Eino, Google ADK Go, LangChainGo, etc.) execute code via subprocesses — spawning python3 or node with os/exec. The subprocess cannot call back into your Go application.

This forces multi-turn LLM conversations:

── Subprocess approach ──────────────────────────────
Turn 1: LLM → tool_call: get_data("users")       → Go function → result
Turn 2: LLM → tool_call: execute_python("sort…")  → subprocess  → result
         ⚠ execute_python cannot call get_data()
Turn 3: LLM → tool_call: save_result(sorted)      → Go function → done
Total: 3 LLM turns

codeact embeds the runtime in-process. RegisterFunc exposes Go functions directly to the script. The LLM writes one program that mixes Go function calls with computation:

── codeact ──────────────────────────────────────────
Turn 1: LLM → execute_js(`
    var data = get_data("users");         // call Go
    data.sort((a, b) => a.age - b.age);   // compute in JS
    save_result(data);                    // call Go again
`) → done
Total: 1 LLM turn

Fewer LLM round-trips means faster, cheaper, and less error-prone execution.

Quick Start

CodeAct — expose a code execution tool to the agent

The agent gets an execute_js tool and explicitly writes code that calls your Go functions.

runner, _ := js.NewRunner()
defer runner.Close()

runner.RegisterFunc("getUsers", func(args []any) (any, error) {
    return db.QueryUsers(ctx)
})

result, _ := runner.Run(ctx, `
    const users = getUsers();
    return users.filter(u => u.active).length;
`)
fmt.Println(result.Result) // "42"
AI Functions — describe what you want, LLM writes the code

Write the desired behavior in natural language. The agent calls analyze_users(minAge: 25) like any regular tool — behind the scenes, a separate LLM call generates the implementation from your description.

spec := &aifunc.Spec{
    Name: "analyze_users",

    // Describe what the function should do — the LLM generates the code
    Description: "Filter users by minimum age, sort by signup date, " +
        "and return a JSON object with totalCount and averageAge fields",

    Params: []aifunc.ParamSpec{
        {Name: "minAge", Type: "number", Description: "Minimum age filter"},
    },
    GoFuncs: []aifunc.GoFuncSpec{
        {Name: "getUsers", Fn: getUsersFunc, Signature: aifunc.InferSignature(getUsersFunc)},
    },
    PostConditions: []string{"result.totalCount >= 0"},
}

model := einoadapter.WrapModel(chatModel)
tool, _ := einoadapter.NewAIFunc(runner, model, spec)
// The outer agent sees "analyze_users(minAge)" — a structured tool like any other.
// Internally: description → LLM generates code → code calls getUsers() → result

Install

# Runtimes (pick one or both)
go get github.com/i2y/codeact/js       # JavaScript/TypeScript
go get github.com/i2y/codeact/python   # Python

# Framework adapters (pick what you use)
go get github.com/i2y/codeact/eino      # Eino
go get github.com/i2y/codeact/adk       # Google ADK
go get github.com/i2y/codeact/langchain # LangChainGo
go get github.com/i2y/codeact/blades    # Blades

RegisterFunc

Expose any Go function to LLM-generated code.

JavaScript

JS uses ramune.GoFunc signature: all arguments arrive as []any.

runner, _ := js.NewRunner()

runner.RegisterFunc("queryDB", func(args []any) (any, error) {
    sql, _ := args[0].(string)
    return db.Query(ctx, sql)
})

runner.RegisterFunc("sendSlack", func(args []any) (any, error) {
    channel, _ := args[0].(string)
    msg, _ := args[1].(string)
    return nil, slackClient.Post(channel, msg)
})
// LLM-generated code
const users = queryDB("SELECT name FROM users WHERE active = true");
const summary = users.map(u => u.name).join(", ");
sendSlack("#reports", `Active users: ${users.length} — ${summary}`);

RegisterAnyFunc accepts arbitrary Go function signatures and handles type conversion via reflection:

runner.RegisterAnyFunc("add", func(a, b int) int { return a + b })
Python

Python accepts any Go function signature directly. Functions are called via go_bridge.

runner, _ := python.NewRunner()

runner.RegisterFunc("queryDB", func(sql string) ([]map[string]any, error) {
    return db.Query(ctx, sql)
})
import go_bridge
rows = go_bridge.queryDB("SELECT * FROM users WHERE active = 1")
print(f"Active users: {len(rows)}")

Framework Adapters

All adapters accept the generic Runner interface.

Eino
import (
    "github.com/i2y/codeact/js"
    einoadapter "github.com/i2y/codeact/eino"
)

jsRunner, _ := js.NewRunner()
jsTool := einoadapter.NewTool(jsRunner)

agent, _ := react.NewAgent(ctx, &react.AgentConfig{
    Model: chatModel,
    Tools: []tool.BaseTool{jsTool},
})
Google ADK
import adkadapter "github.com/i2y/codeact/adk"

jsTool, _ := adkadapter.NewTool(jsRunner)
LangChainGo
import lcadapter "github.com/i2y/codeact/langchain"

jsTool := lcadapter.NewTool(jsRunner)
Blades
import bladesadapter "github.com/i2y/codeact/blades"

jsTool := bladesadapter.NewTool(jsRunner)

AI Functions

Define tools in natural language instead of writing implementation code. You describe what the function should do, list the Go functions it can call, and specify post-conditions — the LLM generates and executes the code at runtime. Think Strands AI Functions for Go.

import (
    "github.com/i2y/codeact/js"
    "github.com/i2y/codeact/aifunc"
    einoadapter "github.com/i2y/codeact/eino"
)

runner, _ := js.NewRunner()

spec := &aifunc.Spec{
    Name:        "analyze_logs",
    Description: "Analyze server logs and detect anomalies",
    Params: []aifunc.ParamSpec{
        {Name: "timeRange", Type: "string", Description: "Time range like '1h', '24h'"},
        {Name: "severity", Type: "string", Description: "Minimum severity: 'info', 'warn', 'error'"},
    },
    GoFuncs: []aifunc.GoFuncSpec{
        {
            Name:      "queryLogs",
            Fn:        queryLogsFunc,
            Signature: aifunc.InferSignature(queryLogsFunc),
        },
        {
            Name:      "getMetrics",
            Fn:        getMetricsFunc,
            Signature: aifunc.InferSignature(getMetricsFunc),
        },
    },
    PostConditions: []string{
        "result.anomalyCount >= 0",
        "result.checkedEntries > 0",
    },
}

// Wrap your framework's model as aifunc.Model
model := einoadapter.WrapModel(chatModel)

// Use as an Eino tool alongside regular CodeAct tools
analyzeTool, _ := einoadapter.NewAIFunc(runner, model, spec)
execTool := einoadapter.NewTool(runner)

agent, _ := react.NewAgent(ctx, &react.AgentConfig{
    Model: chatModel,
    Tools: []tool.BaseTool{analyzeTool, execTool},
})

When the LLM calls analyze_logs(timeRange: "1h", severity: "error"), the flow is:

  1. System prompt is built from the spec (task description, parameter definitions, available Go functions, post-conditions)
  2. LLM generates code that calls queryLogs() and getMetrics() to fulfill the request
  3. Code is executed on the runner
  4. Post-conditions are validated — if any fail, the LLM gets error feedback and retries (up to MaxRetries)
  5. Result is returned to the outer agent

Each adapter provides NewAIFunc(runner, model, spec) and WrapModel to convert the framework's LLM type to aifunc.Model:

// Eino — wraps model.BaseChatModel
model := einoadapter.WrapModel(chatModel)
tool, err := einoadapter.NewAIFunc(runner, model, spec)

// Google ADK — wraps model.LLM
model := adkadapter.WrapModel(llm)
tool, err := adkadapter.NewAIFunc(runner, model, spec)

// Blades — wraps blades.ModelProvider
model := bladesadapter.WrapModel(provider)
tool, err := bladesadapter.NewAIFunc(runner, model, spec)

// LangChainGo — provide your own aifunc.Model implementation
tool, err := lcadapter.NewAIFunc(runner, model, spec)

You can also implement aifunc.Model directly for custom LLM clients:

type Model interface {
    Generate(ctx context.Context, systemPrompt string, messages []aifunc.Message) (string, error)
}

Runtimes

JavaScript/TypeScript (Ramune)
runner, _ := js.NewRunner(
    js.WithDependencies("cheerio@1", "xlsx@0.18"),
    js.WithTimeout(10 * time.Second),
    js.WithPermissions(&ramune.Permissions{
        Net:   ramune.PermGranted,  // allow fetch
        Read:  ramune.PermGranted,  // allow file reads
        Write: ramune.PermDenied,   // deny file writes
        Run:   ramune.PermDenied,   // deny process execution
    }),
)
  • JSC backend (default): macOS (bundled), Linux (requires libjavascriptcoregtk-4.1-dev)
  • QuickJS backend: build with -tags quickjs — pure Go, works everywhere including Windows
  • Top-level await supported. Code is wrapped in an async IIFE
  • Last expression value is returned as RunResult.Result
  • console.log/error/warn/info captured in RunResult.Logs
Python (pyffi/CPython)
runner, _ := python.NewRunner(
    python.WithDependencies("pandas", "requests"),  // installed via uv
    python.WithTimeout(10 * time.Second),
)
  • Requires CPython 3.9+
  • WithDependencies requires uv
  • Assign to __result for explicit return, or the last expression is returned
  • print() output captured in RunResult.Logs
  • No permission sandbox — full CPython access

Runner Interface

type Runner interface {
    Run(ctx context.Context, code string) (*RunResult, error)
    Close() error
    Language() string        // "javascript" or "python"
    ToolName() string        // "execute_js" or "execute_python"
    ToolDescription() string
}

type RunResult struct {
    Result string   `json:"result"`
    Logs   []string `json:"logs,omitempty"`
    Error  string   `json:"error,omitempty"`
}

Code exceptions are captured in RunResult.Error, not returned as Go errors. This lets the LLM observe the error and retry. Only infrastructure failures (runtime crash, etc.) return Go errors.

Architecture

codeact/                Core interface (Runner, RunResult)
├── codeact/aifunc      AI Functions core (Spec, Execute, prompt building)
├── codeact/js          JS/TS runner (Ramune — JSC or QuickJS)
├── codeact/python      Python runner (pyffi/CPython)
├── codeact/eino        Eino adapter
├── codeact/adk         Google ADK adapter
├── codeact/langchain   LangChainGo adapter
└── codeact/blades      Blades adapter

Runtime and adapter modules are independent — each has its own go.mod and only pulls in the dependencies it needs. aifunc is part of the core module with zero external dependencies.

Security

The JS runner supports fine-grained permission control via WithPermissions. File writes and process execution are denied by default.

The Python runner has no permission sandbox. Neither runtime provides OS-level isolation. For untrusted code, combine with containers, nsjail, or bubblewrap.

License

MIT

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ToolDefinition

func ToolDefinition(r Runner) map[string]any

ToolDefinition returns a JSON Schema object describing a runner's tool input.

Types

type RunResult

type RunResult struct {
	Result string   `json:"result"`
	Logs   []string `json:"logs,omitempty"`
	Error  string   `json:"error,omitempty"`
}

RunResult holds the outcome of a code execution.

func (*RunResult) String

func (r *RunResult) String() string

String returns the RunResult as a JSON string for LLM consumption.

type Runner

type Runner interface {
	// Run executes code and returns the result.
	// JS exceptions / Python exceptions are captured in RunResult.Error,
	// not returned as Go errors. Only infrastructure failures return Go errors.
	Run(ctx context.Context, code string) (*RunResult, error)

	// Close releases the underlying runtime.
	Close() error

	// Language returns the language name ("javascript" or "python").
	Language() string

	// ToolName returns the tool name for LLM function calling.
	ToolName() string

	// ToolDescription returns the tool description for LLMs.
	ToolDescription() string
}

Runner is the common interface for code execution backends.

Directories

Path Synopsis
js module
python module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL