Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ jobs:
${{ runner.os }}-bun-

- name: Install dependencies
run: bun install --frozen-lockfile
run: bun install --frozen-lockfile --ignore-scripts

- name: Deploy to Trigger.dev
working-directory: ./apps/sim
Expand Down Expand Up @@ -584,7 +584,7 @@ jobs:
bun-version: 1.3.13

- name: Install dependencies
run: bun install --frozen-lockfile
run: bun install --frozen-lockfile --ignore-scripts

- name: Create release
env:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/docs-embeddings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
${{ runner.os }}-bun-

- name: Install dependencies
run: bun install --frozen-lockfile
run: bun install --frozen-lockfile --ignore-scripts

- name: Process docs embeddings
working-directory: ./apps/sim
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/migrations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ jobs:
${{ runner.os }}-bun-

- name: Install dependencies
run: bun install --frozen-lockfile
run: bun install --frozen-lockfile --ignore-scripts

# The expression maps the explicit environment input to exactly one repo
# secret, so the job never holds another environment's database URL. An
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/publish-cli.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:
- name: Setup Node.js for npm publishing
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: '18'
node-version: '22'
registry-url: 'https://registry.npmjs.org/'

- name: Cache Bun dependencies
Expand All @@ -41,7 +41,7 @@ jobs:

- name: Install dependencies
working-directory: packages/cli
run: bun install --frozen-lockfile
run: bun install --frozen-lockfile --ignore-scripts

- name: Build package
working-directory: packages/cli
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish-ts-sdk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
${{ runner.os }}-bun-

- name: Install dependencies
run: bun install --frozen-lockfile
run: bun install --frozen-lockfile --ignore-scripts

- name: Run tests
working-directory: packages/ts-sdk
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ jobs:
path: ./.turbo

- name: Install dependencies
run: bun install --frozen-lockfile
run: bun install --frozen-lockfile --ignore-scripts

# Surfaces known CVEs in the dependency tree. Non-blocking until the
# existing advisory backlog is triaged, then flip to a required gate by
Expand Down Expand Up @@ -278,7 +278,7 @@ jobs:
fi

- name: Install dependencies
run: bun install --frozen-lockfile
run: bun install --frozen-lockfile --ignore-scripts

- name: Build application
env:
Expand Down
65 changes: 18 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,71 +75,42 @@ Docker must be installed and running. Use `-p, --port <port>` to run Sim on a di

## Self-hosting

### Docker Compose
**Requirements:** [Bun](https://bun.sh/) and [Docker](https://www.docker.com/).

```bash
git clone https://github.com/simstudioai/sim.git && cd sim
docker compose -f docker-compose.prod.yml up -d
```

Open [http://localhost:3000](http://localhost:3000)

Sim also supports local models via [Ollama](https://ollama.ai) and [vLLM](https://docs.vllm.ai/). See the [Docker self-hosting docs](https://docs.sim.ai/self-hosting/docker) for setup details.

### Manual Setup

**Requirements:** [Bun](https://bun.sh/), [Node.js](https://nodejs.org/) v20+, PostgreSQL 12+ with [pgvector](https://github.com/pgvector/pgvector)

1. Clone and install:

```bash
git clone https://github.com/simstudioai/sim.git
cd sim
bun install
bun run prepare # Set up pre-commit hooks
bun run setup
```

2. Set up PostgreSQL with pgvector:
`bun run setup` is an interactive wizard: it provisions the database, generates secrets, writes your `.env` files, connects a Chat API key, and starts Sim the way you choose:

```bash
docker run --name simstudio-db -e POSTGRES_PASSWORD=your_password -e POSTGRES_DB=simstudio -p 5432:5432 -d pgvector/pgvector:pg17
```
- **Local dev** — run from source to contribute or hack on Sim
- **Docker Compose** — a self-contained instance for testing self-hosting
- **Kubernetes (Helm)** — deploy to a local cluster

Or install manually via the [pgvector guide](https://github.com/pgvector/pgvector#installation).
When it finishes, open [http://localhost:3000](http://localhost:3000).

3. Configure environment:
Manage your install with `bun run sim`:

```bash
cp apps/sim/.env.example apps/sim/.env
# Create your secrets
perl -i -pe "s/your_encryption_key/$(openssl rand -hex 32)/" apps/sim/.env
perl -i -pe "s/your_internal_api_secret/$(openssl rand -hex 32)/" apps/sim/.env
perl -i -pe "s/your_api_encryption_key/$(openssl rand -hex 32)/" apps/sim/.env
# DB configs for migration
cp packages/db/.env.example packages/db/.env
# Edit both .env files to set DATABASE_URL="postgresql://postgres:your_password@localhost:5432/simstudio"
bun run sim start | stop | restart # bring your install up / down / cycle
bun run sim status # what's installed and healthy
bun run sim logs # follow logs
bun run sim doctor # diagnose configuration problems
bun run sim down # remove containers (data kept)
bun run sim reset # archive .env and wipe managed data
```

4. Run migrations:
`sim` detects how you're running (Docker Compose, local dev, or Kubernetes) and acts accordingly.

```bash
cd packages/db && bun run db:migrate
```
Prefer a bare `sim`? Run `bun link` once — but note `sim` lands in `~/.bun/bin`, which Homebrew's bun doesn't add to your PATH, so you may need `export PATH="$HOME/.bun/bin:$PATH"` in your shell profile.

5. Start development servers:

```bash
bun run dev:full # Starts Next.js app and realtime socket server
```

Or run separately: `bun run dev` (Next.js) and `cd apps/sim && bun run dev:sockets` (realtime).
Sim also supports local models via [Ollama](https://ollama.ai) and [vLLM](https://docs.vllm.ai/). See the [self-hosting docs](https://docs.sim.ai/self-hosting/docker) for details.

## Chat API Keys

Chat is a Sim-managed service. To use Chat on a self-hosted instance:

- Go to https://sim.ai → Settings → Chat keys and generate a Chat API key
- Set `COPILOT_API_KEY` environment variable in your self-hosted apps/sim/.env file to that value
Chat is a Sim-managed service. `bun run setup` connects a Chat API key for you — sign in when it opens your browser and the key is stored automatically. To view, create, or revoke keys later, go to [sim.ai/account/settings/chat-keys](https://sim.ai/account/settings/chat-keys).

## Environment Variables

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Verified Domains let organization owners and admins on Enterprise plans prove th

## Verify a domain

Go to **Settings → Security → Verified domains** in your organization settings.
Go to **Settings → Security → Single sign-on** in your organization settings. Domains are managed in the **Verified domains** section at the top of that page, directly above the identity provider configuration.

1. Enter the domain, for example `acme.com`, and click **Add domain**.
2. Sim shows a DNS **TXT record** to publish — a host (`_sim-challenge.acme.com`) and a unique value (`sim-domain-verification=…`).
Expand Down
114 changes: 103 additions & 11 deletions apps/docs/content/docs/en/workflows/deployment/agent-events.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,51 @@ Agent blocks can emit more than answer text while they run: **provider-exposed t
Sim does **not** invent thinking for providers that do not stream it. Bedrock Converse, many OpenAI-compat models, and non-reasoning chat models stay text-only (plus tools when a live tool loop is wired).
</Callout>

## Protocol opt-in vs. event gates (public chat / simple SSE)
## Two independent switches

Sending the header is a statement about the **client**: it understands v1 framing, so answer text can stream live and be retracted with `chunk_reset`. That alone does not expose anything — it only changes cadence, and it applies even when both event policies are off.
Agent events are governed by two things that do not depend on each other.

Thinking and tool frames need the header **plus** their own deployment policy:
**Policy decides which frames exist.** `includeThinking` turns on `thinking` frames, `includeToolCalls` turns on `tool` frames, and both default to **off**.

1. Thinking frames require chat `includeThinking` (default **off**).
2. Tool lifecycle frames require chat `includeToolCalls` (default **off**).
3. Both require the request to opt in with:
| Surface | Policy source |
|---------|---------------|
| Deployed chat (`/api/chat/{identifier}`) | The chat deployment's **Thinking** and **Tool calls** toggles |
| Workflow API (`/api/workflows/{id}/execute`) | Per-request `includeThinking` / `includeToolCalls` in the body |

**The header declares the protocol version.** Sending it says the client understands v1 framing:

```http
X-Sim-Stream-Protocol: agent-events-v1
```

Legacy clients that omit the header keep today’s text-only SSE even if either deployment policy is enabled. The hosted chat UI always sends the header for its own deployments, so hosted chats stream token by token regardless of the toggles.
It does two things. It switches answer text to live token-by-token `chunk` frames that `chunk_reset` can retract, and it is **required** for any `thinking` or `tool` frame — a client that never declared a version has no contract for their shape, so it keeps the text-only stream it already understands.

Omitting the header is always valid and always safe: you get settled final-turn text and no agent-event frames, which is what every pre-existing integration receives. The response echoes the header back when the protocol was negotiated.

The header alone exposes nothing, so a chat with both policies off still streams token by token.

<Callout type="warn">
On the workflow API, setting `includeThinking` or `includeToolCalls` **without** the header is rejected with `400`. The flags would otherwise be a silent no-op, which is the failure mode this protocol exists to avoid. Deployed chat degrades instead of rejecting, because there the policy comes from the deployment rather than the request.
</Callout>

### Workflow API

```bash
curl -N https://sim.ai/api/workflows/{id}/execute \
-H "X-API-Key: $SIM_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Sim-Stream-Protocol: agent-events-v1" \
-d '{
"stream": true,
"selectedOutputs": ["agent_1.content"],
"includeThinking": true,
"includeToolCalls": true
}'
```

Both flags default to `false`, so an existing integration receives exactly the frames it does today. The header is required whenever either flag is set; omit it and the request is rejected with `400` rather than silently downgraded.

Two differences from deployed chat are worth knowing. Tool frames carry the name and status only on both surfaces, but the API's terminal `final` envelope keeps tool **arguments and results** — public chat redacts them. And thinking is delivered only as `thinking` frames; it is stripped from `providerTiming.timeSegments` in the envelope on every surface, so enabling the policy is the only way to receive it.

## Simple SSE frame shapes

Expand All @@ -47,8 +77,8 @@ Answer text stays on `chunk`. Thinking and tools never reuse `chunk` (so older c

During a live tool loop, the model can’t be classified mid-turn: text it emits may turn out to be the final answer or preamble before a tool call (the stop reason arrives only at turn end).

- **Opted-in clients** (protocol header; no event policy required) receive answer text as `chunk` frames **live**, token by token. If the turn then resolves to tool calls, a `chunk_reset` frame tells the client to discard that block’s streamed text — the final turn re-streams live after tools settle. Append `chunk`, honor `chunk_reset`, and the displayed answer always converges to the block’s final content.
- **Legacy clients** (no header) never see provisional text: only settled final-turn text is emitted as `chunk`, delivered in one piece when the turn completes. Honoring `chunk_reset` is what buys live cadence, so send the header if you want it.
- **Clients sending the protocol header** (no event policy required) receive answer text as `chunk` frames **live**, token by token. If the turn then resolves to tool calls, a `chunk_reset` frame tells the client to discard that block’s streamed text — the final turn re-streams live after tools settle. Append `chunk`, honor `chunk_reset`, and the displayed answer always converges to the block’s final content.
- **Clients without the header** never see provisional text: only settled final-turn text is emitted as `chunk`, delivered in one piece when the turn completes. Honoring `chunk_reset` is what buys live cadence, so send the header if you want it.

Logs, memory, and the block’s `content` output always contain final-turn text only — intermediate preamble is never persisted.

Expand All @@ -62,7 +92,7 @@ Canvas execution-events `stream:chunk`, `stream:chunk_reset`, `stream:thinking`,

## Canvas (draft Run)

When you click **Run** in the builder, the execution-events SSE path forwards the same sink. The canvas is always opted in — it does not send (or need) the `X-Sim-Stream-Protocol` header; the dual gate applies only to the public chat / simple SSE surface:
When you click **Run** in the builder, the execution-events SSE path forwards the same sink. The canvas is always opted in — it does not send (or need) the `X-Sim-Stream-Protocol` header, and the policy switches do not apply to it:

- `stream:thinking` — `{ blockId, text }`
- `stream:tool` — `{ blockId, phase, id, name, status? }`
Expand All @@ -73,7 +103,7 @@ The terminal output panel shows Thinking / Tools chrome above the block output w

## Chat deployment toggles

In **Deploy → Chat**, enable **Include thinking** for provider-exposed thinking and **Include tool calls** for tool names and lifecycle status. The switches are independent, and both event types still require the protocol header. Neither switch affects answer-text cadence.
In **Deploy → Chat**, enable **Include thinking** for provider-exposed thinking and **Include tool calls** for tool names and lifecycle status. The switches are independent of each other, and both still require the client to send the protocol header. Neither switch affects answer-text cadence.

Tool arguments and results are never exposed to a public chat — not in lifecycle frames, and not through the terminal `final` envelope, where the block's own tool calls are reduced to the same name-and-lifecycle shape. The authenticated workflow API still returns full tool results. Redeploy or update the chat after changing Agent models or tools.

Expand All @@ -89,6 +119,68 @@ Per-model support is generated from the model registry on the [Agent block page]
| OpenAI-compat (Groq, DeepSeek, …) | Only if vendor streams `reasoning` / `reasoning_content` | Live loop where wired (e.g. Groq, DeepSeek) |
| Bedrock | Not invented | Yes when streaming tool loop is used |

## Consuming the stream

A conforming client owes the stream four things:

1. **Discriminate before appending.** Only a frame with **no** `event` field is answer text. Checking `event === undefined` rather than "has a `chunk` field" is what keeps future frame types from leaking into the answer.
2. **Accumulate per `blockId`.** A workflow can stream more than one block; frames interleave.
3. **Honor `chunk_reset`** if you sent the protocol header. Clear that block's accumulated text — it belonged to a turn that resolved to tool calls, and the final turn re-streams.
4. **Stop at the terminal frame.** Exactly one of `final` or `error` arrives, followed by the literal `"[DONE]"` sentinel. `stream_error` is *not* terminal.

### Reference client

```ts
type Frame = Record<string, unknown>

async function consume(response: Response) {
const reader = response.body!.getReader()
const decoder = new TextDecoder()
const answers = new Map<string, string>()
const thinking = new Map<string, string>()
let buffer = ''

while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })

// SSE frames are newline-delimited; keep the trailing partial line.
const lines = buffer.split('\n')
buffer = lines.pop() ?? ''

for (const line of lines) {
if (!line.startsWith('data: ')) continue
const payload = line.slice(6)

const frame = JSON.parse(payload) as Frame | string
if (frame === '[DONE]') return { answers, thinking }

const { blockId, event } = frame as { blockId?: string; event?: string }

if (event === undefined && typeof frame.chunk === 'string') {
answers.set(blockId!, (answers.get(blockId!) ?? '') + frame.chunk)
} else if (event === 'chunk_reset') {
answers.set(blockId!, '')
} else if (event === 'thinking') {
thinking.set(blockId!, (thinking.get(blockId!) ?? '') + String(frame.data))
} else if (event === 'tool') {
// frame.phase is 'start' | 'end'; frame.status is set on 'end'.
renderToolChip(frame)
} else if (event === 'final') {
// Terminal. frame.data.success may be false with frame.data.error.
} else if (event === 'error') {
throw new Error(String(frame.error))
} else if (event === 'stream_error') {
// Non-terminal: log and keep reading.
}
}
}
}
```

Unknown `event` values should be ignored rather than treated as errors — that is what lets new frame types ship without breaking existing clients.

## Example (public chat)

<Tabs items={['cURL']}>
Expand Down
Loading
Loading