Agents APIReference

Examples

Complete programs to copy and adapt. Each reads your key from LS_API_KEY and cleans up after itself.

Change a repository and get the patch

The agent adds a file based on what it finds in the repository, and the program saves the result as a patch you can apply or turn into a pull request. The shell is denied, so it runs without anyone watching.

import { writeFile } from "node:fs/promises"
import { LatentStack } from "@latentcode/sandbox-agents"

const client = new LatentStack({ apiKey: process.env.LS_API_KEY! })
const sessions = client.agents.sessions

const session = await sessions.create({
  agent: {
    model: "bedrock/us.anthropic.claude-sonnet-5",
    instructions: "Keep changes small.",
    // This task only needs the file tools. Denying the shell also means the agent never stops to ask.
    permissions: [{ action: "bash", effect: "deny" }],
  },
  environment: { type: "repos", repos: ["acme/demo"] },
  input: "Add a CONTRIBUTING.md that explains how to set up this project and run its tests, based on what you find in the repository.",
})

try {
  for await (const event of sessions.events(session.id)) {
    if (event.type === "text.ended") console.log(event.data?.text)
    if (event.type === "run.completed" || event.type === "run.failed") break
  }

  const patch = await sessions.artifact(session.id)
  await writeFile("contributing.patch", patch) // apply with: git apply contributing.patch
  console.log(patch) // a unified diff adding CONTRIBUTING.md
} finally {
  await sessions.delete(session.id)
}

Review code without changing it

A read-only reviewer. It can read files and run git, and nothing else: every other tool is denied, so it never waits for approval and can't modify the repository.

import { LatentStack } from "@latentcode/sandbox-agents"

const client = new LatentStack({ apiKey: process.env.LS_API_KEY! })
const sessions = client.agents.sessions

const session = await sessions.create({
  agent: {
    model: "bedrock/us.anthropic.claude-sonnet-5",
    instructions: "You review code. Read before you judge, name files and lines, and never modify files.",
    permissions: [
      { action: "*", effect: "deny" },                       // anything not allowed below is refused
      { action: "read", effect: "allow" },
      { action: "glob", effect: "allow" },
      { action: "grep", effect: "allow" },
      { action: "list", effect: "allow" },
      { action: "bash", resource: "git *", effect: "allow" },
      { action: "bash", resource: "git push*", effect: "deny" }, // later rules win
    ],
  },
  environment: { type: "repos", repos: ["acme/demo"] },
  input: "Review the most recent commit (git show HEAD). List up to three concrete risks or improvements.",
})

try {
  let review = ""
  for await (const event of sessions.events(session.id)) {
    if (event.type === "text.ended") review = String(event.data?.text)
    if (event.type === "run.completed" || event.type === "run.failed") break
  }
  console.log(review)

  const patch = await sessions.artifact(session.id)
  console.log(patch === "" ? "no files changed" : "files changed!")
} finally {
  await sessions.delete(session.id)
}

Make one change across several repositories

One session, two repositories, one patch per repository.

import { writeFile } from "node:fs/promises"
import { LatentStack } from "@latentcode/sandbox-agents"

const client = new LatentStack({ apiKey: process.env.LS_API_KEY! })
const sessions = client.agents.sessions

const session = await sessions.create({
  agent: {
    model: "bedrock/us.anthropic.claude-sonnet-5",
    instructions: "Keep changes minimal.",
    permissions: [{ action: "bash", effect: "deny" }], // file tools only
  },
  environment: {
    type: "repos",
    repos: [
      "acme/demo",                             // its default branch
      { repo: "acme/website", ref: "master" }, // a branch, tag or commit
    ],
  },
  input: "In each repository, add a line at the end of the README: 'Maintained by the platform team.'",
})

try {
  for await (const event of sessions.events(session.id)) {
    if (event.type === "run.completed" || event.type === "run.failed") break
  }

  const { data } = await sessions.artifacts(session.id)
  for (const change of data) {
    console.log(change.repo, (change.files ?? []).map((f) => `${f.status} ${f.file} +${f.additions} -${f.deletions}`))
    const patch = await sessions.artifact(session.id, "patch", { repo: change.repo! })
    await writeFile(`${change.repo!.split("/")[1]}.patch`, patch)
  }
} finally {
  await sessions.delete(session.id)
}

Chat in the terminal, approving commands

An interactive chat. The agent uses the default rules, so it asks before shell commands, and you answer each one with y, a (always) or n.

// A chat with an agent in your terminal. You approve its shell commands as they come up.
import * as readline from "node:readline/promises"
import { LatentStack } from "@latentcode/sandbox-agents"

const client = new LatentStack({ apiKey: process.env.LS_API_KEY! })
const sessions = client.agents.sessions
const terminal = readline.createInterface({ input: process.stdin, output: process.stdout })

const session = await sessions.create({
  agent: { model: "bedrock/us.anthropic.claude-sonnet-5" },
  input: await terminal.question("task> "),
})

try {
  for await (const event of sessions.events(session.id)) {
    if (event.type === "approval.requested" && !("seq" in event)) {
      const answer = await terminal.question(`run \`${(event.resources ?? []).join(" ")}\`? [y]es / [a]lways / [n]o `)
      const reply = answer === "a" ? "always" : answer === "y" ? "once" : "reject"
      await sessions.approve(session.id, event.request_id, reply)
    }
    if (event.type === "tool.called") console.log(`  · ${event.data?.tool}`)
    if (event.type === "text.ended") console.log(`\n${event.data?.text}\n`)
    if (event.type === "run.completed" || event.type === "run.failed") {
      const next = await terminal.question("task> (empty to quit) ")
      if (!next) break
      await sessions.message(session.id, next)
    }
  }
} finally {
  terminal.close()
  await sessions.delete(session.id)
}
task> Run `uname -s` in the shell and tell me the result.
  · bash
run `uname -s`? [y]es / [a]lways / [n]o y

The result of `uname -s` is: Linux

task> (empty to quit)

A long-lived assistant

One session per conversation, kept across runs of your program. It stores the session id and the last event number in a file. Each run sends a message and prints only the new reply. If the session paused in the meantime, the message wakes it with its files and conversation.

// A long-lived assistant: one session per conversation, kept across restarts of your program.
//   tsx assistant.ts "Start a notes.md with three ideas for a blog post."
//   tsx assistant.ts "Expand the second idea into an outline."     (minutes or days later)
import { existsSync, readFileSync, writeFileSync } from "node:fs"
import { LatentStack } from "@latentcode/sandbox-agents"

const client = new LatentStack({ apiKey: process.env.LS_API_KEY! })
const sessions = client.agents.sessions
const STATE = "conversation.json"

type Saved = { sessionId: string; position: number }
const saved: Saved | undefined = existsSync(STATE) ? JSON.parse(readFileSync(STATE, "utf8")) : undefined
const message = process.argv[2] ?? "Hello!"

let sessionId: string
let position = 0
if (saved) {
  // An idle session pauses after a while; a message wakes it with its files and history.
  ;({ sessionId, position } = saved)
  await sessions.message(sessionId, message)
} else {
  const session = await sessions.create({ agent: { model: "bedrock/us.anthropic.claude-sonnet-5" }, input: message })
  sessionId = session.id
}

for await (const event of sessions.events(sessionId, { after: position })) {
  if (!("seq" in event)) continue
  position = event.seq
  if (event.type === "text.ended") console.log(event.data?.text)
  if (event.type === "run.completed" || event.type === "run.failed") break
}

writeFileSync(STATE, JSON.stringify({ sessionId, position } satisfies Saved))

Python with asyncio

AsyncLatentStack has the same methods as LatentStack, awaited. Wrap the event stream in aclosing() so the connection closes when you break out early.

import asyncio
import os
from contextlib import aclosing

from latentcode_sandbox_agents import AsyncLatentStack


async def main() -> None:
    async with AsyncLatentStack(api_key=os.environ["LS_API_KEY"]) as client:
        sessions = client.agents.sessions
        session = await sessions.create(
            agent={"model": "bedrock/us.anthropic.claude-sonnet-5", "instructions": "Answer briefly."},
            input="Name three uses of a lighthouse.",
        )
        try:
            # aclosing() closes the stream when you break out early.
            async with aclosing(sessions.events(session["id"])) as events:
                async for event in events:
                    if event["type"] == "text.ended":
                        print(event["data"]["text"])
                    if event["type"] in ("run.completed", "run.failed"):
                        break
        finally:
            await sessions.delete(session["id"])


asyncio.run(main())

Run it in CI

Any example that doesn't wait for a person runs as-is in a CI job. Store your key as a secret and upload what the agent produced:

YAML
# .github/workflows/agent.yml
name: agent
on: workflow_dispatch
jobs:
  contributing:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/setup-python@v5
        with: { python-version: "3.12" }
      - run: pip install https://latentstack.dev/dl/latentcode_sandbox_agents-0.1.1-py3-none-any.whl
      - run: python fix_repo.py
        env:
          LS_API_KEY: ${{ secrets.LS_API_KEY }}
      - uses: actions/upload-artifact@v4
        with: { name: patch, path: contributing.patch }