Rec is designed for extensibility. This guide covers how to create plugins, custom tools, sub-agents, skills, and new data formats.
Plugins are discovered via Java’s ServiceLoader mechanism.
package net.kimleo.rec.plugin;
public class MyPlugin implements RecPlugin {
@Override
public String name() {
return "myplugin"; // accessible as __myplugin in JS
}
@Override
public Object module() {
return this; // public methods become JS functions
}
// Methods exposed to JS
public Source readFile(String path) {
return new CSVFileSource(path, ',', Schema.of("a", "b", "c"));
}
public Target writeTo(String path) {
return new FlatFileTarget(path);
}
}
Create the file src/main/resources/META-INF/services/net.kimleo.rec.plugin.RecPlugin:
net.kimleo.rec.plugin.MyPlugin
In rec-scripting/build.gradle, add your plugin module:
dependencies {
runtimeOnly project(':rec-myplugin')
}
var result = __myplugin.readFile("input.csv");
result.to(__myplugin.writeTo("output.csv"));
Source, Tee, or Target when possibleRecExceptionTools extend the agent’s capabilities.
import net.kimleo.rec.agent.tool.Tool;
import java.util.Map;
public class WeatherTool implements Tool {
@Override
public String name() {
return "GetWeather";
}
@Override
public String description() {
return "Get current weather for a location";
}
@Override
public Map<String, Object> parametersSchema() {
return Map.of(
"type", "object",
"properties", Map.of(
"location", Map.of(
"type", "string",
"description", "City name or ZIP code"
),
"units", Map.of(
"type", "string",
"enum", List.of("celsius", "fahrenheit"),
"description", "Temperature units"
)
),
"required", List.of("location")
);
}
@Override
public String execute(Map<String, Object> args) {
String location = (String) args.get("location");
// Call weather API...
return "Current weather in " + location + ": 22°C, sunny";
}
}
Via Agent (Java):
Agent agent = new Agent("gpt-4o");
agent.registry().register(new WeatherTool());
Via AgentModule (JS):
var rec = require("rec");
var agent = rec.createAgent("gpt-4o");
rec.addTool(agent, "GetWeather", "Get weather for a location", {
type: "object",
properties: {
location: { type: "string", description: "City name" }
},
required: ["location"]
}, function(args) {
return "Weather in " + args.location + ": 22°C, sunny";
});
Sub-agents are tools that spawn child conversations with their own context.
import net.kimleo.rec.agent.subagent.SubAgent;
import java.util.Set;
public class DataAnalysisAgent extends SubAgent {
public DataAnalysisAgent(Agent parent, ToolRegistry registry) {
super(parent, registry);
}
@Override
public String name() {
return "AnalyzeData";
}
@Override
public String description() {
return "Analyze data files and produce statistical summaries";
}
@Override
protected String systemPrompt() {
return "You are a data analyst. Use the provided tools to "
+ "examine data files, compute statistics, and present "
+ "findings in a clear format.";
}
@Override
protected Set<String> allowedTools() {
return Set.of("Read", "Glob", "Grep", "Bash",
"ExecuteRecScript");
}
}
agent.registry().register(
new DataAnalysisAgent(agent, agent.registry())
);
allowedTools()Skills are markdown files that teach the agent how to perform specific tasks.
.agents/skills/my-skill/
└── SKILL.md
---
name: my-skill
description: Brief description shown in the skills list
---
## Instructions
Detailed markdown instructions that the model follows when this skill is activated.
### Step 1
Write a Rec script to load the data file.
### Step 2
Inspect the output to understand the schema.
### Step 3
Apply the transformation and export results.
## Examples
### Example 1
User: "Analyze sales.csv"
Agent: Loads the file, inspects 5 rows, filters Q4 data, exports to q4_sales.csv
| Field | Required | Description |
|---|---|---|
name |
Yes | Unique skill identifier |
description |
Yes | Short description for the skills list |
Skills use lazy loading:
name and description are parsed from frontmatterSkills in .agents/skills/ are automatically discovered when the agent starts:
workspace/
├── AGENTS.md
└── .agents/
└── skills/
├── analysis/SKILL.md # auto-discovered
└── reporting/SKILL.md # auto-discovered
var rec = require("rec");
var agent = rec.createAgent("gpt-4o");
rec.addSkillDirectories(agent, "/custom/path/to/skills");
rec.activateSkill(agent, "my-skill");
import net.kimleo.rec.model.Source;
import net.kimleo.rec.data.DataSet;
import java.util.stream.Stream;
public class XmlSource implements Source {
private final Path file;
private final Schema schema;
@Override
public Stream<DataSet> stream() {
// Parse XML and return a stream of DataSet records
return parseXml(file).map(row -> new XmlDataRow(row, schema));
}
}
import net.kimleo.rec.model.Target;
public class XmlTarget implements Target {
@Override
public void put(DataSet dataSet) {
// Write DataSet as XML
}
}
public class XmlPlugin implements RecPlugin {
@Override
public String name() { return "xml"; }
@Override
public Object module() { return this; }
public Source file(String path) {
return new XmlSource(Paths.get(path));
}
public Target output(String path) {
return new XmlTarget(Paths.get(path));
}
}
var source = __xml.file("data.xml");
source.to(rec.flat("output.csv"));
The agent can connect to any MCP-compatible server:
var rec = require("rec");
var agent = rec.createAgent("gpt-4o");
// Filesystem MCP server
rec.addMcpServer(agent, "npx", "-y",
"@modelcontextprotocol/server-filesystem", "/data");
// Custom MCP server
rec.addMcpServer(agent, "node", "my-mcp-server.js");
When an MCP server is connected:
listTools() is called to discover available toolsTool interface via McpToolAdapterinputSchemaToolRegistryCurrently supports stdio transport:
McpJsonMapper (Jackson 3.x based)@Test
public void testPluginName() {
MyPlugin plugin = new MyPlugin();
assertEquals("myplugin", plugin.name());
assertNotNull(plugin.module());
}
@Test
public void testToolExecution() {
WeatherTool tool = new WeatherTool();
assertEquals("GetWeather", tool.name());
Map<String, Object> args = Map.of("location", "NYC");
String result = tool.execute(args);
assertTrue(result.contains("NYC"));
}
@Test
public void testSkillParsing() {
Skill skill = SkillLoader.load(skillDir);
assertEquals("my-skill", skill.name());
assertFalse(skill.description().isEmpty());
}