Migrate an existing tool loop¶
You do not need to rewrite a working agent into a cartridge. The safest migration keeps the task, provider, tool implementations, and product tests fixed while replacing one control boundary at a time.
The first useful milestone is small: your existing tools run through
composable_loop(), and every dispatch is returned to your code as a Step.
Capture, hooks, cartridges, and eval gates can follow independently.
Map what you already own¶
| Existing harness responsibility | Looplet surface |
|---|---|
| Provider client | An LLMBackend or bundled backend adapter |
| Tool schema and callable | ToolSpec, @tool, and tools_from() |
| Model-to-tool while-loop | composable_loop() |
| Cross-cutting runtime policy | A focused hook method |
| Run logs | ProvenanceSink and the yielded Step stream |
| Product acceptance check | Collector plus outcome grader |
| Versioned harness directory | Optional cartridge |
Keep domain services and business rules outside Looplet. A tool can continue calling the same function it called before the migration.
1. Adapt one tool¶
Start with a read-only or otherwise low-risk tool. The decorator infers a JSON
Schema from type hints and returns an ordinary ToolSpec:
from looplet import tool, tools_from
@tool(description="Look up one service owner by name.")
def lookup_owner(service: str) -> dict:
return ownership_service.lookup(service)
tools = tools_from([lookup_owner], include_done=True)
Do not move service clients, credentials, or persistence into the tool merely to satisfy Looplet. Close over existing dependencies or expose them through a small host-owned resource.
If your harness already has a schema registry, adapt it to ToolSpec rather
than adding duplicate decorators. The API map links to the registry
and tool types.
2. Replace only the control loop¶
from looplet import LoopConfig, composable_loop
config = LoopConfig(max_steps=8, use_native_tools=False)
for step in composable_loop(
llm=current_backend,
tools=tools,
task={"goal": user_request},
config=config,
):
existing_logger.info(step.pretty())
Keep text-mode tools for the first parity check if the previous harness used a text protocol. Native tool calling can be enabled and tested as a separate change.
Run the same deterministic tool tests and a small set of representative tasks before adding any hook. If behavior changes, the loop replacement is the only new variable.
3. Keep your provider adapter¶
You can use an existing client through the backend protocol:
class ExistingBackend:
def __init__(self, client):
self.client = client
def generate(
self,
prompt: str,
*,
max_tokens: int = 2000,
system_prompt: str = "",
temperature: float = 0.2,
) -> str:
return self.client.complete(
system=system_prompt,
prompt=prompt,
max_tokens=max_tokens,
temperature=temperature,
)
Switching provider SDKs during the loop migration makes failures harder to
attribute. Adopt OpenAIBackend or AnthropicBackend later if they remove
real integration code.
4. Move policy into narrow hooks¶
Add a hook only when you can name the exact lifecycle boundary it owns. For
example, a release guard that prevents premature completion belongs in
check_done:
from looplet import Block
class RequireArtifact:
def check_done(self, state, session_log, context, step_num):
if not artifact_exists(state):
return Block("Create the required artifact before finishing.")
return None
Do not create one hook that contains permissions, retries, metrics, domain state, and acceptance logic. Compose small hooks and keep outcome acceptance in post-run graders. See hooks and runtime operations.
5. Capture one real failure¶
from looplet import ProvenanceSink
sink = ProvenanceSink(dir="traces/incident-001")
recorded_backend = sink.wrap_llm(current_backend)
for step in composable_loop(
llm=recorded_backend,
tools=tools,
hooks=[sink.trajectory_hook()],
task={"goal": user_request},
config=config,
):
existing_logger.info(step.pretty())
sink.flush()
Treat the trace as sensitive prompt evidence. Apply redact= before capture
when prompts or tool results may contain credentials or personal data.
Use captured-response replay only when holding model decisions fixed answers the question you are testing. Tool code, clocks, networks, permissions, and other side effects execute again. The experiment guide separates replay from mocks and fresh model sampling.
6. Turn the failure into an outcome contract¶
Preserve the task as data, inspect the resulting world independently, and grade that observation:
def collect_result(state):
result = read_product_artifact()
return {"observed_status": result.status}
def eval_status_is_ready(ctx):
return ctx.artifacts["observed_status"] == ctx.task["expected"]["status"]
Avoid requiring a historical tool sequence unless the sequence itself is a runtime contract. A better model may take a different route to the same valid outcome. See behavioral evals for cases, required marks, pytest, and host-owned holdouts.
7. Adopt a cartridge only when it helps review¶
Once the Python harness has parity and a useful regression contract, a cartridge can colocate its prompt, tools, hooks, resources, and self-tests:
looplet describe ./agent.cartridge
looplet diff ./agent-v1.cartridge ./agent-v2.cartridge --show
looplet hash ./agent.cartridge
Cartridges are a file representation of the same AgentPreset used by the
Python API. They are not required to use the loop, hooks, provenance, replay,
or eval primitives.
Migration sequence¶
Use separate commits or pull requests for these checkpoints:
- adapt tools without changing their implementation;
- replace the control loop and establish behavior parity;
- capture run evidence with redaction;
- add one failure-derived case, collector, and required grader;
- add only the hooks needed by observed runtime policy;
- package the harness as a cartridge if file-native review helps;
- enable native tools, compaction, or other operational controls one at a time.
At every checkpoint, keep one cheap parity test that can disprove the current migration assumption. The goal is not to use every Looplet feature. The goal is to make the harness boundary inspectable and each future change testable.