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. Live keys start tl_live_, test keys tl_test_.
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.
import { Threadless } from '@threadlessai/sdk';
const memory = new Threadless(); // reads THREADLESS_API_KEY
const { id: memoryId } = await memory.memories.create({ metadata: { conversation: 'c_123' } });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
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
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
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
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), so one call with curl shows the memory working:
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 explains what to send and what comes back.
- Failure behaviour explains what happens when we cannot be reached: your model call still goes ahead.