Prompts and Turns

Submit user input with session.prompt() and understand steering, queueing, and final results

session.prompt() is the only user-input entry point. It accepts one new input and returns a TurnHandle once the SDK has assigned that input to a concrete Turn.

const turn = await session.prompt({
  query: "Analyze this repository",
});

const result = await turn.finished;
if (result.success) {
  console.log(result.text);
}

prompt() does not mean “wait for the final model response.” Receive live activity through subscribe() and the final result through turn.finished.

Input shapes

query accepts a string or a Downcity Session User Part array:

const turn = await session.prompt({
  query: [
    {
      type: "context",
      tag: "reference",
      context: "An earlier quoted message",
    },
    { type: "text", text: "Explain the architecture in this image" },
    {
      type: "file",
      media_type: "image/png",
      url: "/absolute/path/architecture.png",
      filename: "architecture.png",
    },
  ],
});

Both input and persisted messages use Downcity's snake_case fields. Parts passed to prompt() do not have a part_id; they become canonical User Parts only after entering the Session.

A context Part retains structured tag and context fields in the canonical Message. It becomes Text Content such as <reference>An earlier quoted message</reference> only when model context is assembled. A tag must start with a lowercase letter and may contain only lowercase letters, numbers, underscores, and hyphens; XML structural characters in the body are escaped safely. Context is not an ordinary user body, so provide a text Part alongside it in normal prompts.

Data URL attachments are not written directly into Session Messages. The Session decodes each Data URL into the current Session's .downcity/.../attachments/ directory and stores only a path relative to the Workspace root in the Message. Before model execution, the Session reads the attachment from that path and converts it into a model-consumable representation.

When a Session fork is created, local attachments owned by the source Session are copied into the new Session's attachment directory. The fork therefore keeps its historical attachments when the source Session is later archived or removed.

For remote Sessions, Data URLs are stored on the server. A local path supplied by a remote client only works when the server can access it; prefer a Data URL or a URL accessible by the server.

Turn handles

const turn = await session.prompt({ query: "Continue" });

console.log(turn.id);
console.log(turn.result); // null before completion

const result = await turn.finished;

finished never rejects for a model or tool failure. It always resolves to:

{
  turn_id: string;
  success: boolean;
  text: string;
  error?: string;
}

Application code must check success; waiting for promise fulfillment alone is not a success check.

Read the structured Agent Message through messages() and subscribe(); the Turn result does not duplicate it. On failure, the Session closes the Agent Message and persists the failure reason as a user-visible Error Part.

If a Tool throws, returns { success: false }, or receives malformed JSON arguments from the model, the SDK marks that same Tool Part as failed and sends a failed Tool Result into the next model step. These failures do not end the Turn by themselves. A model Provider stream, canonical Message write, or other execution-infrastructure failure can end the Turn. A transient Provider failure is retried up to five times only when no visible output was produced, for at most six calls including the initial request. After partial output, the draft is preserved and the Turn fails explicitly to avoid duplicated text.

Every failed model request publishes a live warning / model_request Mutation. request_kind distinguishes Turn, title, and history-compaction calls. attempt and max_attempts identify the current and maximum call counts, while will_retry tells the subscriber whether another call will follow. Main Turn Warnings include turn_id; background Session requests may omit it. The final failure has will_retry: false. A user-requested cancellation does not publish a Warning.

If an OpenAI-compatible Provider closes its SSE stream in the middle of an event, the SDK reports a retryable transport_error. A complete SSE event containing invalid JSON produces a non-retryable provider_error. Public errors use stable descriptions rather than exposing the underlying JSON parser message.

Sending input while a Turn runs

You can call prompt() again while a Session is running:

const first_turn = await session.prompt({ query: "Analyze the directory layout first" });
const follow_up = await session.prompt({ query: "Only inspect packages/agent" });

The Session decides what happens to the second input:

SituationResult
The current Turn can still absorb inputIt is persisted as a canonical User Message and merged into the current Turn
The Turn has passed that boundaryIt is persisted as a normal User Message and queued for the next Turn
The Session is idleA new Turn starts immediately

Two prompt() calls can return the same turn.id. That means they were merged into one execution, not executed twice. Do not build a second client-side queue or assume every call starts a new Turn.

Live rendering and final result

A typical UI does both:

const unsubscribe = session.subscribe(render_mutation);
const turn = await session.prompt({ query: "Produce an implementation plan" });
const result = await turn.finished;
unsubscribe();
  • delta appends visible text, reasoning text, or raw Tool input immediately.
  • part updates the complete state of the same tool or text Part.
  • turn.finish updates loading state and aligns with turn.finished.

Do not poll turn.result for streaming content. It becomes the final snapshot only after the Turn ends.

Empty input and errors

Empty strings, empty arrays, and arrays containing only empty text Parts are rejected. Disable empty submission in the UI and still handle errors such as remote connection failure, a missing model, or an archived Session.

Use session.stop() to stop a running Turn.