# Let an LLM run your workspace

Everything on this site so far treats Signal as a data source: you fetch, you
decide, you act. This playbook is the other shape — giving a model a
conversational surface and letting it operate the workspace, with the guardrails
that make that safe rather than exciting.

**Read this before building it.** The interesting part is not what an agent can
do. It is what it is prevented from doing, and why.

---

## Two doors, and they are not equivalent

| | API key | Signed-in session |
|---|---|---|
| REST reads | yes, `read` scope | yes |
| Ask questions | yes, `copilot` scope | yes |
| Change anything | **never** | yes, with confirmation |

An `sk_sig_` key reaches the copilot's read tools and no others, whatever it is
asked and however it is asked. This is not a prompt instruction — the tool list
handed to the planner is filtered by how you authenticated, so a key-authed
caller cannot name a mutating tool, let alone invoke one.

That means: **an autonomous agent running on a key can analyse and report, and
cannot change your workspace.** For most automations that is exactly right, and
it is the configuration to reach for first.

---

## Pattern 1 — A read-only analyst (start here)

```python
import requests

def ask(question: str) -> dict:
    r = requests.post(
        "https://app.signal.geysera.com/agent-api/signal/copilot/ask",
        headers={"Authorization": f"Bearer {COPILOT_KEY}"},
        json={"question": question},
        timeout=90,
    )
    r.raise_for_status()
    return r.json()
```

The response is designed for a machine to consume, not just a human to read:

```json
{
  "answer": "Revenue in the last 7 days was ...",
  "refusal": null,
  "clarification": null,
  "question_kind": "descriptive",
  "plan": { "reasoning": "...", "calls": ["data_coverage", "revenue_and_aov"] },
  "trace": [ { "tool": "revenue_and_aov", "data": { "...": "..." } } ],
  "disclosures": ["..."],
  "limits": [],
  "warnings": [],
  "pending_confirmation": null,
  "mutated": [],
  "thread_id": null
}
```

Field by field, and each of these exists because of a specific failure:

- **`answer`** — prose. Null when the system declined.
- **`refusal`** — why it declined, in words. Not an error; do not retry it.
  Surface it.
- **`clarification`** — it needs one fact from you. Ask the user, do not guess.
- **`plan.calls`** — which tools ran. Log it; it is how you debug a bad answer.
- **`trace[].data`** — the structured results the prose was written from. If
  you want your own model to reason, feed it this, not `answer`.
- **`disclosures`** — caveats that change how the number should be read.
  **Propagate these.** Dropping them is how a caveated number becomes a
  confident wrong one downstream.
- **`mutated`** — which tools changed something. On a key, always empty.

## Handling refusals properly

```python
res = ask("Did the price change drive up AOV?")
if res["refusal"]:
    log.info("declined: %s", res["refusal"])
    return None          # NOT a retry, NOT an exception
if res["clarification"]:
    return need_input(res["clarification"])
use(res["answer"], res["disclosures"])
```

A refusal is the system refusing to answer from data that cannot support the
claim. Retrying it produces the same refusal and burns your rate limit;
treating it as a failure hides a correct answer.

---

## Pattern 2 — A supervised operator

To let a model *change* things, you need a signed-in session, and the workspace
must have switched on assistant-run changes — off by default, and an owner has
to turn it on in the dashboard. The assistant cannot enable it for itself.

When a turn would change something, it does not. It returns a plan:

```json
{
  "answer": "",
  "pending_confirmation": {
    "actions": [
      {
        "tool": "classify_account",
        "arguments": { "domain": "acme.com", "classification": "competitor" },
        "effect": "Mark acme.com as a competitor...",
        "irreversible": false
      }
    ],
    "irreversible": false,
    "approve_with": { "...": "..." }
  },
  "mutated": []
}
```

`mutated: []` and a non-null `pending_confirmation` together mean **nothing
has happened yet**. Show `effect` to a human — it is written for them, not for
you — and submit `approve_with` only if they agree.

Do not build an agent that auto-approves its own plans. The confirmation gate
is the entire safety model for writes; an agent that approves itself has
removed it and kept the ceremony.

---

## What it can and cannot do, and how to find out

Ask it:

```python
ask("what can you do?")
```

The answer is generated from the live tool registry rather than a written list,
so it cannot drift from the truth. For the machine-readable version:

```bash
curl -s https://app.signal.geysera.com/agent-api/capabilities
```

Unauthenticated, and it publishes the auth model, scopes, rate limits, the
error vocabulary, pagination bounds and the read-tool catalogue — all derived
from the code. Bootstrap from it rather than from anything written down,
including this page.

---

## Addressing things by name, and why that is not a style choice

Tools take names, not ids: a rule by its name, a webhook by its URL, an account
by its domain, a remembered fact by its key.

This is load-bearing. A plan's calls run concurrently with static arguments, so
no call can read another's output. A tool taking an opaque id would be
unreachable from a sentence — the planner cannot list things and use an id from
that list in the same turn. Ask for "the rule called Hot Leads", never "rule
7f3a".

Where a name is ambiguous, resolution refuses and lists what exists rather than
guessing. That refusal is correct behaviour; surface it.

---

## It remembers things, and you should check what

The assistant keeps short facts about your workspace and feeds them into later
turns, so a wrong one shapes answers you have not asked yet.

```python
ask("what have you remembered about us?")
```

On a real workspace this surfaced two domains someone had told it to exclude
from reports — invisible until asked, and quietly shaping every answer since.
Put this in your monthly review. To remove one: *"forget what you know about
X"*.

---

## Rate limits and cost

300 requests per minute per key. A copilot turn is seconds to tens of seconds —
it is doing real work, sometimes a Bayesian time series — so the practical
limit is wall-clock, not the quota. On 429 the `Retry-After` header is the real
remaining window, not a constant; honour it.

---

## The shape of a safe agent

1. Read-only key for anything unattended.
2. Log `plan.calls` and `trace` for every turn. When an answer is wrong, this
   is the only way to find out why.
3. Propagate `disclosures` everywhere the number goes.
4. Treat `refusal` as an answer.
5. Never auto-approve `pending_confirmation`.
6. Check what it remembers, monthly.
7. Bootstrap from `/capabilities`, not from documentation.

---

Next: [ask questions in natural language](../workflows/ask-questions-in-natural-language.md) ·
[watch your own data quality](../workflows/watch-your-own-data-quality.md)
