Docs

The three calls

decorate before every model call, report after it, and the note tool the model writes facts with, answered with NOTE_RESULT.

/docs/calls.md

decorate

Call it before every model call, including every call inside a tool loop.

TypeScript
const request = await memory.chat.decorate(memoryId, newMessages, requestFields);
Python
request = memory.chat.decorate(memory_id, new_messages, request_fields)

newMessages is everything new since the last call, in the API's own shape:

  • the first call of a conversation: your system or developer message and the user's message;
  • a new user turn: the model's last reply and the user's message;
  • inside a tool loop: the model's reply and your tool results.

Replies you already reported are recognised and not stored twice, so sending the reply again is safe and expected. System and developer messages are the instructions: the latest set you send replaces the stored one and always leads the request. Send them once and again only when they change.

requestFields is the rest of the request body as you would send it to OpenAI, without the history: model, tools, tool_choice, temperature, instructions and the rest. They pass through unchanged. previous_response_id and store are dropped, because we hold the history.

It returns the full request body, ready to submit with your own key. Its history has:

  1. your instructions;
  2. the facts block: facts remembered from history that has left the view, most active first;
  3. the history still in view, with each turn's recall block before its user message;
  4. the note ask, last, on calls where episodes are leaving.

The HTTP API also returns a memory object: the current turn, whether notes were requested, how many facts were recalled and are in view, and our estimate of the request size. The Server-Timing header carries each step's duration.

report

Call it after every model reply, with the response exactly as OpenAI returned it, usage included.

TypeScript
memory.chat.report(memoryId, reply);
Python
memory.chat.report(memory_id, reply)

It returns at once and never throws; failures go to the onError callback (on_error in Python). The reply is stored, notes in it are taken, and the work that can wait runs in the background: scoring facts, indexing what left the view, preparing recall for the next message.

The usage figures in the reply let us measure our size estimate against the provider's count, which keeps forgetting on target for your model. If a report is still in flight when the next decorate on the same memory starts, the SDK waits briefly for it.

The note tool

Add it to your tools once:

TypeScript
const tools = [memory.chat.noteTool, ...yourTools]; // or memory.responses.noteTool
Python
tools = [memory.chat.note_tool, *your_tools]  # or memory.responses.note_tool

When part of the history is leaving the model's view, the request ends with the note ask: an instruction to begin or end the response with one call to the note tool holding that history's facts. The model writes each fact as a knowledge-graph triple: a sentence that reads on its own, with its subject and object under their canonical names, and marks milestones: what was agreed, decided, done, blocked or next.

Nothing runs. The call itself is the note: its arguments reach us in the reply you report. Your handler answers every note call with noted as the tool result and does nothing else.

TypeScript
import { isNoteCall, NOTE_RESULT } from '@threadlessai/sdk';

const output = isNoteCall(call.function.name) ? NOTE_RESULT : await runTool(call);
Python
from threadlessai import NOTE_RESULT, is_note_call

output = NOTE_RESULT if is_note_call(call.function.name) else run_tool(call)

Answer it like any tool call: Chat Completions needs a tool message for every tool call before the next request, and Responses a function_call_output. Then call decorate again with the reply and the results, as you would after any tool call.

The tool is named memory_note. Its definition for Chat Completions:

JSON
{
  "type": "function",
  "function": {
    "name": "memory_note",
    "description": "Writes facts into this conversation's long-term memory. Nothing runs: the call itself is the note. When the system asks for facts from history that is leaving live context, answer with one call holding every fact: one entry per fact, a knowledge-graph triple readable on its own, identifiers and the source's own terms verbatim. Once those turns leave, these facts are what stays in view of them. When they hold nothing needed later, one entry with an empty note.",
    "parameters": {
      "type": "object",
      "properties": {
        "notes": {
          "type": "array",
          "description": "Every fact being written, one entry each.",
          "items": {
            "type": "object",
            "properties": {
              "note": {
                "type": "string",
                "description": "One fact, as a sentence readable on its own. Empty when the history leaving holds nothing needed later."
              },
              "subject": {
                "type": "string",
                "description": "The triple's subject entity, resolved to its canonical name (entity resolution): the name earlier facts already use, else the most specific name in the conversation."
              },
              "object": {
                "type": "string",
                "description": "The triple's object entity, resolved to its canonical name (entity resolution). The fact becomes the edge between subject and object in a knowledge graph."
              },
              "milestone": {
                "type": "boolean",
                "description": "True when the fact says where the work stands (a milestone reached, a point of alignment, what is next or blocked)."
              }
            },
            "required": [
              "note",
              "subject",
              "object",
              "milestone"
            ],
            "additionalProperties": false
          }
        }
      },
      "required": [
        "notes"
      ],
      "additionalProperties": false
    },
    "strict": true
  }
}

For Responses, the same fields sit at the top level of the tool:

JSON
{
  "type": "function",
  "name": "memory_note",
  "description": "Writes facts into this conversation's long-term memory. Nothing runs: the call itself is the note. When the system asks for facts from history that is leaving live context, answer with one call holding every fact: one entry per fact, a knowledge-graph triple readable on its own, identifiers and the source's own terms verbatim. Once those turns leave, these facts are what stays in view of them. When they hold nothing needed later, one entry with an empty note.",
  "parameters": {
    "type": "object",
    "properties": {
      "notes": {
        "type": "array",
        "description": "Every fact being written, one entry each.",
        "items": {
          "type": "object",
          "properties": {
            "note": {
              "type": "string",
              "description": "One fact, as a sentence readable on its own. Empty when the history leaving holds nothing needed later."
            },
            "subject": {
              "type": "string",
              "description": "The triple's subject entity, resolved to its canonical name (entity resolution): the name earlier facts already use, else the most specific name in the conversation."
            },
            "object": {
              "type": "string",
              "description": "The triple's object entity, resolved to its canonical name (entity resolution). The fact becomes the edge between subject and object in a knowledge graph."
            },
            "milestone": {
              "type": "boolean",
              "description": "True when the fact says where the work stands (a milestone reached, a point of alignment, what is next or blocked)."
            }
          },
          "required": [
            "note",
            "subject",
            "object",
            "milestone"
          ],
          "additionalProperties": false
        }
      }
    },
    "required": [
      "notes"
    ],
    "additionalProperties": false
  },
  "strict": true
}

Without the note tool

A call that asks for facts carries the note tool whether or not your request had it, so the model can always answer. Handle memory_note calls in your tool loop and answer them with noted (the SDKs' isNoteCall and NOTE_RESULT). If a run ends without the note it was asked for, that history stays leaving and the next call asks again. Only the model writes facts: it was there, so it writes the best ones.