18  Migration Guide

18.1 From Python Scripts

18.1.1 Before (Python)

import openai

def run_agent(prompt):
    messages = [{"role": "user", "content": prompt}]
    while True:
        response = openai.chat.completions.create(
            model="gpt-4",
            messages=messages,
            tools=[search_tool, done_tool]
        )
        if done_called(response):
            return extract_result(response)
        messages.append(response.choices[0].message)

18.1.2 After (Tactus)

local done = require("tactus.tools.done")
search = Tool { ... }

worker = Agent {
  model = "openai/gpt-4o",
  system_prompt = "...",
  tools = {search, done}
}

Procedure {
  input = {prompt = field.string{required = true}},
  output = {result = field.string{required = true}},
  function(input)
    worker({message = input.prompt})
    repeat worker() until done.called() or Iterations.exceeded(20)
    return {result = done.last_result() or ""}
  end
}

Benefits: - Automatic checkpointing - HITL built-in - BDD testing - Type-safe I/O

18.2 From LangChain/LangGraph

18.2.1 Before (LangGraph)

class State(TypedDict):
    messages: list
    done: bool

graph = StateGraph(State)
graph.add_node("agent", agent_node)
graph.add_node("tools", tool_node)
graph.add_conditional_edges(...)
app = graph.compile(checkpointer=memory)

18.2.2 After (Tactus)

local done = require("tactus.tools.done")
worker = Agent { model = "openai/gpt-4o-mini", tools = {search, done} }

Procedure {
  function(input)
    repeat worker() until done.called()
    return {result = done.last_result() or ""}
  end
}

Benefits: - Imperative code instead of graph definition - Simpler mental model - Same durability guarantees

18.3 From Large MCP Tool Catalogs

When an MCP server has many related operations, consider replacing the catalog with one programmable Tactus gateway. The host exposes an application module, and the model writes a short snippet that composes those APIs for the current task.

18.3.1 Before

MCP server exposes:
- score_info
- score_predict
- feedback_find
- evaluation_run
- report_run
- procedure_run
- ...

18.3.2 After

local score = plexus.score.info({ id = "compliance-score" })
local docs = plexus.docs.get({ key = "overview" })

return {
  score_name = score.scoreName,
  docs = docs.content,
}

Generic hosts usually expose this shape through require("your_app"); Plexus hides that bootstrap by injecting plexus as a global inside execute_tactus.

Benefits: - One MCP tool definition instead of a large context-heavy catalog - Smaller base context, with focused docs loaded on demand through discovery APIs - Programmatic composition inside sandboxed Tactus - Task-specific tools assembled from lower-level APIs - Optional bounded Agent loops inside the snippet when the task needs agentic investigation - Lazy discovery with api.list and docs.get - Host-owned policy, traces, budgets, HITL, and async handles

18.4 Key Differences

Python/LangChain Tactus
Classes and decorators Assignment-based declarations
Manual state management Durable state + checkpoints
Graph-based flow Imperative loops
Separate test files Embedded Specifications([[]])
Multiple files Single .tac file

18.5 From Older Tactus Syntax

Older examples often use “named blocks” (e.g., Agent "worker" { ... }). In v5, declarations are assignment-based and referenced as variables:

worker = Agent { ... }
repeat worker() until done.called()

18.6 Script Mode (Zero-Wrapper)

Script mode lets Tactus wrap simple Lua scripts as a procedure automatically. It’s useful when migrating a quick script into a full Procedure { ... } file.