Loopy has five extension points, from zero-code Markdown files to full Java SPIs.
Skills Domain knowledge the agent reads on demand
Subagents Specialist agents for delegation
Tool Profiles New tools via Java SPI
Skills
Skills are the fastest way to make Loopy smarter. A skill is a Markdown file with YAML frontmatter. The agent sees skill names and descriptions upfront but only loads full content when relevant — no tokens wasted.
Create a skill
Place a SKILL.md in your project:
.claude/skills/my-conventions/SKILL.md
---
name : my-conventions
description : Team coding conventions for our Spring Boot services
---
# Instructions
When working on this codebase:
- Use constructor injection, never field injection
- All REST endpoints return ProblemDetail for errors (RFC 9457)
- Tests use @WebMvcTest with MockMvc, not @SpringBootTest
- Entity IDs are UUIDs, never auto-increment
Next time you run Loopy in that directory, the agent discovers the skill automatically.
Where skills live
Location Path Use case Project .claude/skills/*/SKILL.mdTeam conventions, checked into the repo Global ~/.claude/skills/*/SKILL.mdPersonal skills across all projects Classpath META-INF/skills/*/SKILL.md in JARsPublished skill packages (Maven dep)
Install from the catalog
Loopy ships with 23+ curated skills from 8 publishers:
/skills search testing
/skills info systematic-debugging
/skills add systematic-debugging
Publish skills as a JAR (SkillsJars)
Package skills as a Maven dependency so teams get them automatically:
my-skills.jar
└── META-INF/skills/
└── my-org/my-repo/api-design/
└── SKILL.md
Add the JAR to pom.xml and Loopy discovers it on the classpath. Skills follow the agentskills.io spec — they work in 40+ agentic CLIs, not just Loopy.
Subagents
Subagents are specialist agents the main agent delegates to via the Task tool. Define them as Markdown files — no Java required.
Create a subagent
Create .claude/agents/test-runner.md:
---
name : test-runner
description : Runs tests and reports pass/fail summary
tools : Bash, Read
---
You are a testing specialist. When invoked:
1. Run `./mvnw test` in the working directory
2. Parse output for pass/fail/skip counts
3. If tests fail, read the relevant test source files
4. Report a concise summary: what passed, what failed, and why
The main agent delegates testing tasks to this subagent automatically based on the description field.
Frontmatter fields
Field Required Description nameYes Unique identifier (lowercase, hyphens) descriptionYes When to use — the main agent reads this to decide toolsNo Allowed tools (comma-separated). Inherits all if omitted modelNo haiku, sonnet, or opus
Tips
Keep description specific — “Runs tests and reports results” routes better than “helps with testing”
Restrict tools to what the subagent needs. A test runner doesn’t need Edit
Subagents run in isolated context windows — they can’t see the main conversation
Subagents cannot spawn other subagents (the Task tool is excluded automatically)
Add new tools the agent can call. Implement a Java interface, package as a JAR, and Loopy discovers it at startup via ServiceLoader.
public class DatabaseToolProfile implements ToolProfileContributor {
@ Override
public String profileName () {
return "database-tools" ;
}
@ Override
public List < ToolCallback > tools ( ToolFactoryContext ctx ) {
return List . of (
ToolCallbacks . from ( new QueryTool ( ctx . workingDirectory ())),
ToolCallbacks . from ( new SchemaTool ( ctx . workingDirectory ()))
);
}
}
Register via ServiceLoader
Create META-INF/services/io.github.markpollack.loopy.tools.ToolProfileContributor:
com.example.tools.DatabaseToolProfile
What ToolFactoryContext provides
Field Type Description workingDirectoryPathAgent’s working directory chatModelChatModelThe active LLM (for tools that need AI) commandTimeoutDurationTool execution timeout (default 120s) interactivebooleanTrue in TUI mode, false in print/REPL
Built-in profiles
Profile Description devFull interactive toolset (bash, file I/O, search, skills, subagents) bootSpring Boot scaffolding tools headlessSame as dev minus AskUserQuestion (for CI/CD) readonlyRead-only: file read, grep, glob, list directory
Custom profiles load alongside built-in profiles, not replacing them.
Listeners
Observe what the agent does without changing its behavior.
Fires around every tool execution:
public class CostTracker implements ToolCallListener {
@ Override
public void onToolExecutionCompleted ( String runId , int turn ,
AssistantMessage . ToolCall toolCall , String result ,
Duration duration ) {
log . info ( "Tool {} took {}ms" , toolCall . name (), duration . toMillis ());
}
}
AgentLoopListener
Fires at loop lifecycle boundaries:
public class ProgressReporter implements AgentLoopListener {
@ Override
public void onLoopCompleted ( String runId , LoopState state ,
TerminationReason reason ) {
System . err . printf ( "Done: %s (%d turns)%n" , reason, state . turns ());
}
}
Wire listeners through the MiniAgent builder:
var agent = MiniAgent . builder ()
. config (config)
. model (chatModel)
. toolCallListener ( new CostTracker ())
. loopListener ( new ProgressReporter ())
. build ();
Listener methods should not throw exceptions — exceptions are logged and swallowed to avoid crashing the agent loop.
Programmatic API
Embed Loopy’s agent in other Java applications:
LoopyAgent agent = LoopyAgent . builder ()
. workingDirectory ( Path . of ( "/path/to/project" ))
. build ();
LoopyResult result = agent . run ( "add input validation to UserController" );
Multi-step with session memory
Context is preserved across run() calls by default:
LoopyAgent agent = LoopyAgent . builder ()
. workingDirectory (workspace)
. maxTurns ( 80 )
. build ();
agent . run ( "plan a refactoring of the service layer" );
agent . run ( "now execute the plan" ); // sees the previous conversation
Builder options
Method Description Default .model(String)Model ID claude-sonnet-4-6.workingDirectory(Path)Agent’s working directory required .systemPrompt(String)Custom system prompt Built-in coding prompt .maxTurns(int)Max loop iterations 80.costLimit(double)Max cost in dollars $5.00.sessionMemory(boolean)Preserve context across run() calls true.timeout(Duration)Overall loop timeout 10 min.disabledTools(Set<String>)Tools to exclude none
Custom endpoints (vLLM, LM Studio)
LoopyAgent agent = LoopyAgent . builder ()
. baseUrl ( "http://localhost:1234/v1" )
. apiKey ( "lm-studio" )
. model ( "local-model" )
. workingDirectory (workspace)
. build ();