Skip to main content
maestro-lib is the part of Maestro that starts a coding agent (Claude Code, Codex, OpenCode and others), sends it a prompt, reads its reply as it streams, stops it, and reports how the turn ended. The desktop app, maestro-cli and Cue all run their agents through it. You can use it from your own Node program through one entry module. For a worked example, see the terminal example: a small interactive program that starts, streams, resumes and stops turns with nothing but this entry.

What it is, and what it is not

It is:
  • Plain Node. Nothing it loads needs Electron or the desktop app. It runs on Node 20 or later, on a laptop, a server or in CI.
  • One turn at a time. A turn is one prompt sent to one agent process. The agent keeps the conversation; to continue it, you pass the session id the last turn returned.
  • Local. It starts the agent on the machine your program runs on, in the folder you give it.
It is not:
  • Not the desktop app or the CLI. There are no agents in a Left Bar, no tabs, no Auto Run, no playbooks, no Cue pipelines and no stored history. Your program keeps whatever state it needs.
  • Not an agent. It runs agent CLIs you have installed and logged in to. It does not call a model API itself.
  • Not remote. SSH remotes belong to the desktop app and the CLI, which store them.
  • Not a provider installer. If the provider’s binary is not on this machine, planning the turn fails with not-installed.

Providers

planSessionTurn checks every request before anything starts, and refuses what could not run correctly: What each provider supports in maestro-lib 0.3.0: Hermes is refused with no-parser. To list providers at run time, use getVisibleAgentDefinitions() and getAgentCapabilities(id).

Install

maestro-lib is built from the Maestro repository. It is not published to a package registry.
The build writes dist/maestro-lib/: Add it to your program as a local dependency:
The bundle has no runtime dependencies. For types, your program needs @types/node. The declarations also name node-pty, for stopping a PTY you started yourself; it is an optional peer, and with skipLibCheck (the TypeScript default for most setups) you do not need it.

Version

The library has its own version, separate from the Maestro app’s version. The app can ship many releases with no change to the library.
  • MAESTRO_LIB_VERSION, exported from the entry, and version in the built package.json hold the same value.
  • It follows semver over what the entry exports. Below 1.0.0, a minor release may change the surface.
  • To pin it, build from a Maestro commit or tag, keep that dist/maestro-lib/ folder, and check the version at startup:
Import from maestro-lib only. The files under types/ mirror the library’s internal modules; they are there for the declarations, and their paths can change in any release.

Run a first turn

planSessionTurn turns an agent id, a folder and a prompt into a process spec. runToCompletion starts it and resolves once the agent has exited.
The request (SessionTurnRequest) takes: The run options (RunTurnOptions) need three fields. agentId is the same provider. sessionId is a label for your program, used in log lines; it is not the provider’s session id, and a fixed name such as 'my-tool' is fine. stopGraceMs is how long each stop stage waits before the next one: INTERACTIVE_STOP_GRACE_MS (2 seconds) for a turn someone is watching, BACKGROUND_STOP_GRACE_MS (5 seconds) for an unattended one. The agent’s environment is your program’s environment, then the provider’s defaults for any variable your environment leaves unset, then envVars. This is the order maestro-cli uses.

Stream the reply

runTurn returns at once with a handle and a promise. The handlers are called while the agent runs. Each line the provider’s parser understands arrives in onEvent as a ParsedEvent.
The event types: Providers differ in which events they send and how they split text. Show events as they arrive, and use CompletedTurn.answerText for the final answer: it is the result text when the provider sent one, and otherwise the text streamed during the turn. The other handlers are onStdout and onStderr (raw chunks), onLine (each stdout line before parsing), onOversizedLine (output dropped because a line grew past the limit) and onStdinError (the prompt could not be written to the agent). Every handler for a chunk returns before the next chunk is read, and all of them return before completed resolves.

Resume a session

The provider keeps the conversation. A finished turn returns its sessionId; pass it as resumeSessionId to send the next message in the same conversation. planned.resuming is true when the plan resumes one.
Store the session id if your program should continue the conversation after a restart. Each provider keeps its sessions in its own files, so the id only works on the machine (and for the account) that created it.

Stop a turn

A running turn is stopped through its TurnHandle: A stop also ends the processes the agent started during the turn (a shell command a tool was running, for example), once the agent has exited. A turn you stopped resolves with outcome interrupted, whatever the process’s exit code says.
You can also pass an AbortSignal as signal in the run options. Aborting it stops the turn from terminate.

Read the outcome

completed resolves with a CompletedTurn: The outcomes:
  • completed: the agent finished its turn.
  • completed-with-warning: there is an answer, but the agent exited with an error code or never sent its final result message. Show the answer, and flag it.
  • interrupted: you stopped it.
  • crashed: the agent failed. error.type names the cause when it is known, for example auth_expired, rate_limited, token_exhaustion, network_error or agent_crashed (the full list is the AgentErrorType type), and error.message describes it. A crash with no error (killed by a signal you did not send, for example) is described by exit.

Host hooks

The library sends log lines and error reports to whatever your program registers. All hooks are optional; without them, log lines and reports are dropped.
Log lines go to the logger you register at the time of the call, so register hooks once at startup. Keep the library’s output on stderr if your program prints results on stdout. Two more hooks exist for hosts that have the data: setMaestroLibImageRefResolver resolves Maestro’s maestro-image:// image references (without it, such a reference is treated as unreadable), and setMaestroLibCapabilitySnapshotLookup supplies live per-agent capability data (without it, context windows fall back to built-in defaults). A program that is not Maestro does not need either.

Windows

npm installs most agent CLIs on Windows as .cmd shims (claude.cmd, codex.cmd), and Node cannot start a .cmd file without a shell. planSessionTurn handles this the same way the Maestro desktop app does:
  • If the binary is a .cmd or .bat shim, a bare .exe name, or a shebang script, the spec has shell: true. Every argument is escaped for cmd.exe, and a command path with spaces is quoted. A plain .exe with a path starts directly, with no shell.
  • The prompt goes over stdin for every provider that can read it there, so quotes, %, &, |, ^ and line breaks reach the agent as typed. This also avoids cmd.exe’s limit of about 8 KB per command line.
  • A provider that only takes its prompt on the command line keeps it there. If an argument contains a line break, % or ^, cmd.exe cannot pass it intact, so the turn is refused with windows-shell instead of running with a changed prompt.
On other hosts the spec is unchanged. If you build a spec yourself, pass it through applyWindowsShellRules(spec) to apply the same rules. It returns the spec unchanged on other hosts, for a command that can start directly, and for a spec that already sets shell, so applying it twice is safe. turnProcessSpecFromPlan already applies it. Put the prompt in spec.stdin when the provider reads its prompt from stdin, and check other arguments with cmdShellArgProblem(arg), which returns the reason cmd.exe cannot carry an argument, or null if it can.

Lower-level pieces

planSessionTurn and runTurn cover a program that runs turns. The entry also exports the steps they are made of, for a program that needs more control:
  • Plan: buildAgentArgs (the provider’s arguments, resume included), buildAgentLaunchPlan (environment and prompt delivery), turnProcessSpecFromPlan (which applies applyWindowsShellRules, see Windows), and checkBinaryExists / checkCustomPath to find a provider’s binary.
  • Run: startTurn starts and streams a turn and reports how the process ended, without deciding whether it succeeded. TurnCapture folds its events into an answer, session id and usage, and resolveTurnOutcome decides the outcome. runTurn is these three together.
  • Stop: stopProcess runs the same stop stages on a process you started yourself; snapshotProcessTree and killProcessTreeNow record and end what it started.
  • Parsers: createOutputParser(agentId) returns a new parser for one stream. initializeOutputParsers(), then getOutputParser(agentId), gives a shared instance for read-only checks.

The headless runtime

Since 0.3.0 the entry also exports what lets Maestro run with no desktop. This page does not cover these yet; each line points at where they are described.
  • Paths: resolveMaestroPaths and the data directory resolvers, plus the doctor that reports on a data directory (paths/).
  • Locks: the cross-process lock primitive (createProcessLock) and the data-dir lock that makes one process the only writer of a data directory (acquireDataDirLock). See Plans/maestro-tui-runtime.md, sections 3 and 4.
  • Runtime and in-process client: createMaestroRuntime(), the in-process MaestroClient that owns writes to a data directory, and the server maestro-cli host runs it in. See Headless Host.
  • Store readers: read-only readers of the agent, group, settings and history stores, without electron-store.
  • Auto Run: the playbook and goal engines the CLI and the runtime share. See Plans/maestro-tui-autorun-engine.md.
  • Group chat and consults: the group chat engine, its storage, and the cross-agent consult service.
  • Client and discovery: the MaestroClient interface with its WebSocket implementation, and the discovery file (cli-server.json) a client finds a running host by. See Plans/maestro-tui-client-api.md.

See also

  • Maestro CLI for running agents and playbooks from a shell.
  • Running Cue on a Server for unattended pipelines.
  • examples/maestro-lib-tui, an interactive terminal chat built on the built entry alone: provider choice, streaming, resume, Ctrl+C to stop. Its README says how to run it.
  • maestro-lib-run, a small program in the Maestro build that runs one turn with these same calls and prints JSON lines. Its source, src/shared/maestro-lib/bin/run-turn.ts, is a complete example.