Cheela Labs
CONCEPTS

Executions

One execution is one agent loop: the model runs, calls capabilities, reads the results, and runs again until it has an answer. However many steps that takes, it bills as one.

What an execution is

You send messages. Cheela returns a transcript, token counts, a finish reason, and a duration. Between those two points it may have called the model a dozen times and your endpoint twenty more.

TypeScript
interface ExecutionResult {
  executionId: string;
  status: "completed" | "failed";
  messages: readonly Message[];
  finishReason: "stop" | "tool_calls" | "length" | "error";
  inputTokens: number;
  outputTokens: number;
  totalTokens: number;
  durationMs: number;
  error?: string;
}

The loop

  1. Call the model with the transcript so far and your capabilities as tools.
  2. Append the model’s message to the transcript, and its shape to the trace.
  3. If it contains no tool calls, stop — that message is the answer.
  4. Otherwise dispatch each tool call to your endpoint, in parallel up to a cap, and append the results.
  5. Go back to step 1.

Token usage accumulates across every step rather than being overwritten, because the execution is billed for all of them.

The step budget is 25 by default

A loop that reaches it stops with finishReason: "length" — a budget outcome, not a crash. Together with the parallel-call cap, this is what bounds the cost of a single execution, since no quota check happens between steps.

Messages and parts

A message has a role and a list of parts. Parts are how tool calls travel in the same structure as text.

TypeScript
type MessageRole = "system" | "user" | "assistant" | "tool";

type MessagePart =
  | { type: "text"; content: string }
  | { type: "tool_call"; id: string; name: string; input: unknown }
  | { type: "tool_result"; toolCallId: string; name: string; output: unknown };

interface Message {
  role: MessageRole;
  parts: readonly MessagePart[];
}

A minimal request is one user message with one text part:

JSON
{
  "messages": [
    { "role": "user", "parts": [{ "type": "text", "content": "Where is order 8812?" }] }
  ]
}

The returned transcript includes everything that happened in between, so reading the tool_call and tool_result parts tells you exactly which capabilities ran and what they returned.

How a loop ends

FINISHREASONMEANING
stopThe model produced an answer with no tool calls. The normal ending.
tool_callsThe model ended its turn asking for tools. Visible mid-stream; not a terminal state for a completed execution.
lengthThe step budget ran out, or the model hit its own output ceiling.
errorSomething outside the loop failed. Read `error`.

Failure is not an error

Two things fail independently, and Cheela reports them differently.

  • A capability that throws becomes a tool_result carrying an error, and the loop continues. One bad call does not abort the whole run — the model gets to see what went wrong and try something else.
  • An execution that fails still returns HTTP 200, with status: "failed" and an error string in the body.
Check the body, not the status line

This route once returned 502 on a failed execution, and clients treated that as terminal and discarded the body — so every capability error and missing endpoint arrived as "request failed (502)" and nothing else, while the server had carefully explained the problem. The request succeeded; the execution is what did not.

Streaming

Send Accept: text/event-stream and the same execution arrives as server-sent events. Without the header, the response is unchanged — this is content-negotiated, so nothing breaks by adding it.

Terminal
curl -N https://api.cheelalabs.com/v1/runtime/execute \
  -H "Authorization: Bearer $CHEELA_PUBLIC_KEY" \
  -H "Accept: text/event-stream" \
  -H "Content-Type: application/json" \
  -d '{ "messages": [...] }'

Streaming does not make an execution faster. Nearly all of a request’s wall time is the provider generating tokens, and no amount of engineering on this side moves that. What it changes is that the user stops waiting in silence for the whole thing.

The stream always terminates with a done event carrying the same ExecutionResult the JSON path returns — including when something fails, because the status line has already been sent by then.

What gets counted

One execution is one unit, no matter how many steps the model takes to complete it. Capability calls are counted and reported, but they are not a limit.

Quota is a token bucket rather than a per-hour counter, so a quiet hour pays for a busy one. Bucket capacity is your hourly rate multiplied by your rollover window:

PLANEXECUTIONS / HOURROLLOVERBURST CAPACITY
Free1002 hours200
Pro2,00024 hours48,000
Enterpriseunlimited

Usage responses carry periodStart and periodEnd, so a dashboard can say “resets in 23m” rather than leaving people to guess.

Broker calls

Anonymous calls through the public broker spend the owner’s quota, and draw on a smaller sub-allowance as well as the main one. Without that, traffic against a public manifest could starve the owner’s own widget — which is a remote denial of service against anyone who publishes one.

Traces

Every execution is recorded: every capability call, token counts, duration, any error, and the shape of the conversation. List them with GET /v1/executions, fetch one with GET /v1/executions/:executionId.

What a trace does not contain

Message content is never stored. A trace holds messageShape instead — one entry per turn carrying its role and the type of each part. You can see that a run went user → assistant → tool; you cannot read what was said, and neither can we.

Capability input and output payloads are the exception — those are stored in full, because they are how you debug a capability being called with the wrong arguments. Tool arguments are often the user’s own words rephrased, so treat a capability signature as a decision about what gets retained.

Your handler receives the same executionId Cheela recorded, so logging it joins your own logs to the trace:

TypeScript
async handler(context, input) {
  logger.info({ executionId: context.executionId }, "order lookup");
  // ...
}
Metadata is traced. Credentials are not.

Anything you put in metadata is recorded in the trace. The end user’s credential travels in its own field that the trace path never sees. Do not move one into the other.