Skip to main content
Conversations & Sessions

Input Modes

Input mode determines one thing: whether your application can keep sending messages to the Agent after a task starts. It does not distinguish between text, images, or other message content, and it does not determine whether responses are displayed incrementally. With either input mode, the SDK returns the Agent's responses as a message stream. To process those responses, see Streaming Output. Qoder Agent SDK provides two input modes:
Input modeTypeScriptPythonJavaWhen to use it
Single-message inputquery({ prompt: string })query(prompt=str)client.query(OneShotQueryRequest)One-off tasks, batch jobs, and CI scripts
Streaming inputquery({ prompt: AsyncIterable<SDKUserMessage> })QoderSDKClient is recommended; a predetermined message stream can also be passed to query()QoderSession returned by client.openSession(options)Chat interfaces, multi-message sessions, and tasks that need additional or revised instructions while running
Choose between them by answering one question: will your application need to send more messages after the task starts?
  • No. Use single-message input.
  • Yes. For example, the user may follow up, or the application may choose the next step based on the Agent's response. Use streaming input.

Single-message input

Passing a string to query() starts an independent task. After the task starts, you cannot append another user message to that call. The SDK ends the session when the task completes.
import { accessTokenFromEnv, query } from '@qoder-ai/qoder-agent-sdk';

for await (const message of query({
  prompt: 'Find functions in the current project that lack test coverage and report them.',
  options: {
    auth: accessTokenFromEnv(),
    allowedTools: ['Read', 'Glob', 'Grep'],
  },
})) {
  if (message.type === 'result' && message.subtype === 'success') {
    console.log(message.result);
  }
}
A Java one-shot query ends after the CLI returns the turn's ResultMessage. Still read the message stream to the end, then close the cursor or the QoderQuery. ResultOutcome.ERROR means the turn failed; the absence of an exception does not mean success. Single-message input does not disable tool approval. If the Agent needs to run a tool that has not been authorized, canUseTool / can_use_tool can still ask the user. You do not need to turn prompt into a message stream for that purpose. See Approval and User Input.

Streaming input

Streaming input lets your application keep sending messages after a task starts. In addition to multi-turn chat, it can supply new information, redirect a running task, or queue a message for later. The recommended interface differs by SDK:
  • TypeScript: pass an AsyncIterable<SDKUserMessage> to query(). The async iterator can continue to yield user messages supplied by a chat UI, message queue, or another event source.
  • Python: use QoderSDKClient to keep the session open. Call client.query(...) to send a new message, then use client.receive_response() to receive that turn's response.
  • Java: open a QoderSession with client.openSession(options). Call session.send(...) to send a new message, then read the reply from the same message cursor; a ResultMessage marks the end of the turn.

Multi-message session

import {
  accessTokenFromEnv,
  query,
  type SDKUserMessage,
} from '@qoder-ai/qoder-agent-sdk';

async function* messages(): AsyncGenerator<SDKUserMessage> {
  yield {
    type: 'user',
    message: {
      role: 'user',
      content: [{ type: 'text', text: 'Check this codebase for security issues.' }],
    },
    parent_tool_use_id: null,
  };

  // A real application can wait here for a UI, message queue, or another event.
  await new Promise((resolve) => setTimeout(resolve, 2_000));

  yield {
    type: 'user',
    message: {
      role: 'user',
      content: [{ type: 'text', text: 'After the analysis, produce a brief report.' }],
    },
    parent_tool_use_id: null,
    priority: 'later',
  };
}

for await (const message of query({
  prompt: messages(),
  options: {
    auth: accessTokenFromEnv(),
    allowedTools: ['Read', 'Glob', 'Grep'],
  },
})) {
  if (message.type === 'result' && message.subtype === 'success') {
    console.log(message.result);
  }
}
For TypeScript message fields, see SDKUserMessage. In Python, each receive_response() call receives one turn. After receiving a ResultMessage, you can send the next message. In Java, each input is a UserInput. Completion of send() only means the input was written to the CLI, not that the reply has finished. A session's message stream can be consumed only once, so reuse the same cursor across turns; closing the cursor also closes the session.

Python async message stream boundary

Python query() also accepts AsyncIterable[dict[str, Any]]. If every message is known before the task starts, you can send them sequentially this way. The following fragment only shows the core async-message-stream pattern; see the complete example above for the creation of query and options:
Python
async def prompts():
    yield {
        "type": "user",
        "message": {"role": "user", "content": "Inspect the authentication module."},
        "parent_tool_use_id": None,
    }
    yield {
        "type": "user",
        "message": {"role": "user", "content": "Then summarize the findings."},
        "parent_tool_use_id": None,
    }


async for message in query(prompt=prompts(), options=options):
    print(message)
Use this form only for messages prepared in advance. It cannot choose the next message from content the Agent has just returned, and it cannot call interrupt() or cancel a queued message. Interactive applications such as chat interfaces should use QoderSDKClient.

Prestart the CLI (Java)

Java can also start the CLI before user input arrives. client.prepare(options) completes startup, authentication, and initialization without sending input, which removes the startup latency from the first request:
Java
// Prewarm at application startup: start the CLI and finish authentication and initialization without sending input.
PreparedQuery prepared = client.prepare(options).toCompletableFuture().join();

// When user input arrives, turn it into a session; toQuery(input) turns it into a one-shot query instead.
QoderSession session = prepared.toSession();
session.send(UserInput.text("Check this codebase for security issues."));
A PreparedQuery can be converted only once. After conversion, the new QoderSession or QoderQuery owns the resources; if it is never used, call close() to discard it.

Add input while the Agent is running

You can send another message while the Agent is responding. priority determines when the Agent processes it:
ValueBehavior
nowStop the current response and process this message immediately
nextDefault; process the message at the next suitable opportunity
laterWait until the current response finishes
Messages with the same priority are processed in send order. Use priority: 'now' to redirect the current work immediately. If you only want to stop the current response without sending a new message, use Interrupt the current response. In Java, set the priority with priority(SendPriority.NOW / NEXT / LATER) on UserInput.Builder. The default priority of a Java UserInput is NOW, not next; set it explicitly when you add messages during a run. Place the following snippets inside the TypeScript message generator, the connected Python client session, or the Java QoderSession shown above:
yield {
  type: 'user',
  message: {
    role: 'user',
    content: [{ type: 'text', text: 'Stop the current direction and analyze only the failing tests.' }],
  },
  parent_tool_use_id: null,
  priority: 'now',
};

Add context without triggering a response

Sometimes you only want to provide background information without asking the Agent to respond immediately. Set shouldQuery: false in TypeScript, should_query=False in Python, or shouldQuery(false) in Java. The message is still added to the conversation, and priority determines when it takes effect.
yield {
  type: 'user',
  message: {
    role: 'user',
    content: [{ type: 'text', text: 'All subsequent suggestions must support Python 3.10.' }],
  },
  parent_tool_use_id: null,
  shouldQuery: false,
};

Interrupt the current response

Call interrupt() to stop the Agent's current response without ending the session. You can continue sending messages afterward. In TypeScript, call it on the object returned by query(); Python requires QoderSDKClient; in Java, call it on QoderQuery or QoderSession.
import { accessTokenFromEnv, query } from '@qoder-ai/qoder-agent-sdk';

const q = query({
  prompt: 'Inspect every file in the project.',
  options: { auth: accessTokenFromEnv() },
});

const interruptTimer = setTimeout(() => {
  void q.interrupt().catch(console.error);
}, 5_000);

try {
  for await (const message of q) {
    console.dir(message, { depth: null });
  }
} finally {
  clearTimeout(interruptTimer);
}
Java's interrupt() returns InterruptResult. Success only means the CLI accepted the interrupt request; the turn still ends when its ResultMessage arrives. interrupt() does not remove messages waiting in the queue. Cancel any queued message that is no longer needed. To end the entire session, call a close method or leave the async with block.

Cancel a queued message

To cancel a message that has not started, first give it a UUID that is unique within the session. TypeScript uses the message's uuid field; Python uses the message_uuid argument. Java does not need you to generate an ID: send with session.submit(input) instead of send(input), and the returned MessageSubmission immediately carries the SDK-assigned MessageId: The following fragments only show the cancellation operation and assume that q / client and the corresponding message UUID have already been created:
const cancelled = await q.cancelAsyncMessage(uuid);
Cancellation returns true / True when successful and false / False when the message does not exist or has already started. A message without a UUID cannot be cancelled individually. Do not reuse a UUID within the same session. In Java, the SDK cancels a message locally if it has not been written to the CLI yet; that message's written() completes with CancellationException, and the session is unaffected. For a message that has started writing, the SDK attempts a remote cancel only when the CLI supports the corresponding capability; otherwise it returns false. Use send() when you only need write confirmation, and submit() when you need the ID before the write.

End input and close the session

  • TypeScript: the SDK ends a string-prompt session when the task finishes. With an async message stream, completion of the iterator means no more messages will be sent. To close the entire session early, use AbortController or call q.close().
  • Python: one-off query() calls end automatically. With QoderSDKClient, use async with to connect and disconnect automatically, or call connect() / disconnect() manually.
  • Java: after reading a one-shot query's stream to the end, close the cursor or the QoderQuery. Close a QoderSession when you are done with it; to make sure all accepted input reaches the CLI, wait for completeInput() first. It rejects further input but does not wait for replies. Closing the QoderClient cascades to every query and session it created.
The following code only shows how a session ends and reuses the imports, messages(), and options definitions from the earlier examples:

Close automatically

const q = query({
  prompt: messages(),
  options: { auth: accessTokenFromEnv() },
});

try {
  for await (const message of q) {
    console.dir(message, { depth: null });
  }
} finally {
  await q.close();
}

Close early in response to an external condition

const abortController = new AbortController();
const q = query({
  prompt: 'Inspect every file in the project.',
  options: { auth: accessTokenFromEnv(), abortController },
});

const taskTimeout = setTimeout(() => abortController.abort(), 5_000);
try {
  for await (const message of q) {
    console.dir(message, { depth: null });
  }
} finally {
  clearTimeout(taskTimeout);
}
In Java, after another thread closes the session, a cursor that is reading ends with SessionClosedException. Do not call the blocking close() in SDK callbacks or push handlers; use closeAsync() there. You cannot send another message after the session closes.