# How to Use Completion Signals to Stop Sandcastle Agent Iterations Early

> Learn how to use completion signals to stop Sandcastle agent iterations early and reduce token costs. Maximize LLM efficiency effortlessly.

- Repository: [Matt Pocock/sandcastle](https://github.com/mattpocock/sandcastle)
- Tags: how-to-guide
- Published: 2026-05-24

---

**Sandcastle monitors each LLM agent iteration for a configurable completion signal and terminates the loop immediately when detected, preventing wasted iterations and reducing token costs.**

Sandcastle, available at `mattpocock/sandcastle`, runs LLM-based agents inside configurable sandbox iterations. By implementing **completion signals to stop Sandcastle agent iterations early**, you create deterministic termination conditions that halt execution immediately when your agent finishes its task, preventing unnecessary token consumption and keeping the sandbox clean.

## How Completion Signals Work in Sandcastle

The orchestration logic resides in [`src/Orchestrator.ts`](https://github.com/mattpocock/sandcastle/blob/main/src/Orchestrator.ts) and implements a four-stage pipeline for detecting early termination.

### Default Signal Detection

If you do not specify a custom signal, Sandcastle automatically searches for the literal string `"<promise>COMPLETE</promise>"` after each iteration (see [Orchestrator.ts L75-L77]):

```ts
const DEFAULT_COMPLETION_SIGNAL = "<promise>COMPLETE</promise>";

```

### Signal Normalization

The `orchestrate` function accepts `completionSignal` as either a single string or an array of strings. The orchestrator normalizes this input into an array for uniform processing (see [Orchestrator.ts L38-L45]):

```ts
if (options.completionSignal === undefined) {
  completionSignals = [DEFAULT_COMPLETION_SIGNAL];
} else if (Array.isArray(options.completionSignal)) {
  completionSignals = options.completionSignal;
} else {
  completionSignals = [options.completionSignal];
}

```

### Detection Logic

After each iteration completes, Sandcastle checks the `agentOutput` against all configured signals using substring matching (see [Orchestrator.ts L4-L7]):

```ts
const matchedSignal = completionSignals.find((sig) =>
  agentOutput.includes(sig)
);

```

### Early Exit Mechanism

When a signal matches, the orchestrator immediately returns results without processing remaining iterations (see [Orchestrator.ts L30-L38]):

```ts
if (lifecycleResult.result.completionSignal !== undefined) {
  yield* display.status(
    label(`Agent signaled completion after ${i} iteration(s).`),
    "success",
  );
  return {
    iterations: allIterations,
    completionSignal: lifecycleResult.result.completionSignal,
    stdout: allStdout,
    commits: allCommits,
    branch: resolvedBranch,
    preservedWorktreePath: iterationPreservedPath,
  };
}

```

## Configuring Completion Signals via CLI

The CLI defined in [`src/cli.ts`](https://github.com/mattpocock/sandcastle/blob/main/src/cli.ts) forwards the `--completion-signal` flag to the orchestrator. Specify this flag multiple times to create an array of valid termination signals.

Use a single custom signal:

```bash
sandcastle run \
  --iterations 10 \
  --completion-signal "TASK_COMPLETE" \
  --prompt "Refactor the utils module"

```

Use multiple signals to stop when any condition is met:

```bash
sandcastle run \
  --iterations 20 \
  --completion-signal "BUILD_SUCCESS" \
  --completion-signal "TESTED_AND_VERIFIED" \
  --prompt "Build and test the application"

```

## Programmatic API Usage

When using Sandcastle programmatically via [`src/run.ts`](https://github.com/mattpocock/sandcastle/blob/main/src/run.ts), pass the `completionSignal` option to the `orchestrate` function:

```ts
import { orchestrate, DEFAULT_COMPLETION_SIGNAL } from "sandcastle";

const result = await orchestrate({
  hostRepoDir: "/path/to/repo",
  iterations: 15,
  prompt: "Generate API documentation",
  completionSignal: ["<docs-done>", DEFAULT_COMPLETION_SIGNAL],
});

console.log(`Completed after ${result.iterations.length} iterations`);
console.log(`Signal used: ${result.completionSignal}`);

```

The function accepts either a string or string array, providing type-safe flexibility for complex workflows.

## How Agents Emit Completion Signals

Agents can trigger early termination through two output channels:

- **Plain-text stdout**: The agent prints the exact signal string to standard output
- **Structured result events**: When providers like Claude Code emit a `result` event containing the signal string, the orchestrator extracts it from `agentOutput`

Both methods converge on the same `agentOutput.includes(sig)` check in [`src/Orchestrator.ts`](https://github.com/mattpocock/sandcastle/blob/main/src/Orchestrator.ts), ensuring consistent detection regardless of transport format.

## Summary

- Sandcastle checks for the literal string `"<promise>COMPLETE</promise>"` by default unless you override it with the `completionSignal` option
- The `completionSignal` parameter in [`src/Orchestrator.ts`](https://github.com/mattpocock/sandcastle/blob/main/src/Orchestrator.ts) accepts either a single string or an array of strings to match against agent output
- Detection occurs via substring matching (`agentOutput.includes(sig)`) after each iteration completes
- When matched, the orchestrator returns immediately from [`src/Orchestrator.ts`](https://github.com/mattpocock/sandcastle/blob/main/src/Orchestrator.ts) with the current iteration count, signal string, and preserved state
- Configure signals via the `--completion-signal` CLI flag or the `completionSignal` property in the programmatic API

## Frequently Asked Questions

### What is the default completion signal in Sandcastle?

If you do not specify a custom signal, Sandcastle searches for the literal string `"<promise>COMPLETE</promise>"` after each agent iteration. This constant is defined in [`src/Orchestrator.ts`](https://github.com/mattpocock/sandcastle/blob/main/src/Orchestrator.ts) and serves as the fallback termination marker.

### Can I use multiple completion signals for a single run?

Yes. Pass an array of strings to the `completionSignal` option programmatically, or use the `--completion-signal` flag multiple times in the CLI. The orchestrator checks each iteration against all provided signals and stops when any one matches.

### How does Sandcastle detect completion signals in agent output?

After each iteration, Sandcastle performs a substring search using `agentOutput.includes(sig)` against all configured signals. This detects markers appearing in either plain stdout text or structured result events emitted by the LLM provider.

### Where is the early termination logic implemented?

The early exit mechanism resides in [`src/Orchestrator.ts`](https://github.com/mattpocock/sandcastle/blob/main/src/Orchestrator.ts). When a signal matches, the orchestrator yields a success status and returns an `OrchestrateResult` object containing the completed iterations, matched signal string, and preserved worktree path, bypassing remaining iterations.