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
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='...')| Argument | Meaning |
|---|---|
workspace | The directory the agent works in. None is the current directory (or config.workspace). ~ is expanded, and it must exist. |
config | A ready ZettCodeConfig. Nothing is read from disk and the embedding defaults are not applied. |
config_path | A config file to read instead of $ZETTCODE_CONFIG or ~/.zettcode/config.toml. |
models | Models to use without any config file; the first is active. See below. |
model | The model to start on, by the name client.models lists. |
effort | The reasoning level to start on. |
permission | The shell permission level: deny, ask, safe or full (auto, the old name of full, is still taken). |
resume | A session id to continue instead of starting a new conversation. |
| every other setting | A 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.
try:
zc.Client(".", permission="sometimes")
except zc.ConfigurationError as error:
print(error) # Unknown permission: sometimes. Try one of: deny, ask, safe, fullSettings
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.
| Setting | Type | What it is |
|---|---|---|
store | str, Path | Where sessions are stored. |
theme_file | str, Path, None | A palette file laid over the theme. |
theme | str, None | The palette in force: dark, light, or a file's name. |
themes_dir | str, Path | Where the reader's own palettes live. |
pet | str, None | The companion over the conversation, or None for none. |
pet_position | tuple[float, float], None | Where it stands. |
reasoning_effort | ReasoningEffort, str | The level a new session starts at. |
shell_approval | ShellApprovalMode, str | deny, ask, safe or full. |
reduced_motion | bool | No animation, for a reader who asked for none. |
parallel_tool_call | bool | Let the model call several tools in one round. |
max_iterations | int | How many rounds one request may take. |
transcript_max_entries | int | How many rows the terminal keeps. |
mouse_scroll_speed | float | Rows per wheel notch. |
tips | bool | Tips in the waiting row. |
agents_md_enabled | bool | Read AGENTS.md into the conversation. |
ask_user_enabled | bool | Give the model the ask_user tool. |
skills_enabled | bool | Offer skills. |
skill_roots | Sequence[str | Path] | Where skills are looked for. |
skills_scope | str | global, workspace or both. |
mcp_enabled | bool | Start the configured MCP servers. |
mcp_config | str, Path, None | The servers' file. |
mcp_scope | str | global, workspace or both. |
self_compaction | bool | Let the model compact its own context. |
statusline_hidden | Sequence[str] | The parts of the status line that are switched off. |
plugins_enabled | bool | Load plugins. |
disabled_plugins | Sequence[str] | Plugins to leave out by name. |
plugin_settings | Mapping[str, Mapping[str, object]] | The [plugins.<name>] tables: what each plugin reads, by plugin name. |
subagent_enabled | bool | Give the model the subtask tool. |
subagent_max_iterations | int | Rounds a subtask may take. |
subagent_messaging | bool | Let a subtask message other sessions. |
todowrite_enabled | bool | Give the model the to-do list tool. |
jit_harness | bool | Let the model write and run its own tools. |
shell_network | bool | Let a sandboxed command reach the network. |
sessions_enabled | bool | Join the broker other sessions reach this one through. Off unless asked, and the terminal turns it on. |
update_enabled | bool | Check PyPI for a newer release in the background. Off unless asked. |
update_file | str, Path | What that check leaves behind. |
harness_dir | str, Path | Where 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:
config=, a readyZettCodeConfig, used as given.models=, laid over the defaults. The config file is not read at all.- 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:
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:
| Field | Default | Meaning |
|---|---|---|
model | required | The 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_model | None | The 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_url | None | The API root. A trailing /chat/completions, /v1/messages or /responses is dropped. |
anthropic | False | Speak Anthropic's Messages API. |
responses_api | False | Use the Responses API on an OpenAI-compatible endpoint. Not combinable with anthropic. |
chat | False | Say 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. |
multimodal | False | Whether the model accepts images. |
temperature | None | Sampling 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_window | 128000 | Tokens the model can carry. |
compact_percent | 80.0 | The share of the window at which the conversation is compacted. |
reasoning_efforts | all levels | The 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.
| Setting | Default | Effect |
|---|---|---|
store | ~/.zettcode/sessions | Where sessions are kept. Point it at a temporary directory in tests. |
max_iterations | 360 | Tool-call rounds allowed in one turn. |
mcp_enabled | True | Load MCP servers. |
skills_enabled | True | Discover local skills and offer them to the model. |
agents_md_enabled | True | Give the model the AGENTS.md files that apply to the workspace. |
ask_user_enabled | True | Give the model the ask_user tool. Switch it off for runs nobody can answer. |
subagent_enabled | True | Offer the task tool, which runs a child agent. |
todowrite_enabled | False | Offer the todo_write tool. |
plugins_enabled | True | Load third-party plugins. |
parallel_tool_call | True | Let the model issue tool calls in parallel. |
sessions_enabled | False here | Join the broker so terminal sessions can message this one. |
update_enabled | False here | Check 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:
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:
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.
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)| Member | Returns |
|---|---|
client.models | A tuple of ModelInfo(name, model, context_window, multimodal, efforts, active, compact_percent, web_search, web_search_name). |
client.model | The 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.efforts | The 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.permissions | The 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.harness | Whether 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.
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, bThe 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.
| Level | A 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
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 spentclient.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.
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:
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 oneadd_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
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.
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
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.