SDK reference
LIQ CODE TypeScript SDK
Drive the agent from a script and stream everything it does - files edited, commands run, searches, tokens spent - as a typed async iterable. The SDK is a thin wrapper around the CLI: it spawns liq run --print --output-format stream-json and turns the NDJSON event stream into typed messages, so a script gets exactly what the terminal session gets.
The package is @liq-code/sdk. It is written in TypeScript, ships its own types, has zero runtime dependencies, is ESM-only, and needs Node 18 or newer.
Install
The SDK talks to the installed CLI, so install both. The CLI is what actually runs the agent; the SDK only starts it and reads its stream.
npm install @liq-code/sdk
npm install -g @liq-code/cliPreview. The SDK is still developed inside the product repository under sdk/ and is not published to npm yet. Until it is, import it from source or link the folder. The API below is what ships in 0.1.0 and is what the published package will expose.
Quick start
query({ prompt, options? }): AsyncGenerator<LiqCodeMessage>One call runs one turn of the agent. Iterate it and you see the work as it happens rather than after it finishes.
import { query } from "@liq-code/sdk";
for await (const message of query({
prompt: "Find and fix the bug in auth.ts",
options: { model: "liq-ai" },
})) {
if (message.kind === "block" && message.block.type === "file") {
console.log(`${message.block.action}: ${message.block.path}`);
}
if (message.kind === "message-end") {
console.log(message.message.parts);
}
}Authentication
Create a key in the desktop app under Settings → API Keys, then give it to the SDK in one of three ways.
- Environment variable (recommended):
LIQ_CODE_API_KEY=gsk_live_… - Per call:
query({ prompt, options: { apiKey } }) - Nothing at all: the CLI falls back to a provider key in the environment (
OPENAI_API_KEY/OPENROUTER_API_KEY) or to the key configured in the desktop app.
Resolution order is options.apiKey, then the environment variable, then the provider or app configuration.
query()
Returns an async generator of events. The stream ends when the CLI exits. A failure the agent reports itself arrives as an error event and then the stream ends normally; any other non-zero exit - a crash, a missing key, a failed spawn - throws a LiqCodeError instead.
The generator is lazy: nothing is spawned until the first iteration, and abandoning the loop (break, return, an exception) kills the subprocess.
collect()
collect(stream): Promise<QueryResult>When you want the answer rather than the play-by-play, drain the stream into a single result: final text, sessionId, usage, and isError / error. Every event stays available on messages.
import { query, collect } from "@liq-code/sdk";
const result = await collect(query({ prompt: "Summarise README.md" }));
console.log(result.text);
// Continue the same conversation:
await collect(query({
prompt: "Now shorten it",
options: { resume: result.sessionId },
}));Events
Each yielded message is one of five kinds. All of them carry sessionId.
| kind | when | payload |
|---|---|---|
message-start | A new agent message begins. | model |
block | A tool ran or advanced. | a command, file, search, image or choice block |
usage | Token spend was updated. | usage |
message-end | The answer is complete. | the full ChatMessage, including its text |
error | The turn failed. | error |
The token-by-token text and thinking deltas that the interactive UI renders are deliberately not on this stream. The complete answer arrives once, on message-end.
Action blocks
A block event describes one thing the agent did. Each block carries an id, so repeated events for the same id are updates rather than new work.
| type | what it reports |
|---|---|
command | A shell command: command, title, status (running / done / error), captured output, and a parsed shell analysis. |
file | A file written: path, action (created / modified), the new content, and a line diff for anything but very large files. |
search | A content search: query, matches with file, line and text, a truncated flag and durationMs. |
image | A generated image: workspace-relative path and mime. |
choice | A question with options. Headless runs have nobody to ask, so the CLI answers with the first option and this block is generally not surfaced. |
Options
Every option maps to a real CLI flag or to subprocess behaviour.
| option | maps to | notes |
|---|---|---|
cwd | --dir | Workspace directory. Defaults to process.cwd(). |
model | --model | liq-ai (default), auto, or liq-local. |
maxEffort | --max-effort | Strongest tier and highest reasoning effort. |
maxTurns | --max-turns | Caps main-loop turns. The safety valve for unattended runs. |
apiKey | --api-key | See Authentication. |
files | --file | Files to attach; the agent inlines their contents. |
skills | --skill | Skills to load before the turn. |
continue | --continue | Continue the most recent session. Excludes resume. |
resume | --resume | Resume a session by id. Excludes continue. |
sessionPersistence | --no-session-persistence | false keeps the run in memory, so it leaves no session file and cannot be resumed. |
codeIndex | --no-code-index | false skips the semantic index for this run. |
mode | --mode | code (default) or swarm, which fans parallel workers over the task, verifies, and reduces. Swarm needs a CLI build with that feature enabled. |
swarmKind | --swarm-kind | audit (read-only, default) or build. Swarm only. |
workers | --workers | Maximum parallel workers. Swarm only. |
signal | - | An AbortSignal. Aborting kills the subprocess. |
timeoutMs | - | Kills the run after N milliseconds. |
env | - | Extra environment variables, merged over the parent process. |
pathToExecutable | - | Explicit argv for the CLI, bypassing discovery. |
onStderr | - | Receives the CLI stderr as it arrives. Usually empty on success. |
Tool scoping
allowedTools and disallowedTools exist in the type surface for compatibility with the Anthropic Agent SDK, but the CLI does not enforce them yet, so passing either one throws UNSUPPORTED_OPTION. An allow-list that silently does nothing is worse than none. Omit them and the run uses the default tool set.
Sessions
Every event carries the sessionId, and collect() hands it back on the result. Pass it as resume to continue that conversation, or set continue: true to pick up the most recent one. Set sessionPersistence: false for throwaway queries that should leave nothing behind.
Token usage
usage events and QueryResult.usage report turns, inputTokens, cachedInputTokens, outputTokens and reasoningTokens, plus the context-window fill of the latest turn. When routing escalates mid-prompt, byModel breaks the spend down per model actually used.
Errors
Everything the SDK throws is a LiqCodeError. Discriminate on .code rather than on subclasses; .exitCode and .stderr are filled in where they are relevant.
INVALID_OPTION- the prompt or options were rejected before anything was spawned.UNSUPPORTED_OPTION- an option the CLI cannot honour yet, such as tool scoping.EXECUTABLE_NOT_FOUND- the CLI could not be located.SPAWN_FAILED- the subprocess did not start.CLI_EXITED- a non-zero exit with no structured error event.ABORTED- the run was cancelled by a signal or a timeout.
import { query, collect, LiqCodeError } from "@liq-code/sdk";
try {
const result = await collect(query({ prompt }));
if (result.isError) console.error(`Run failed: ${result.error}`);
} catch (err) {
if (err instanceof LiqCodeError) {
console.error(`[${err.code}] ${err.message}`, err.exitCode, err.stderr);
} else {
throw err;
}
}Cancellation
const controller = new AbortController();
setTimeout(() => controller.abort(), 5_000);
for await (const message of query({ prompt, options: { signal: controller.signal } })) {
// Aborting kills the CLI subprocess; the loop then throws
// LiqCodeError { code: "ABORTED" }. options.timeoutMs does the same on a timer.
}Finding the CLI
The SDK looks for the executable in this order:
options.pathToExecutable, an explicit argv override.LIQ_CODE_CLI, an environment variable pointing at the binary.liq-codeorliqonPATH, which is the normal installed case.- A product checkout around the SDK, which runs the live CLI source. This is the in-repo development path.
The same resolution is exported as resolveExecutable(), with which() and resolveApiKey() alongside it, so a script can report what it is about to run before it runs it.
Exported types
Besides query, collect and LiqCodeError, the package exports the full event and message surface: LiqCodeMessage and its five event types, Options, QueryResult, ChatMessage, MessagePart, ActionBlock and each block type, TokenUsage, MessageSource and LiqCodeModel. Narrow on kind for events and on type for blocks and parts: both unions are discriminated, so TypeScript resolves the payload for you.