Loop mode gives a session explicit workflow state across iterations: a goal, iteration counters, and loop-specific status you can inspect separately from the standard session detail. It's the shape you want for review loops, bounded research, or any iteration-aware workflow that needs explicit pause and resume state.
The Pattern
Create the session with workflowMode: 'loop', then call getLoopState() to read the workflow state back.
import { CoderClient } from '@agentuity/coder';
async function main(): Promise<void> {
const client = new CoderClient();
try {
const created = await client.createSession({
task: 'Review the repository and suggest the next documentation steps.',
// Opt into loop workflow state; standard sessions don't expose loop fields
workflowMode: 'loop',
loop: {
goal: 'Produce a concise docs plan',
maxIterations: 3,
// Default to supervised advancement; flip to true for unattended loops
autoContinue: false,
allowDetached: false,
},
tags: ['docs', 'loop-example'],
});
// getSession is for lifecycle state; getLoopState is the only place loop fields live
const session = await client.getSession(created.sessionId);
const loopState = await client.getLoopState(created.sessionId);
console.log(
JSON.stringify(
{
session: {
sessionId: session.sessionId,
status: session.status,
bucket: session.bucket,
workflowMode: session.workflowMode,
},
loopState,
},
null,
2
)
);
} catch (error) {
const message = error instanceof Error ? error.stack ?? error.message : String(error);
console.error('Failed to create or inspect the loop-mode session');
console.error(message);
process.exitCode = 1;
}
}
void main();Example Output
{
"session": {
"sessionId": "codesess_6727f29e6086",
"status": "creating",
"bucket": "provisioning",
"workflowMode": "loop"
},
"loopState": {
"sessionId": "codesess_6727f29e6086",
"workflowMode": "loop",
"loop": {
"status": "starting",
"iteration": 0,
"maxIterations": 3,
"goal": "Produce a concise docs plan",
"startedAt": 1776110497864,
"updatedAt": 1776110497864,
"autoContinue": false,
"allowDetached": false
}
}
}What to Inspect
getSession() returns the session's lifecycle state. getLoopState() returns the workflow state inside a loop-mode run. When you read loop state, the fields that usually matter:
status: where the loop currently is (starting,running,paused, or a terminal status)iterationandmaxIterations: how far the loop has advanced and its upper boundgoal: the high-level goal you gave the loopautoContinueandallowDetached: whether the loop can advance and keep running without a manual step or an attached client
Polling a Loop to Completion
For headless, unattended runs, configure the loop with autoContinue: true and allowDetached: true, then poll getLoopState() until the loop reaches a terminal status. This lets your app dispatch the work and check back at a controlled interval rather than staying connected.
The terminal statuses from CoderLoopStatusSchema are completed, cancelled, and blocked. The status awaiting_input is a non-terminal pause: the loop stopped and is waiting for external input before it can continue. All other statuses (idle, starting, running, paused) indicate the loop is still in progress.
import { CoderClient } from '@agentuity/coder';
import type { CoderLoopStatus } from '@agentuity/coder';
const TERMINAL: ReadonlySet<CoderLoopStatus> = new Set(['completed', 'cancelled', 'blocked']);
const POLL_MS = 5_000;
const MAX_WAIT_MS = 10 * 60 * 1_000; // 10-minute wall-clock ceiling
async function main(): Promise<void> {
const client = new CoderClient();
const created = await client.createSession({
task: 'Audit the repository for outdated dependencies and open a summary issue.',
workflowMode: 'loop',
loop: {
goal: 'Identify and report all packages more than two major versions behind.',
maxIterations: 10,
autoContinue: true, // advance each iteration without manual approval
allowDetached: true, // continue even when no client is attached
},
tags: ['deps', 'audit'],
});
console.log('Session created:', created.sessionId);
const deadline = Date.now() + MAX_WAIT_MS;
while (Date.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, POLL_MS));
const { loop } = await client.getLoopState(created.sessionId);
if (!loop) {
// Loop state isn't initialized yet. The session is still starting
continue;
}
const { status, iteration, maxIterations } = loop;
console.log(`iteration ${iteration}/${maxIterations ?? '?'}: ${status}`);
if (TERMINAL.has(status)) {
console.log('Loop finished with status:', status);
if (loop.summary) {
console.log('Summary:', loop.summary);
}
return;
}
if (status === 'awaiting_input') {
// The loop paused and is waiting for external input. Your app would handle this
// by prompting the user or sending a response through the Hub WebSocket or REST API
console.log('Loop is awaiting input. Exiting poll loop.');
return;
}
}
console.error('Timed out waiting for loop to complete.');
process.exitCode = 1;
}
void main();The Hub broadcasts loop.summary and loop.nextAction as the loop progresses, so your UI or downstream step can pick them up without holding a live controller socket.
When to Use This Pattern
Use loop mode when your app needs explicit iteration-aware workflow state, not just a long-running session.
- structured review or planning flows
- bounded research or implementation loops
- apps that need to display loop progress separately from normal session status
If you only need a normal interactive coding session, stick with workflowMode: 'standard'.
Key Points
- Loop mode is explicit. Sessions do not become loop-mode sessions automatically.
getLoopState()is the right place to inspect loop progress. Do not infer it only from top-level session status.- Keep the loop goal concrete. It becomes part of the state your app can show to users.
- Start with
autoContinue: falseunless you specifically want unattended advancement.
See Also
- Managing Coder Sessions with the SDK: Start with the standard session flow and core lifecycle
- Reconnecting to Existing Coder Sessions: Resume or inspect sessions that may still be live
- Coder Client: Full method reference for
createSession()andgetLoopState()