# Quickstart

> Install the SDK, create a memory, and run an agent loop through decorate and report, in TypeScript or Python, for Chat Completions or Responses.

## 1. Install and set your key

Get a project key from the [console](https://threadlessai.com/app). Live keys start `tl_live_`, test keys `tl_test_`.

```bash
npm install @threadlessai/sdk        # TypeScript
pip install threadlessai          # Python

export THREADLESS_API_KEY=tl_test_...
```

Your OpenAI key stays in your process. Requests go from your code straight to OpenAI; they never pass through us.

## 2. Create a memory per conversation

A memory holds one conversation. Create it when the conversation starts and keep its id next to your conversation id.

```ts
import { Threadless } from '@threadlessai/sdk';

const memory = new Threadless(); // reads THREADLESS_API_KEY
const { id: memoryId } = await memory.memories.create({ metadata: { conversation: 'c_123' } });
```

```python
from threadlessai import Threadless

memory = Threadless()  # reads THREADLESS_API_KEY
memory_id = memory.memories.create(metadata={"conversation": "c_123"})["id"]
```

## 3. Run the loop: Chat Completions

Each call sends only what is new since the last one: the user's message, or the model's reply plus your tool results. Send your system message on the first call and whenever it changes; it is kept and always leads the request.

### TypeScript

```ts
import OpenAI from 'openai';
import { isNoteCall, NOTE_RESULT, Threadless } from '@threadlessai/sdk';

const openai = new OpenAI(); // reads OPENAI_API_KEY
const memory = new Threadless();
const tools = [memory.chat.noteTool /* , ...your own tools */];

export async function send(memoryId: string, text: string) {
  let newMessages: Array<Record<string, unknown>> = [{ role: 'user', content: text }];
  for (;;) {
    const request = await memory.chat.decorate(memoryId, newMessages, { model: 'gpt-5', tools });
    const reply = await openai.chat.completions.create(
      request as unknown as OpenAI.ChatCompletionCreateParamsNonStreaming,
    );
    memory.chat.report(memoryId, reply);

    const message = reply.choices[0].message;
    if (!message.tool_calls?.length) return message.content;
    newMessages = [{ ...message }];
    for (const call of message.tool_calls) {
      if (call.type !== 'function') continue;
      const output = isNoteCall(call.function.name) ? NOTE_RESULT : await runTool(call);
      newMessages.push({ role: 'tool', tool_call_id: call.id, content: output });
    }
  }
}
```

### Python

```python
from openai import OpenAI
from threadlessai import NOTE_RESULT, Threadless, is_note_call

openai = OpenAI()  # reads OPENAI_API_KEY
memory = Threadless()
tools = [memory.chat.note_tool]  # plus your own tools

def send(memory_id: str, text: str):
    new_messages = [{"role": "user", "content": text}]
    while True:
        request = memory.chat.decorate(memory_id, new_messages, {"model": "gpt-5", "tools": tools})
        reply = openai.chat.completions.create(**request)
        memory.chat.report(memory_id, reply)

        message = reply.choices[0].message
        if not message.tool_calls:
            return message.content
        new_messages = [message.model_dump(exclude_none=True)]
        for call in message.tool_calls:
            output = NOTE_RESULT if is_note_call(call.function.name) else run_tool(call)
            new_messages.append({"role": "tool", "tool_call_id": call.id, "content": output})
```

## 4. Or run the loop: Responses

The same loop with `responses`: items instead of messages, `function_call_output` for tool results.

### TypeScript

```ts
import OpenAI from 'openai';
import { isNoteCall, NOTE_RESULT, Threadless } from '@threadlessai/sdk';

const openai = new OpenAI();
const memory = new Threadless();
const tools = [memory.responses.noteTool /* , ...your own tools */];

export async function send(memoryId: string, text: string) {
  let newItems: Array<Record<string, unknown>> = [{ role: 'user', content: text }];
  for (;;) {
    const request = await memory.responses.decorate(memoryId, newItems, { model: 'gpt-5', tools });
    const reply = await openai.responses.create(
      request as OpenAI.Responses.ResponseCreateParamsNonStreaming,
    );
    memory.responses.report(memoryId, reply);

    const calls = reply.output.filter((item) => item.type === 'function_call');
    if (calls.length === 0) return reply.output_text;
    newItems = reply.output.map((item) => ({ ...item }));
    for (const call of calls) {
      const output = isNoteCall(call.name) ? NOTE_RESULT : await runTool(call);
      newItems.push({ type: 'function_call_output', call_id: call.call_id, output });
    }
  }
}
```

### Python

```python
from openai import OpenAI
from threadlessai import NOTE_RESULT, Threadless, is_note_call

openai = OpenAI()
memory = Threadless()
tools = [memory.responses.note_tool]  # plus your own tools

def send(memory_id: str, text: str):
    new_items = [{"role": "user", "content": text}]
    while True:
        request = memory.responses.decorate(memory_id, new_items, {"model": "gpt-5", "tools": tools})
        reply = openai.responses.create(**request)
        memory.responses.report(memory_id, reply)

        calls = [item for item in reply.output if item.type == "function_call"]
        if not calls:
            return reply.output_text
        new_items = [item.model_dump(exclude_none=True) for item in reply.output]
        for call in calls:
            output = NOTE_RESULT if is_note_call(call.name) else run_tool(call)
            new_items.append({"type": "function_call_output", "call_id": call.call_id, "output": output})
```

## 5. Check it worked

The SDK's `decorate` returns the request body only. The HTTP API returns a `memory` object beside it ([DecorateResult](https://threadlessai.com/docs/api-reference#decorateresult)), so one call with `curl` shows the memory working:

```bash
MEMORY_ID=$(curl -s https://api.threadlessai.com/v1/memories -X POST \
  -H "Authorization: Bearer $THREADLESS_API_KEY" -H 'content-type: application/json' -d '{}' \
  | sed -E 's/.*"id":"([^"]+)".*/\1/')

curl -s https://api.threadlessai.com/v1/memories/$MEMORY_ID/decorate \
  -H "Authorization: Bearer $THREADLESS_API_KEY" -H 'content-type: application/json' \
  -d '{"format":"chat.completions","messages":[{"role":"system","content":"You are terse."},{"role":"user","content":"Hello"}],"request":{"model":"gpt-5"}}'
```

On the first call of a new memory, `memory.turn` is `1` and the request's history is what you sent. As the conversation grows, `facts_in_view` and `recalled` rise, and `notes_requested` is true on calls that ask the model for notes.

`memory.memories.get(memoryId)` shows a memory's size and activity: turns, messages, episodes by state, facts and stored bytes.

## Next

- [The three calls](https://threadlessai.com/docs/calls) explains what to send and what comes back.
- [Failure behaviour](https://threadlessai.com/docs/failure-behaviour) explains what happens when we cannot be reached: your model call still goes ahead.
