Docs
Jan Agent
Process & Failures

Process & Failures

Your app owns the Jan process. Close it when you are done, and check how each turn ended.

The quickstarts already do both. This page explains what to change when your app needs multiple conversations, a Stop button, tool approval, or recovery from a failure.

Keep the runtime, reuse the session

  • Start one runtime for your app's work, rather than one process per message.
  • Reuse a session to keep conversation history. Create another session for a separate conversation.
  • Wait for a turn to finish before starting another. One runtime runs one active turn at a time.
  • Use ephemeral: true / ephemeral=True when the conversation should not be saved. Its answers are also kept out of automatic memory recall, so one session's answers never reach a later session in the same cwd. Memory notes written with memory_write in other sessions are still listed in its prompt.

Always arrange cleanup, including on errors. In JavaScript, put await runtime.close() in a finally block. In Python, use with JanRuntime.start() as runtime:. The JavaScript and Python quickstarts show both patterns.

The ADK closes the channel and reaps its child process, escalating when the process does not exit. It does not attach to an existing process just because you know a session id.

Stop a turn, not the whole app

Call await session.interrupt() in JavaScript or session.interrupt() in Python, for example from your application's Stop action. Keep consuming the turn and read its result:

Final statusMeaning
completedThe agent finished normally.
interruptedWork was stopped. Earlier tool side effects remain.
errorThe turn failed. Read the result's error message.

These are RPC ADK statuses: JavaScript uses result.stopReason; Python uses result.stop_reason. They are not the stream-json CLI's exit codes or stop_reason values.

Closing the runtime ends all work owned by that process. Use forced close only when necessary: await runtime.close({ force: true }) or runtime.close(force=True). A handler that has already changed an external system cannot be rolled back by killing Jan.

Answer permission prompts

If you leave permissions at its default, jan, a host actuator waits for your app's approval. Handle that event while consuming the turn. The example below denies requests until you add your own approval UI or policy:


for await (const event of turn) {
if (event.type === 'permission_request') {
await session.respondPermission(event.request_id, 'deny')
} else if (event.type === 'token') {
process.stdout.write(event.text)
}
}

Answer with allow_once, allow_always where offered, or deny. Denying a tool call lets the model continue without that action. A read tool is not prompted.

If your application already gates every host action, set permissions to host instead. That removes Jan's approval step for host tools; it does not provide a replacement safety policy.

Handle the right kind of failure

FailureHow it appearsWhat your app should do
Runtime cannot be installedJanInstallErrorCheck the manifest, platform and checksum error. Do not bypass verification.
Runtime cannot start, crashes, or speaks another protocolJanRuntimeErrorClose it, fix the binary/configuration issue, and start a new runtime.
An RPC operation is refusedJanRpcErrorRead its code and message. A busy-session error is retryable after the active turn ends.
Provider authentication, model or request failureTurn result with error statusShow the message. Fix the provider or model configuration before trying again.
Your tool handler failsFailed tool result sent to the modelLet the agent handle that result; do not assume the whole turn failed.

A completed stream is not proof of success: inspect the terminal result, as the quickstarts do. Do not discard JanRuntimeError diagnostics; stderr can explain why startup or the process failed.

Retry only after checking side effects

A retry can run your tools again. After any failure or disconnect, first reconcile writes, payments, device movements or other external actions that might already have happened. A missing answer does not prove an action never ran.

Download a runtime from code

For deployments that do not put jan on PATH, the ADK can install the nightly runtime itself:


import { installRuntime, JanRuntime } from '@janhq/adk'
const installed = await installRuntime()
const runtime = await JanRuntime.start({ bin: installed.bin })
try {
console.log(runtime.serverInfo)
} finally {
await runtime.close()
}

Installation downloads a prebuilt executable, verifies its published SHA-256, and caches it. It does not configure a model provider. Use the binary's login or config set command for that. JAN_AGENT_HOME selects the cache root. Previously returned binary paths remain valid across concurrent installs; remove old cache generations only when no runtime is using them.

For reproducible deployments, pass both version and sha256 from the nightly manifest (opens in a new tab) to the installer. They must match that manifest when a download is needed; an arbitrary historical version is not a download service. A matching pinned cache entry can be reused without network access.

Working directly with the CLI?

The ADK uses a long-lived jan cli agent rpc process. A direct jan cli agent run --output-format stream-json integration is different: one run ends with a result record and a process exit. Do not mix its messages or status names with RPC.

Use Protocol records and the CLI reference for that lower-level path.