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.
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
- Call the model with the transcript so far and your capabilities as tools.
- Append the model’s message to the transcript, and its shape to the trace.
- If it contains no tool calls, stop — that message is the answer.
- Otherwise dispatch each tool call to your endpoint, in parallel up to a cap, and append the results.
- Go back to step 1.
Token usage accumulates across every step rather than being overwritten, because the execution is billed for all of them.
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.
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:
{
"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
| FINISHREASON | MEANING |
|---|---|
| stop | The model produced an answer with no tool calls. The normal ending. |
| tool_calls | The model ended its turn asking for tools. Visible mid-stream; not a terminal state for a completed execution. |
| length | The step budget ran out, or the model hit its own output ceiling. |
| error | Something 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_resultcarrying 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 anerrorstring in the body.
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.
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:
| PLAN | EXECUTIONS / HOUR | ROLLOVER | BURST CAPACITY |
|---|---|---|---|
| Free | 100 | 2 hours | 200 |
| Pro | 2,000 | 24 hours | 48,000 |
| Enterprise | unlimited | — | — |
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.
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:
async handler(context, input) {
logger.info({ executionId: context.executionId }, "order lookup");
// ...
}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.