Skip to content

Client & configuration ​

Client is where a program starts. This page covers how to create one, where its configuration comes from, how to choose the model, reasoning effort and permission level, and how to close it.

Creating a client ​

python
import zettcode.sdk as zc

async with zc.Client("~/project", model="gpt-5", effort="high", permission="safe") as client:
    print(client)  # Client('/Users/me/project', session='...')
ArgumentMeaning
workspaceThe directory the agent works in. None is the current directory (or config.workspace). ~ is expanded, and it must exist.
configA ready ZettCodeConfig. Nothing is read from disk and the embedding defaults are not applied.
config_pathA config file to read instead of $ZETTCODE_CONFIG or ~/.zettcode/config.toml.
modelsModels to use without any config file; the first is active. See below.
modelThe model to start on, by the name client.models lists.
effortThe reasoning level to start on.
permissionThe shell permission level: deny, ask, safe or full (auto, the old name of full, is still taken).
resumeA session id to continue instead of starting a new conversation.
every other settingA configuration field by name: store=, mcp_enabled=, max_iterations=, and the rest. They are named and typed in the signature — see Settings.

The constructor is synchronous and does its work at once: a bad value raises here, not on the first prompt.

python
try:
    zc.Client(".", permission="sometimes")
except zc.ConfigurationError as error:
    print(error)  # Unknown permission: sometimes. Try one of: deny, ask, safe, full

Settings ​

Every field of the configuration is a keyword argument of the constructor, named and typed: no name is taken and hoped for. They are laid over whichever configuration the client read (see below), and one that is left out keeps what it read. A name that is not one of these is a TypeError from Python at the call; a value of the wrong type is a ConfigurationError when the client is built.

SettingTypeWhat it is
storestr, PathWhere sessions are stored.
theme_filestr, Path, NoneA palette file laid over the theme.
themestr, NoneThe palette in force: dark, light, or a file's name.
themes_dirstr, PathWhere the reader's own palettes live.
petstr, NoneThe companion over the conversation, or None for none.
pet_positiontuple[float, float], NoneWhere it stands.
reasoning_effortReasoningEffort, strThe level a new session starts at.
shell_approvalShellApprovalMode, strdeny, ask, safe or full.
reduced_motionboolNo animation, for a reader who asked for none.
parallel_tool_callboolLet the model call several tools in one round.
max_iterationsintHow many rounds one request may take.
transcript_max_entriesintHow many rows the terminal keeps.
mouse_scroll_speedfloatRows per wheel notch.
tipsboolTips in the waiting row.
agents_md_enabledboolRead AGENTS.md into the conversation.
ask_user_enabledboolGive the model the ask_user tool.
skills_enabledboolOffer skills.
skill_rootsSequence[str | Path]Where skills are looked for.
skills_scopestrglobal, workspace or both.
mcp_enabledboolStart the configured MCP servers.
mcp_configstr, Path, NoneThe servers' file.
mcp_scopestrglobal, workspace or both.
self_compactionboolLet the model compact its own context.
statusline_hiddenSequence[str]The parts of the status line that are switched off.
plugins_enabledboolLoad plugins.
disabled_pluginsSequence[str]Plugins to leave out by name.
plugin_settingsMapping[str, Mapping[str, object]]The [plugins.<name>] tables: what each plugin reads, by plugin name.
subagent_enabledboolGive the model the subtask tool.
subagent_max_iterationsintRounds a subtask may take.
subagent_messagingboolLet a subtask message other sessions.
todowrite_enabledboolGive the model the to-do list tool.
jit_harnessboolLet the model write and run its own tools.
shell_networkboolLet a sandboxed command reach the network.
sessions_enabledboolJoin the broker other sessions reach this one through. Off unless asked, and the terminal turns it on.
update_enabledboolCheck PyPI for a newer release in the background. Off unless asked.
update_filestr, PathWhat that check leaves behind.
harness_dirstr, PathWhere the tools a model wrote are kept.

The meanings are the ones the configuration reference gives each key, with the same names; a value here says exactly what the same key in the file would say.

Where the configuration comes from ​

In order of precedence:

  1. config=, a ready ZettCodeConfig, used as given.
  2. models=, laid over the defaults. The config file is not read at all.
  3. The config file: config_path=, else $ZETTCODE_CONFIG, else ~/.zettcode/config.toml. This is the same file the terminal reads, so models and settings you have already set up apply.

Keyword settings are laid over whichever of the three you used. This is the whole story: there is no hidden fourth source.

Models without a config file ​

Pass the models directly when the program should not depend on what is installed on the machine, as in CI or a container:

python
async with zc.Client(
    ".",
    models=[
        {"model": "claude-sonnet-4-5", "token": "sk-ant-...", "anthropic": True, "multimodal": True},
        {"model": "gpt-5", "token": "sk-...", "context_window": 400_000},
    ],
    store="/tmp/zettcode-sessions",
) as client:
    print([m.name for m in client.models])  # ['claude-sonnet-4-5', 'gpt-5']

An entry is a ModelConfig or a dict of the same fields:

FieldDefaultMeaning
modelrequiredThe model id sent to the endpoint.
token""The credential. May be left out for a local server that wants none, or taken from OPENAI_API_KEY (ANTHROPIC_API_KEY for an Anthropic model). Read it from the environment rather than writing it into code.
display_modelNoneThe name to show and to pass to use_model. Give one when two entries share a model id. No two entries may be shown by the same name, and a file that has two is refused when it is read.
base_urlNoneThe API root. A trailing /chat/completions, /v1/messages or /responses is dropped.
anthropicFalseSpeak Anthropic's Messages API.
responses_apiFalseUse the Responses API on an OpenAI-compatible endpoint. Not combinable with anthropic.
chatFalseSay that the endpoint speaks chat completions, which is what an entry with neither of the other two already is. Not combinable with anthropic or responses_api.
multimodalFalseWhether the model accepts images.
temperatureNoneSampling temperature sent with every request, from 0 to 2 (0 to 1 for an Anthropic model). Left out, the provider's default is used.
context_window128000Tokens the model can carry.
compact_percent80.0The share of the window at which the conversation is compacted.
reasoning_effortsall levelsThe reasoning levels the model accepts.

WARNING

A token written into source ends up in version control. Read it from the environment: {"model": "gpt-5", "token": os.environ["OPENAI_API_KEY"]}.

A malformed entry raises ConfigurationError that names the entry and the problem.

The settings a program usually wants ​

The complete list, with each one's type, is above. This is the short version: the ones a program is most likely to change, and the default each has in the configuration file.

Any other keyword is a field of ZettCodeConfig, so everything the config file can say, a client can say in code.

SettingDefaultEffect
store~/.zettcode/sessionsWhere sessions are kept. Point it at a temporary directory in tests.
max_iterations360Tool-call rounds allowed in one turn.
mcp_enabledTrueLoad MCP servers.
skills_enabledTrueDiscover local skills and offer them to the model.
agents_md_enabledTrueGive the model the AGENTS.md files that apply to the workspace.
ask_user_enabledTrueGive the model the ask_user tool. Switch it off for runs nobody can answer.
subagent_enabledTrueOffer the task tool, which runs a child agent.
todowrite_enabledFalseOffer the todo_write tool.
plugins_enabledTrueLoad third-party plugins.
parallel_tool_callTrueLet the model issue tool calls in parallel.
sessions_enabledFalse hereJoin the broker so terminal sessions can message this one.
update_enabledFalse hereCheck PyPI for a new release.

Strings are accepted where a path is expected (store="~/runs"), and choices are accepted in any case (reasoning_effort="HIGH"). A mistyped name is a TypeError from Python at the call, before anything is read:

python
zc.Client(".", max_iteration=50)
# TypeError: Client.__init__() got an unexpected keyword argument 'max_iteration'

A value the configuration refuses is reported as ConfigurationError("Invalid configuration: ..."), with the original error as __cause__.

A hermetic client ​

For a test or a sandboxed job, switch off everything that reads from or reaches outside the workspace:

python
client = zc.Client(
    workspace,
    models=[{"model": "gpt-5", "token": token}],
    store=scratch / "sessions",
    mcp_enabled=False,
    skills_enabled=False,
    agents_md_enabled=False,
    plugins_enabled=False,
    subagent_enabled=False,
)

Choosing the model, effort and permission ​

These are set when the client is made, or changed at any time between runs.

python
async with zc.Client(".") as client:
    for info in client.models:
        marker = "*" if info.active else " "
        print(marker, info.name, info.context_window, info.efforts)

    client.use_model("gpt-5")
    client.use_effort("high")
    client.use_permission("safe")

    print(client.model.name, client.effort, client.permission)
MemberReturns
client.modelsA tuple of ModelInfo(name, model, context_window, multimodal, efforts, active, compact_percent, web_search, web_search_name).
client.modelThe ModelInfo of the active model.
client.use_model(name)Makes a model active and returns its ModelInfo. Takes the name or a ModelInfo; a name is a display name first, a model id only when nothing is shown by it.
client.effort, client.effortsThe current level, and the levels the active model accepts.
client.use_effort(name)Sets the level and returns it.
client.use_compact_percent(percent)Moves the point, as a share of the window (above 0, at most 100), at which the conversation is summarized, from the next request on. Returns the active ModelInfo and why the configuration file was not rewritten (None when it was): the change then holds for this run only.
client.permission, client.permissionsThe current level, and ("deny", "ask", "safe", "full").
client.use_permission(name)Sets the level; it applies from the next shell command, even in a run that is already going. "auto" is taken as "full".
client.harnessWhether the just-in-time harness is on: the model may write small Python tools of its own and run them. Off by default; Client(jit_harness=True) starts a client with it on.
client.use_harness(on)Turns it on or off from the next prompt. Returns the new state.

A name that does not exist, or a reasoning level the active model does not take, raises ConfigurationError with the valid choices in the message.

python
async with zc.Client(".", models=[{"model": "a", "token": "t"}, {"model": "b", "token": "t"}]) as client:
    try:
        client.use_model("c")
    except zc.ConfigurationError as error:
        print(error)  # Unknown model: c. Configured: a, b

The model change takes effect for the next run. A conversation can move between models: the history is kept.

Permission levels ​

The permission level decides who answers when the model wants to run a shell command. It governs the shell only; file tools and MCP tools are not behind it.

LevelA shell command…
deny…never runs. The model is told and carries on with the file tools.
ask…is put to you (the ApprovalRequested event). This is the default.
safe…runs unasked, inside an operating-system sandbox (Seatbelt on macOS, bubblewrap on Linux): it can write the workspace and /tmp only, has no network, and cannot reach credential files. Where the machine has no sandbox (Windows, or Linux without bwrap) it runs the commands that provably cannot change anything and puts the rest to you.
full…runs unasked with your own access and no sandbox. Only for a workspace and a model you would trust with a shell. Its old name, auto, still works.

See Approvals & questions for how to answer.

Looking at what loaded ​

python
async with zc.Client(".") as client:
    report = client.plugins
    print(report.loaded)  # ('shell', 'skills', ..., 'coding', ..., 'ask-user', 'mcp', 'compaction')
    print(report.failures)  # one message per plugin that could not load; never fatal
    print(client.config.workspace, client.config.store)
    print(client.usage)  # what the current session has spent

client.config is the ZettCodeConfig in force, which is frozen: build a new client to change a setting that is not one of the use_* methods.

@ resources, usage and health ​

Three more questions the terminal answers have their own methods: what an @ can point at, what the workspace spent, and whether the models answer.

python
async with zc.Client(".") as client:
    for mention in client.mentions("cli", limit=5):
        print(mention.token, mention.kind, mention.description)
    ledger = await client.usage_ledger()
    print(ledger.total.total_tokens)
    for check in await client.check_models(timeout=15):
        print(check.model, check.ok, check.detail)

Usage and health explains the ledger, session stats and the model check; Sessions covers what a session holds.

Models before there is a client ​

The functions above all need a client, and a client needs at least one model. A fresh machine has none, which is why the package also edits the configuration file itself — the same [[models]] entries the terminal's /config edits:

python
import zettcode.sdk as zc

for model in zc.list_models():  # (), when the file is new or has no models
    print(model.name, model.context_window)

zc.add_model({"model": "gpt-5-mini", "token": "sk-…", "display_model": "Fast"})
zc.update_model("Fast", {"context_window": 200000})  # the token is kept
zc.remove_model("Fast")  # refused for the last one

add_model writes the first entry itself and later ones through the terminal's editor, so comments and the rest of the file are kept; what it writes is read back and validated before it returns, and a write that would not parse is undone. The functions take path= for a configuration file other than the default. Once a model is in the file, Client(...) (or Client(config_path=...)) can be made.

Closing ​

python
client = zc.Client(".")
try:
    await client.prompt("hello")
finally:
    await client.aclose()

async with does this for you. Closing stops a run in progress, releases the provider connection and the session lock, and restores the process's working directory when the last client closes. It is safe to call twice. After it, every method raises ClientClosedError.

TIP

If the constructor raises, nothing is left open: the lock and the working directory are released before the error reaches you.

One workspace per process ​

A client changes the process's working directory to its workspace because the coding tools resolve relative paths against it. Two clients on the same workspace are fine. A client on a different one raises WorkspaceConflictError until the first closes.

python
from zettcode.sdk.errors import WorkspaceConflictError

async with zc.Client("/work/a") as a:
    try:
        zc.Client("/work/b")
    except WorkspaceConflictError as error:
        print(error)

To work on two workspaces at once, use two processes: multiprocessing, a subprocess, or a job queue.

Resuming a stored session ​

python
async with zc.Client(".", resume="3f8a1c20") as client:
    print(client.session.title)
    result = await client.prompt("where were we?")

An id that cannot be read raises SessionNotFoundError from the constructor. See Sessions for finding ids.

Released under the MIT license.