Managing Coder Sessions with the SDK

Create a Coder session, read its state, and manage its lifecycle with CoderClient

Drive Coder from your own code with CoderClient. Coder sessions run on a shared Hub, the real-time broadcast bus that apps, agents, and humans all subscribe to. The flow below creates a session, reads it back, and manages its lifecycle over time.

The examples below are standalone @agentuity/coder TypeScript snippets. Bun is the quickest way to run them locally. If you prefer Node, use tsx or another TypeScript runner.

The Pattern

Install the package:

npm install @agentuity/coder @agentuity/telemetry

If your app creates sessions from more than one place (an HTTP route, a background job, a webhook handler), construct a CoderClient once and share it:

typescriptsrc/lib/coder.ts
import { CoderClient } from '@agentuity/coder';
 
export const coder = new CoderClient();

With AGENTUITY_SDK_KEY set in your environment, this authenticates and discovers the Hub URL for your region. Pass orgId or region explicitly when your app needs to pin a specific one.

The standalone scripts on this page keep new CoderClient() inline so each one stays copy-pasteable. Move to a shared module once your app has more than one call site.

Start with a minimal script that covers discovery, session creation, and the first session read:

import { CoderClient } from '@agentuity/coder';
import { logger } from '@agentuity/telemetry';
 
const DEFAULT_TASK = 'Inspect this repo and explain the current architecture.';
function getTask(): string {
  const task = process.argv.slice(2).join(' ').trim();
  return task || DEFAULT_TASK;
}
 
async function main(): Promise<void> {
  // Let the SDK discover the Hub from AGENTUITY_SDK_KEY
  const client = new CoderClient();
  const task = getTask();
 
  try {
    const resolvedUrl = await client.getUrl(); 
    logger.info('Connected to Coder Hub:', resolvedUrl);
 
    const created = await client.createSession({ 
      task,
      workflowMode: 'standard',
      tags: ['architecture', 'docs-example'],
    });
    logger.info('Created session:', created.sessionId);
 
    // Read the session back to inspect the first lifecycle state
    const session = await client.getSession(created.sessionId); 
    logger.info(
      JSON.stringify(
        {
          sessionId: session.sessionId,
          label: session.label,
          status: session.status,
          bucket: session.bucket,
          workflowMode: session.workflowMode,
          runtimeAvailable: session.runtimeAvailable,
          controlAvailable: session.controlAvailable,
          wakeAvailable: session.wakeAvailable,
          historyOnly: session.historyOnly,
          liveExpected: session.liveExpected,
        },
        null,
        2
      )
    );
  } catch (error) {
    const message = error instanceof Error ? error.stack ?? error.message : String(error);
    logger.error('Failed to create or inspect the session');
    logger.error(message);
    process.exitCode = 1;
  }
}
 
void main();

This gives you two things you need right away:

  • a sessionId you can reuse in later calls or other apps
  • the first session detail from getSession()

What you should see

The exact values will vary, but a brand-new session usually looks like this:

{
  "sessionId": "codesess_f64101b6ad47",
  "label": "Inspect this repo and explain the current architecture.",
  "bucket": "provisioning",
  "workflowMode": "standard",
  "runtimeAvailable": false,
  "controlAvailable": false,
  "wakeAvailable": false,
  "historyOnly": false,
  "liveExpected": true
}

The Hub auto-derives label from the task prompt when the caller doesn't set one, so a fresh session always has a label to show in list views.

The exact status string varies and is not the field to branch on in this example. What matters is:

  • creation succeeded and you have a real sessionId
  • getSession() returns usable session detail immediately
  • bucket and the availability/history booleans tell your app whether the session is still provisioning, attachable, wakeable, or replay-only

Creating Sessions from Your App

In app code, the shape you usually reach for adds three fields on top of task: label, metadata, and env. These cover the pieces apps actually need: a short label for list UIs, correlation ids that tie the session back to your own records, and any credentials the sandbox-side agent reads at runtime.

typescriptsrc/lib/start-session.ts
import { coder } from './coder';
 
type StartSessionInput = {
  userId: string;
  conversationId: string;
  task: string;
  apiToken: string;
};
 
export async function startSession(input: StartSessionInput) {
  return coder.createSession({
    task: input.task,
    // label shows up in list UIs; set it when your task prompt is too long for display
    label: input.task.slice(0, 100),
    // metadata is for app correlation ids and app-owned session context
    metadata: {
      userId: input.userId,
      conversationId: input.conversationId,
    },
    // env is what the sandbox-side agent can read at runtime
    env: {
      API_TOKEN: input.apiToken,
    },
  });
}
FieldWhat it carries
taskThe prompt the agent executes, including any context you want it to act on
labelA short human string for list UIs and dashboards
metadataApp correlation ids and app-owned session context
envCredentials and runtime config the sandbox-side agent reads

When your app already resolved user or account context, the common split is: put resolved context into task, correlation ids into metadata, and integration credentials into env. env is fixed once the session is created, so prefer credentials the agent can refresh itself over short-lived access tokens.

Add an Output Contract

Coder sessions are long-running work, so make the expected result explicit before the session starts. A good task names the goal, inputs, files or reports, verification command, stopping rule, and missing-evidence behavior.

typescriptsrc/lib/start-release-review.ts
import { coder } from './coder';
 
type StartReleaseReviewInput = {
  runId: string;
  repoUrl: string;
  branch: string;
  files: readonly string[];
};
 
export async function startReleaseReview(input: StartReleaseReviewInput) {
  const resultPath = `agent-output/${input.runId}/release-review.json`;
  const reportPath = `agent-output/${input.runId}/release-review.md`;
 
  return coder.createSession({
    label: `Release review ${input.runId}`,
    workflowMode: 'standard',
    repo: {
      url: input.repoUrl,
      branch: input.branch,
    },
    metadata: {
      runId: input.runId,
      resultPath,
      reportPath,
    },
    tags: ['release-review', input.runId],
    task: [
      'Goal: review the selected files for release blockers and risky assumptions.',
      '',
      'Files:',
      ...input.files.map((file) => `- ${file}`),
      '',
      'Write these artifacts:',
      `- ${resultPath}`,
      `- ${reportPath}`,
      '',
      'Contract shape:',
      '{',
      '  "status": "completed" | "blocked",',
      '  "filesInspected": string[],',
      '  "findings": [{',
      '    "path": string,',
      '    "risk": string,',
      '    "sourceOfTruth": string,',
      '    "severity": "low" | "medium" | "high"',
      '  }],',
      '  "commandsRun": string[],',
      '  "missingEvidence": string[]',
      '}',
      '',
      'Success criteria:',
      '- Every finding cites a file path and source of truth.',
      '- Missing evidence is listed instead of turned into a claim.',
      '- The markdown report summarizes decisions, tradeoffs, and verification.',
      '',
      'Stop when:',
      '- Both outputs are written, or a blocker is proven with the exact command, file, or API response.',
      '',
      'Do not include hidden reasoning. Summarize findings, decisions, evidence, and tradeoffs.',
    ].join('\n'),
  });
}

createSession() returns session state, not completed files. Keep the sessionId, store the expected output paths in your own app state, then check event history, replay, or artifacts until the output contract is satisfied. Use the retrieval path approved for your product surface; do not assume a file read helper exists unless your SDK version exposes one.

Start from Files

Use files when your app wants to hand Coder a small repro, generated fixture, or one-off workspace. Keep the task prompt focused on the output contract. The session can stay live after the task finishes, so the contract is how your app decides whether the work is done.

typescriptscripts/start-coder-fix.ts
import { Buffer } from 'node:buffer';
import { CoderClient } from '@agentuity/coder';
import { logger } from '@agentuity/telemetry';
 
async function main(): Promise<void> {
  const client = new CoderClient();
  const runId = `calc-fix-${Date.now()}`;
  const resultPath = `agent-output/${runId}/result.json`;
  const reportPath = `agent-output/${runId}/report.md`;
 
  const created = await client.createSession({
    label: `Fix total() ${runId}`,
    workflowMode: 'standard',
    defaultAgent: 'builder',
    enabledAgents: ['builder', 'reviewer'],
    tags: ['docs-example', runId],
    metadata: {
      runId,
      resultPath,
      reportPath,
    },
    files: [
      {
        path: 'package.json',
        content: Buffer.from(
          JSON.stringify(
            {
              type: 'module',
              scripts: { test: 'bun test' },
              devDependencies: { '@types/bun': 'latest' },
            },
            null,
            2
          )
        ),
      },
      {
        path: 'src/calc.ts',
        content: Buffer.from(
          [
            'export function total(values: readonly number[]): number {',
            '  return values.reduce((sum, value) => sum - value, 0);',
            '}',
            '',
          ].join('\n')
        ),
      },
      {
        path: 'src/calc.test.ts',
        content: Buffer.from(
          [
            "import { expect, test } from 'bun:test';",
            "import { total } from './calc.ts';",
            '',
            "test('totals positive values', () => {",
            '  expect(total([2, 3, 5])).toBe(10);',
            '});',
            '',
          ].join('\n')
        ),
      },
    ],
    task: [
      'Goal: fix the TypeScript utility in this session workspace.',
      '',
      'Inputs:',
      '- src/calc.ts currently implements total incorrectly.',
      '- src/calc.test.ts is the verification command target.',
      '',
      'Do:',
      '- inspect the provided files',
      '- fix src/calc.ts',
      '- run bun test',
      `- write ${resultPath}`,
      `- write ${reportPath}`,
      '',
      'Result JSON shape:',
      '{',
      '  "status": "completed" | "blocked",',
      '  "changedFiles": string[],',
      '  "commandsRun": string[],',
      '  "verification": { "command": string, "passed": boolean, "outputExcerpt": string },',
      '  "blockers": string[]',
      '}',
      '',
      'Stop when bun test passes and the two output files are written, or when a blocker is proven with exact command output.',
    ].join('\n'),
  });
 
  logger.info('coder session started', {
    sessionId: created.sessionId,
    resultPath,
    reportPath,
  });
}
 
void main();

Example log shape:

{
  "sessionId": "codesess_23bc720766d7",
  "resultPath": "agent-output/calc-fix-123/result.json",
  "reportPath": "agent-output/calc-fix-123/report.md"
}

The two agent-selection fields do different jobs:

  • enabledAgents chooses the specialist roster for this session.
  • defaultAgent chooses the default route target when work is not explicitly routed elsewhere.
  • Omit both fields when you want the Hub's normal Coder team behavior.

After creation, store sessionId, resultPath, and reportPath with your app record. Then use the read APIs below from a UI refresh, background job, webhook flow, or scheduler.

const [session, history] = await Promise.all([
  client.getSession(sessionId),
  client.listEventHistory(sessionId, { limit: 50 }),
]);
 
const taskCompleted = history.events.some((event) => event.event === 'task_complete');
 
logger.info('coder session progress', {
  sessionId: session.sessionId,
  status: session.status,
  bucket: session.bucket,
  runtimeAvailable: session.runtimeAvailable,
  taskCompleted,
});

In that shape, status: "active" does not mean the task is unfinished. A session can remain live for attach or follow-up work after the task completion event and output contract are satisfied.

Reading Session State

Once you have a session ID, use the read APIs to build dashboards, admin tools, or a simple status view in another app.

import { CoderClient } from '@agentuity/coder';
import { logger } from '@agentuity/telemetry';
 
const sessionId = process.argv[2];
 
if (!sessionId) {
  throw new Error('Pass a session ID as the first argument, for example: bun example.ts codesess_123');
}
 
async function main(): Promise<void> {
  const client = new CoderClient();
 
  try {
    // Start with session detail; it's the fullest single view
    const session = await client.getSession(sessionId); 
 
    // Pull related data in parallel when you need more than session detail alone
    const [participants, history, replay] = await Promise.all([
      client.listParticipants(sessionId, { limit: 20 }), 
      client.listEventHistory(sessionId, { limit: 20 }), 
      client.getReplay(sessionId, { limit: 50 }), 
    ]);
 
    logger.info(
      JSON.stringify(
        {
          session: {
            sessionId: session.sessionId,
            label: session.label,
            status: session.status,
            bucket: session.bucket,
            workflowMode: session.workflowMode,
            historyOnly: session.historyOnly,
            runtimeAvailable: session.runtimeAvailable,
            controlAvailable: session.controlAvailable,
          },
          participantsCount: participants.participants.length,
          historyCount: history.events.length,
          replaySessionId: replay.sessionId,
        },
        null,
        2
      )
    );
  } catch (error) {
    const message = error instanceof Error ? error.stack ?? error.message : String(error);
    logger.error('Failed to read session state');
    logger.error(message);
    process.exitCode = 1;
  }
}
 
void main();

Fresh or idle sessions can legitimately return empty participants, event history, or replay data. That usually means the session has not produced much activity yet.

Listing Sessions for Dashboards or History Views

If your app needs a broader session list before it drills into one session, start with listSessions().

import { CoderClient } from '@agentuity/coder';
import { logger } from '@agentuity/telemetry';
 
async function main(): Promise<void> {
  const client = new CoderClient();
 
  try {
    const { sessions, total } = await client.listSessions({ 
      limit: 20,
      includeArchived: true,
    });
 
    logger.info(
      JSON.stringify(
        {
          total,
          sessions: sessions.map((session) => ({
            sessionId: session.sessionId,
            label: session.label,
            status: session.status,
            bucket: session.bucket,
            workflowMode: session.workflowMode,
            historyOnly: session.historyOnly,
          })),
        },
        null,
        2
      )
    );
  } catch (error) {
    const message = error instanceof Error ? error.stack ?? error.message : String(error);
    logger.error('Failed to list sessions');
    logger.error(message);
    process.exitCode = 1;
  }
}
 
void main();

Use listSessions() when you are building a dashboard or history view. Once the user picks a session, switch back to getSession(), replay, participants, or lifecycle methods. List items include status, bucket, workflow mode, and tags, but not the task prompt; that only appears on the full getSession() response. total can exceed sessions.length because the Hub counts a broader set than the SDK surfaces; render from sessions and treat total as a hint that more exist.

Pinning a GitHub Repo

When your app needs Coder to operate on a specific repository, use listGitHubAccounts() to retrieve the GitHub accounts connected via the GitHub App installation, then listGitHubRepos(accountId) to list repositories under that account. Pass the chosen repository to createSession() via the repo field.

import { CoderClient } from '@agentuity/coder';
import { logger } from '@agentuity/telemetry';
 
async function main(): Promise<void> {
  const client = new CoderClient();
 
  let github;
  try {
    // This depends on the GitHub App installations visible to the current org
    github = await client.listGitHubAccounts();
  } catch {
    throw new Error(
      'Check your Agentuity org and GitHub App installation, then run this again to list the GitHub accounts available to your org.'
    );
  }
 
  const { connected, accounts } = github;
  if (!connected) {
    throw new Error('Connect GitHub in the Agentuity Console first.');
  }
 
  if (accounts.length === 0) {
    throw new Error('No GitHub accounts connected. Install the Agentuity GitHub App first.');
  }
 
  // Pick the first account, or match by accountName in your own logic
  const account = accounts[0];
  if (!account) {
    throw new Error('No GitHub account selected.');
  }
 
  // List repos visible under that account
  const { repositories } = await client.listGitHubRepos(account.accountId);
  if (repositories.length === 0) {
    throw new Error(`No repositories found under account ${account.accountName}`);
  }
 
  // Pick a repository; this example just takes the first
  const repo = repositories[0];
  if (!repo) {
    throw new Error('No repository selected.');
  }
 
  // Create the session with the pinned repository reference
  // CoderSessionRepositoryRef accepts owner, name, and optionally branch
  const created = await client.createSession({
    task: 'Review the open pull requests and summarize any security-related changes.',
    workflowMode: 'standard',
    repo: {
      owner: repo.owner.login,
      name: repo.name,
      // branch: 'main', // optional: pin to a specific branch
    },
  });
 
  logger.info('Session:', created.sessionId, 'pinned to repo:', repo.fullName);
}
 
void main();

listGitHubAccounts() reflects the GitHub App installations active for the API key's org. If accounts is empty, install the Agentuity GitHub App from your org settings in the Agentuity Console and grant it access to the repositories you need, then try again.

Updating and Cleaning Up

Use updateSession() to relabel a session, retag it, adjust its visibility, or update its metadata. Then archive the session for later replay, or delete it.

import { CoderClient } from '@agentuity/coder';
import { logger } from '@agentuity/telemetry';
 
const sessionId = process.argv[2];
const action = process.argv[3] ?? 'archive';
 
if (!sessionId) {
  throw new Error(
    'Pass a session ID as the first argument, for example: bun example.ts codesess_123 archive'
  );
}
 
async function main(): Promise<void> {
  const client = new CoderClient();
 
  try {
    // updateSession is for app-level session detail, not runtime control
    await client.updateSession(sessionId, { 
      tags: ['docs', 'follow-up'],
      visibility: 'organization',
      // metadata carries app-owned session context for later lookup
      metadata: {
        reviewStage: 'follow-up',
      },
    });
 
    if (action === 'delete') {
      // Delete temporary sessions when you don't need replay later
      await client.deleteSession(sessionId); 
      logger.info(`Deleted ${sessionId}`);
      return;
    }
 
    // Archive by default so session history remains available for replay
    const archived = await client.archiveSession(sessionId); 
    logger.info(JSON.stringify({ sessionId, status: archived.status }, null, 2));
  } catch (error) {
    const message = error instanceof Error ? error.stack ?? error.message : String(error);
    logger.error('Failed to manage the session');
    logger.error(message);
    process.exitCode = 1;
  }
}
 
void main();

For quick local validation, deleting the session is fine:

await client.deleteSession(sessionId);

Archive the session instead when you want to inspect its history later:

await client.archiveSession(sessionId);

Key Points

  • Keep the sessionId from createSession(). Most follow-on APIs start there.
  • In app code, pair task with label for list UIs, metadata for correlation ids, and env for credentials the sandbox-side agent reads.
  • Put output paths, run ids, and expected artifacts in the task and metadata before the session starts.
  • Treat createSession() as the beginning of long-running work, not a synchronous result.
  • Use listSessions() for dashboards and history views, then getSession() for the chosen entry.
  • Use archiveSession() for sessions you may want to inspect later, and deleteSession() for temporary tests and cleanup.
  • To reconnect to an older session, switch to the reconnect flow instead of starting from listSessions().

See Also