A small demo repository demonstrating how to use the Letta Agent SDK in Tangled CI.

Files
README.md

tangled-letta #

Teaching demo. This repository runs the Letta Agent SDK from a Tangled pipeline. The agent inspects a Letta-managed Cloud sandbox, then files a boolean pipeline verdict through a runner-local tool.

This is a wiring example, not a general CI agent.

The two computers #

A run uses two separate execution environments:

Computer Role Visible files
Tangled runner Executes .tangled/workflows/agent-ci.yaml and the report_pipeline tool The pipeline checkout, public environment values, and repository secrets
Letta Cloud sandbox Runs the agent's shell and file tools Only files created or cloned inside the sandbox

The runner checkout is not mounted in the sandbox. A local runner path such as process.cwd() therefore has no meaning to the agent. See managed sandboxes.

The pipeline performs the following steps:

push or manual trigger
    → Tangled checks out the repository
    → npm ci from package-lock.json
    → local type checks and contract tests
    → client.createSession(LETTA_AGENT_ID, { tools: [report_pipeline] })
    → session.send(message) and session.stream()
        → new conversation on the pinned Cloud agent
        → managed sandbox fetches the runner's exact commit
        → sandbox reports its HEAD and tracked top-level names
        → agent calls report_pipeline({ success: true | false })
    → runner requires sandbox git evidence, then accepts the boolean verdict

Each run starts a new conversation on the same agent. Agent-level memory remains in Letta Cloud and may change between runs. Each successful trigger also makes a model call and may incur Letta usage charges. Reuse an agent when cross-pipeline memory is intentional; create a dedicated agent when the pipeline should be isolated.

One-time setup #

  1. Install the locked dependencies:

    npm ci --ignore-scripts
    
  2. Create a Letta API key at platform.letta.com/api-keys.

  3. Choose which Letta Cloud agent should run CI:

    Reuse an existing agent. Put any existing agent-… ID in the workflow. One agent can serve several repositories and CI pipelines. Every run opens a new conversation while the agent's identity and memory remain shared. Each pipeline can choose its own model for that conversation. Reuse an agent only across repositories in the same trust boundary because every pipeline can send turns to and potentially change the shared memory. Use separate agents when repositories should not influence the same memory or behavior.

    Create a dedicated agent. Run the included provisioning script:

    export LETTA_API_KEY='…'
    npm run create-agent
    # LETTA_AGENT_ID=agent-…
    
  4. Copy the chosen agent ID into .tangled/workflows/agent-ci.yaml as environment.LETTA_AGENT_ID. Agent names are not unique; the ID selects the exact agent.

  5. Optionally set environment.LETTA_MODEL in the workflow to choose the model for each new CI conversation:

    environment:
      LETTA_AGENT_ID: "agent-…"
      LETTA_MODEL: "letta/auto"
    

    The script passes this value as createSession(agentId, { model }), a per-conversation option documented in the SDK reference. Omit it to use the model configured on the agent. Model IDs are public configuration, not secrets. Use /model in the Letta app or CLI to see available models; see models.

    This example uses letta/auto, Letta's Cloud-managed automatic model route. Free plans include limited Letta Auto usage, and Pro or Teams Pro plans include quota; automated API-key workloads can follow developer usage pricing. See pricing for the current plan boundaries.

  6. Open this repository's Settings → Pipelines page and add a secret named LETTA_API_KEY.

  7. Push to main, or start a manual run from the Pipelines page.

The API key is a secret. Keep it in Tangled's repository secrets. The agent ID and model ID are public configuration and belong in the workflow's public environment: map. Tangled explicitly warns against putting secrets in that map; see environment.

Repository files #

Path Role
.tangled/workflows/agent-ci.yaml Installs the lockfile, runs local checks, and starts the Cloud turn
scripts/create-agent.ts Creates the dedicated agent once and prints its ID
scripts/ci-agent.ts Runs one new conversation, registers report_pipeline, and captures streamed tool events
scripts/ci-contract.ts Validates Tangled metadata, shell quoting, sandbox evidence, and the pipeline verdict
tests/ci-contract.test.ts Proves missing sandbox evidence and success: false fail
package-lock.json Fixes the complete npm dependency graph used by CI

The script imports @letta-ai/letta-agent-sdk/client, the portable entry that talks to Letta Cloud without starting a local App Server. The published SDK package still installs its local-backend dependency graph, so the workflow uses --ignore-scripts to prevent unused node-pty native rebuilds.

The script uses createSession(), send(), and stream() because the one-shot prompt() helper returns only the terminal result. Streaming exposes the tool-call and tool-result events needed by this CI check (sessions). report_pipeline is a custom SDK tool, so it executes in the Tangled runner rather than the sandbox (client tools). success: false throws from that tool and fails the turn; success: true still requires sandbox git evidence so the agent cannot pass by reporting success from the prompt alone. Each session starts a new conversation on the supplied agent ID. The turn uses permissionMode: "unrestricted" so the sandbox can run Git without an interactive approval callback (permissions). The client requests terminateOnClose: true because this short-lived session exclusively owns its sandbox (deployment).

What a green run proves #

The runner computes its own commit SHA and tracked top-level names before calling the SDK. The agent then inspects the sandbox and calls report_pipeline({ success }). A green run requires both a successful success: true call and streamed sandbox tool results that contain that SHA and every tracked top-level name. Missing tool events, failed sandbox tools, a missing verdict, or success: false exits nonzero.

A successful push currently ends with output shaped like this:

agent: agent-…
model: letta/auto
conversation: conv-…
duration_ms: 12345
tool_receipts: <count> successful
pipeline: success
checkout: <sha>
files: .gitignore, .tangled, LICENSE, README.md, package-lock.json, package.json, scripts, tests, tsconfig.json

The streamed sandbox events confirm the expected Git state. The boolean tool is the pipeline verdict. The command does not review the repository or authorize deployment.

If LETTA_API_KEY or LETTA_AGENT_ID is missing, the workflow exits before calling the SDK. A missing TANGLED_REPO_URL, invalid repository URLs, non-hexadecimal SHAs, trigger/checkout mismatches, failed agent turns, and success: false also fail the step.

Watch the current run from a checkout:

tg pipeline list
tg pipeline status
tg pipeline logs

You can also use the repository's Pipelines page. After a push, the knot prints an SSH log command.

Run checks locally #

The local gate makes no Letta API call:

npm ci --ignore-scripts
npm run check

To run the Cloud turn locally, provide the same agent ID and an HTTPS clone URL. The current local HEAD must already exist at that URL because the sandbox fetches the exact commit.

export LETTA_API_KEY='…'
export LETTA_AGENT_ID='agent-…'
export TANGLED_REPO_URL='https://tangled.org/<handle>/tangled-letta'
export LETTA_MODEL='letta/auto' # optional conversation override
npm run ci-agent

Without TANGLED_PIPELINE_KIND, the prompt labels the run as local. The verdict is still report_pipeline({ success }).

Documentation #

Letta

Tangled