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.