Agent
Agents: hosted agents you message by name.
class AgentA hosted agent. Agent.from_name("host", create_if_missing=True, instructions=...) is lazy; agent.message("hi").text talks to it.
An account-level agent is shared: each user that messages it (via
as_user or user=) gets its own conversations. A user's own
agent always acts for that user.
The agents built in the NoClick app are listed and messaged here too
(origin app, an id like node:<workflow>:<node>): send and
message reach them, and what they are is changed in the app.
- Like every live object, it also has hydrate(), refresh() and raw.
| id | str | |
| origin | str | None |
|
| name | str | |
| model | str | None | The model it runs, as its harness names it ( |
| harness | str | None | What runs it: |
| model_connection | str | None | The connection whose key or plan it thinks with, by name ( |
| instructions | str | None | |
| tools | list[Any] | What it can use, as listed: refs, |
| can_request | list[str] | What it may ask its user to connect at run time. |
| user_id | str | None | |
| workflow_id | str | None | |
| node_id | str | None | An app agent's step in its workflow (None for an API agent). |
| workflow_name | str | None | An app agent's workflow, by name. |
| access | str | None | An app agent's workflow as you hold it: |
| enabled | bool | None | Whether an app agent is switched on; a disabled one takes no messages. |
| app_url | str | None | Where an app agent is changed: its workflow in the NoClick app. |
| parent_id | str | None | |
| ttl_s | int | None | The time to live it was created with, in seconds (None if it isn't temporary); |
| expires_at | Any | When a temporary agent's |
| budget_usd_per_month | float | None | |
| region | str | None | The region its sandboxes run in ( |
| machine_id | str | None | The |
| created_at | Any | |
| browser_profile_id | str | None | The browser profile its |
| card_id | str | None | The |
| volumes | dict[str, str] | None | The volumes its shell mounts, by mount path: |
| memory_id | str | None | Its own memory ( |
| memories | list[dict[str, Any]] | None | The memories it reads besides its own, in order: each |
| secret_ids | list[str] | None | The secrets in its shell's environment, in order (a later one's key wins). |
| timezone | str | None | The IANA timezone each turn is told the date and time in ( |
| repos | list[dict[str, Any]] | None | The repositories cloned into its workspace at a conversation's first turn: each its |
| network | Any | What its shell and code reach: |
| shell | bool | None | Whether it has a shell, the |
| status | str | None |
|
| last_active_at | Any | When one of its turns last started or ended ( |
| running_turns | int | None | How many of its turns are queued, running or waiting on a person, any conversation (hidden like |
| child_count | int | None | How many live (not expired) children it has; |
| job_id | str | None | The task it was spawned with, an |
| used_by | list[dict[str, Any]] | The apps that bind it: |
| paused | bool | Whether |
| job | Job | None | The task it was spawned with, as a |
| trigger_kinds | list[str] | The kinds of its triggers ( |
| console_url | str | None | This agent in the developer console (None off the hosted API). |
| a2a_url | str | The A2A JSON-RPC endpoint ( |
| memory | AgentMemory | What it remembers across conversations: |
createclassmethodasync
noclick.Agent.create(
name, *, instructions, model=None, harness=None, model_connection=None, tools=None,
ttl_s=None, parent_id=None, budget_usd_per_month=None, can_request=None,
triggers=None, browser_profile=None, card=None, volumes=None, secrets=None,
timezone=None, repos=None, network=None, shell=None, region=None, machine=None,
memories=None, user=..., client=None
) -> AgentCreate an agent (names are unique per account and user; a
duplicate raises noclick.Conflict with existing_id).
harness is what runs it: "sdk" (the default), NoClick's own agent
loop, or a coding agent in a sandbox of its own with a shell and files:
"claude_code", "codex", "opencode", "hermes" or "openclaw"
(Agent.harnesses() lists them). model is the harness's own: an
OpenRouter id with the openrouter/ prefix on sdk, an alias such as
"sonnet" on claude_code, a provider/model on opencode, hermes
and openclaw; omitted, the harness's default. model_connection is the
connection whose key or plan it thinks with (a Connection of a
kind="model" integration such as anthropic, or its name, in the
agent's own scope; a lazy create_if_missing one is created first): every
CLI harness needs one, and it may still be pending. On sdk it's optional,
and lets model name anthropic/... or openai/... on your own key.
A Connection object from another scope, such as your account's key for a
user's agent, stays that connection (invite-only beta): only a key
acting for the whole account may set one, and it must hold a key, since a
person's Claude or ChatGPT plan is never shared.
tools are connections (a Connection, or its name: every operation), one
operation of one ("gmail.send_email_message"), the capabilities "search" and
"fetch", a Toolkit, or a Database (its query, execute and
schema, or db.only(...); invite-only beta), or a browser ("browser", or
"browser.act" and the like; invite-only beta), or a person (a Human, or human.only(...):
tools that notify them, ask them and wait for the answer, or hand them the
conversation), or the agents capability ("agents", or noclick.Agents.only(...):
tools to spawn, message, ask and keep track of other agents), or NoClick itself ("noclick", or
noclick.Platform.only(...): your account's API as tools, a purchase or a deletion waiting for
your approval): at most 64 tools in all. A string names
a connection in the agent's own user (the account for an account agent, whose
"credential:<id>" names an account connected in the NoClick app), else an
integration's operations that need no account (on NoClick's key), so
tools=["exa.search"] (or "exa", all of them) needs no connection. A
Connection object from another scope, another user's or the
account's, stays that connection (a key acting for the whole account only;
invite-only beta). A lazy create_if_missing connection is created first. Every agent can also
run shell commands (the code capability; a policy that denies
code refuses them, and shell=False removes it). ttl_s makes it temporary:
once that many seconds have passed it's expired, taking no turns (its pending
wake-ups are canceled) and readable for 30 days, then deleted; parent_id (an Agent or id) makes it a
child of that agent, its budget carved from the parent's, with nothing started
(parent.spawn also starts a task); budget_usd_per_month caps its spend.
can_request lets it ask its user to connect accounts at run time
(a request_connection tool): integration names, single
"integration.operation"s, or "*". What the user connects is a
grant (grants()), kept apart from this definition.
triggers wake it without a message: noclick.OnWebhook, OnEvent, OnPlatformEvent,
Cron and OnQueue (see triggers()).
timezone (an IANA name) is the zone every turn is told the date and time in;
without one, the acting user's timezone, else UTC.
region (noclick.regions()) is where its sandboxes run, for good: a coding
harness runs there whole, and an sdk agent's shell does while its loop runs in
NoClick's own service. Without one it takes its parent's, else your account's
default region, else runs wherever NoClick has capacity. region=db.region keeps
an agent beside its data. machine (a Machine or id; invite-only
beta) runs its harness on an always-on machine instead: no sandbox per
conversation, nothing reaped when it's quiet, no per-minute sandbox charge,
and it runs in the machine's region. A coding harness there reaches its model
only through NoClick's credential proxy, so its model_connection holds an
Anthropic, OpenAI or OpenRouter key (a Claude or ChatGPT plan raises
noclick.NotAvailable); a machine runs at most 50 agents.
volumes mounts Volumes (or their ids or names: its user's own,
then the account's) read-write in its shell at absolute paths
({"/data": vol}), in every conversation, up to 5; not at
/workspace, /root or a system folder. An sdk agent's shell
sees each as it was when the turn started and saves its writes when the
turn ends; a coding harness's sandbox, from when it starts until it stops.
memories are the Memorys it reads besides its own (agent.memory), in order, up to 10:
a Memory, its id or name (its user's own, then the account's), or another agent's memory.
Each is read-only to it unless given as memory.writable(); a user's agent reads the account's
memories but never writes them, and never another user's. Every turn starts with each entry's name
and description, its own memory's first.
repos clones repositories into its workspace, /workspace/, at a
conversation's first turn: connection.checkout(...) for one a github
connection of its own scope reaches, up to 10 (none with shell=False). Its git reaches those
repositories only, through NoClick, with a key made for its sandbox; the
connection's token never enters it. A push goes through only to the branches
the checkout's push globs name (none: read-only): NoClick judges each push
on its way to the git host, because a harness's own git push is no tool
call your policy could judge. gh only reads them there; pull requests and
issues are the connection's operations, in tools. repo.checkout(...),
for a NoClick Repo, is in invite-only beta.
secrets (Secrets, ids or names; up to 20) become environment
variables in its shell on every turn, a later secret's key winning a clash
(a change reaches its next turn): its user's and the account's, an account
agent's the account's only. Its shell takes no model provider's key
(OPENAI_API_KEY and the like belong on model_connection): a secret
holding one raises noclick.InvalidRequest.
shell=False takes the shell (the code capability) away from an sdk
agent, and from the children it spawns; a CLI harness refuses it with
noclick.InvalidRequest, since its shell is the harness.
network says what its shell and code may reach: True (the default:
anywhere), False, or up to 50 hosts ("pypi.org", or "*.example.com"
for a domain and its subdomains), over HTTPS on port 443 only. What it needs
to work stays reachable, and False leaves only that: an sdk agent's
shell reaches NoClick's git proxy when it has repos (its model and tools run
on NoClick); a coding harness reaches NoClick's API, NoClick's proxy for its
model on a key and for git, and on a Claude or ChatGPT plan that provider's own
hosts. Its tools run on NoClick, outside the limit. Without one it takes its
parent's, else anywhere, and the children it spawns take its. Where NoClick
can't keep to a limited network (a coding harness on a key other than
Anthropic, OpenAI or OpenRouter, say), it raises noclick.NotAvailable
rather than widen it.
Invite-only beta:
browser_profile (a BrowserProfile or id of its own user's, or your
account's for an account agent) signs its browser tool in with that
person's logins: each conversation gets a browser of its own for a turn, a
step only a person can do holds the turn awaiting_human, and its calls
follow policy as browser.*. card (a Card or id of its own scope: a
user's agent pays with that user's cards, an account agent with your account's;
giving one takes the key's cards scope) is what it pays with: it holds the card
(a card has one agent at a time) and gets the tools card__status (what it can
spend now) and card__purchase (approve before paying: above the card's threshold
the turn waits for a person, and their answer is its next turn), which policy names
card.status and card.purchase. It never sees the number, and a turn answering
someone outside its scope, or a test run, can't buy anything. A child it spawns pays
with the same card only when given "card" (or one of the tools) in its tools.
NoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneWho the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)The new agent, loaded.
Outside as_user the agent is your account's, shared by every user that messages it:
support = noclick.Agent.create(
"support",
instructions="You answer questions about our product. Search the docs before answering.",
tools=["search", "fetch"],
budget_usd_per_month=50,
)Inside as_user it's that user's own, and uses the user's connections:
with noclick.as_user("user_42"):
gmail = noclick.Connection.from_name("gmail", integration="gmail", create_if_missing=True)
assistant = noclick.Agent.create("assistant", instructions="You help the user with their email.", tools=[gmail])An operation that needs no account needs no connection, for any agent, a user's included:
with noclick.as_user("user_42"):
scout = noclick.Agent.create("scout", instructions="You research companies for the user.", tools=["exa.search"])Run it on Claude Code, thinking with your Anthropic key. Its reply arrives whole, and a conversation's first turn starts its sandbox:
claude = noclick.Connection.from_name("claude", integration="anthropic", create_if_missing=True)
coder = noclick.Agent.create(
"coder",
instructions="You maintain our docs repo.",
harness="claude_code",
model="sonnet",
model_connection=claude,
tools=["github", "search"],
)
if not claude.connected:
print(claude.connect_url) # where someone pastes an Anthropic keyIf the name may be taken, the conflict names the agent already there:
try:
support = noclick.Agent.create("support", instructions="You answer questions about our product.")
except noclick.Conflict as exc:
support = noclick.Agent.get(exc.existing_id)Give a coding agent a repository to work in, branches of its own to push to, and a clock in your team's timezone:
github = noclick.Connection.from_name("github", integration="github", create_if_missing=True)
fixer = noclick.Agent.create(
"fixer",
instructions="You fix failing tests in acme/web, push the fix to a branch and open a pull request.",
harness="claude_code",
model_connection="claude",
timezone="Europe/Berlin",
tools=[github.only("create_pull_request")],
repos=[github.checkout("acme/web", push=["agent/*"])],
)Keep a coding agent's sandbox in Europe, with everything it works on:
reviewer = noclick.Agent.create(
"reviewer",
instructions="You review pull requests for our EU customers' code.",
harness="codex",
model_connection="openai",
region="eu",
)An agent that only answers from its tools, with no shell:
faq = noclick.Agent.create("faq", instructions="You answer from our help center.", tools=["search"], shell=False)Keep its shell to the package index it installs from, beside its data:
analyst = noclick.Agent.create(
"analyst",
instructions="You answer questions about our orders with pandas, from the data in /data.",
volumes={"/data": noclick.Volume.from_name("orders", create_if_missing=True)},
network=["pypi.org", "files.pythonhosted.org"],
)from_nameclassmethod
noclick.Agent.from_name(
name, *, create_if_missing=False, instructions=None, model=None, harness=None,
model_connection=..., tools=None, ttl_s=None, parent_id=None,
budget_usd_per_month=..., can_request=None, triggers=None, browser_profile=...,
card=..., volumes=None, secrets=None, timezone=..., repos=None, network=None,
shell=None, region=None, machine=None, memories=None, user=..., client=None
) -> AgentA lazy reference to the agent called name in the current (or
given) user: inside as_user only that user's own agent, never an account agent
of the same name (reach one of those outside as_user, then message it for the
user). With create_if_missing=True your code is the
definition: it's created on first use with the fields Agent.create
takes, and an existing agent whose instructions, model, harness,
model_connection, tools, can_request, budget, timezone, volumes, memories, secrets,
repos, network or shell differ is
updated to match (the grants its user gave it are kept; a new harness without a
model takes that harness's default model, and its conversations carry on
on the new harness). So are its triggers: a trigger
your code no longer lists is removed and a new one added, while one that
stays keeps its webhook URL; mail and call triggers are never touched. So are its
browser_profile and card (invite-only beta). A field you leave
out is never changed, and None clears model_connection, budget_usd_per_month,
browser_profile, card or timezone, as in update(): moving an agent back to sdk takes
harness="sdk", model_connection=None. region is fixed once it's created: an existing agent in
another region raises noclick.Conflict naming its region.
FalseNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneNoneWho the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)An agent that loads (or is created, or brought in step with your definition) the first time it's used. Nothing is sent yet.
Refer to an agent by name without a request:
support = noclick.Agent.from_name("support")
print(support.model) # loads it now; noclick.NotFound if there's noneOr make your code the definition. instructions is required then, and a field
you leave out is never changed on an agent that already exists:
with noclick.as_user("user_42"):
assistant = noclick.Agent.from_name(
"assistant",
create_if_missing=True,
instructions="You help the user run their day. Ask for access to an account when you need it.",
tools=["search"],
can_request=["google_calendar", "gmail"],
)
reply = assistant.message("Put lunch with Bo on my calendar for Friday at noon")An automation is an agent whose definition includes a schedule:
digest = noclick.Agent.from_name(
"digest",
create_if_missing=True,
instructions="You summarize yesterday's support tickets for the team.",
tools=["search", "slack.send_message_to_channel"],
triggers=[noclick.Cron("0 8 * * 1-5", tz="Europe/London", message="Post the overnight summary in #support")],
)Give its shell a dataset and an API key:
analyst = noclick.Agent.from_name(
"analyst",
create_if_missing=True,
instructions="You answer questions about our orders. The data is in /data; call the warehouse "
"API with $WAREHOUSE_TOKEN for stock levels.",
volumes={"/data": noclick.Volume.from_name("datasets", create_if_missing=True)},
secrets=["warehouse"],
)from_idclassmethod
noclick.Agent.from_id(agent_id, *, client=None, user=...) -> AgentA lazy reference to an agent by NoClick id.
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
An agent that loads the first time it's used. Nothing is sent yet.
agent = noclick.Agent.from_id("7b1e4f2a-3c9d-4e8b-a5f6-0d2c8e9b1a47")
reply = agent.message("Hi")getclassmethodasync
noclick.Agent.get(agent_id, *, user=..., client=None) -> AgentLoad an agent by NoClick id now; an unknown id raises noclick.NotFound.
Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)The agent, loaded.
agent = noclick.Agent.get("7b1e4f2a-3c9d-4e8b-a5f6-0d2c8e9b1a47")
print(agent.name, agent.model, agent.user_id)listclassmethodasync
noclick.Agent.list(
*, name=None, status=None, active_since=None, parent=None, temporary=None,
harness=None, order=None, origin=None, limit=None, user_filter=None, user=...,
client=None
) -> list[Agent]The agents the current user can message: inside as_user, that user's own
agents and the account's (which the user can message but not change; their
user_id is None), so a name may match one of each; outside it, the
account's. user_filter lists exactly whose agents instead: a User or your
id for one (only theirs), noclick.NO_USER (the account's) or noclick.ALL_USERS.
After the account's API agents come the ones built in the NoClick app, which are
the account's too (origin="api" or "app" lists only one kind). Of the
filters below, only name and harness apply to them; the others leave them out.
Expired agents are listed for the 30 days they're kept. status keeps the
working, idle or expired ones; active_since (an aware datetime)
those with a turn since then; parent (an Agent or id) its children;
temporary those with (or, False, without) a ttl_s; harness those on one
harness; and order="last_active" puts the most recently active first, those that
never ran last ("created", the default, the newest first). These reads are cheap:
last_active_at is a column stamped as turns start and end, and the counts read
partial indexes.
NoneNoneNoneNoneNoneNoneNoneNoneReturn at most this many.
None)Which users a read covers: noclick.ALL_USERS, noclick.NO_USER, a User, or your id for one. It filters without acting as that user.
None)Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)Every matching agent, newest first (or most recently active first), fetched page by page.
The agents the current user can message, then one name across every user:
for agent in noclick.Agent.list():
print(agent.name, agent.user_id) # user_id is None for an account agent
assistants = noclick.Agent.list(name="assistant", user_filter=noclick.ALL_USERS) # every user's, and the account'sWhat's busy right now, and what ran in the last hour:
from datetime import datetime, timedelta, timezone
for agent in noclick.Agent.list(status="working"):
print(agent.name, agent.running_turns, agent.last_active_at)
hour_ago = datetime.now(timezone.utc) - timedelta(hours=1)
recent = noclick.Agent.list(active_since=hour_ago, order="last_active", temporary=False)The agents built in the app, and a message to one:
for agent in noclick.Agent.list(origin="app"):
print(agent.name, agent.workflow_name, agent.enabled, agent.app_url)
triage = noclick.Agent.list(origin="app", name="Triage")[0]
print(triage.message("Anything urgent in the queue this morning?").text)list_pageclassmethodasync
noclick.Agent.list_page(
*, cursor=None, limit=None, name=None, status=None, active_since=None, parent=None,
temporary=None, harness=None, order=None, origin=None, user_filter=None, user=...,
client=None
) -> PageOne page of agents; pass next_cursor back as cursor. Takes list's
filters and order.
The next_cursor of the page before; omit it for the first page.
None)Return at most this many.
None)NoneNoneNoneNoneNoneNoneNoneNoneWhich users a read covers: noclick.ALL_USERS, noclick.NO_USER, a User, or your id for one. It filters without acting as that user.
None)Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)A page whose data holds the agents and whose next_cursor is None on the last page.
page = noclick.Agent.list_page(limit=50)
while True:
for agent in page.data:
print(agent.name)
if page.next_cursor is None:
break
page = noclick.Agent.list_page(limit=50, cursor=page.next_cursor)harnessesclassmethodasync
noclick.Agent.harnesses(*, client=None) -> list[HarnessInfo]What can run an agent: sdk, NoClick's own agent loop, and the coding
agents that each run in a sandbox of their own (claude_code, codex,
opencode, hermes, openclaw). On a CLI harness a reply arrives whole
rather than streaming, a conversation's first turn starts its sandbox (it stays
warm about a minute between turns), and the sandbox's uptime is billed.
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)Each harness's name (what harness= takes) and default_model, whether it
needs a model_connection and which model providers' integrations serve it,
whether its reply streams (streaming) and whether its shell is built in.
models() on one searches the models it offers.
Pick a harness, then search the models it offers:
for harness in noclick.Agent.harnesses():
print(harness.name, harness.default_model)
[codex] = [h for h in noclick.Agent.harnesses() if h.name == "codex"]
print([m.id for m in codex.models(search="gpt-5")])Or find which model providers can run Codex, to make its connection:
[codex] = [h for h in noclick.Agent.harnesses() if h.name == "codex"]
print(codex.integrations, codex.notes) # ['openai'] [...]harness_infoclassmethodasync
noclick.Agent.harness_info(name, *, client=None) -> HarnessInfoOne of harnesses() by the name harness= takes; an unknown name raises
noclick.NotFound, naming the ones there are.
The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)The harness: its default_model, whether it needs a model_connection, the model
providers' integrations that serve it, and models() to search what it offers.
opencode = noclick.Agent.harness_info("opencode")
print(opencode.default_model, [m.id for m in opencode.models(search="claude", limit=5)])uploadasync
agent.upload(file, *, name=None, mime_type=None, voice=False, peaks=None) -> AttachmentStore a file (a path, bytes or a binary file object) to send this agent with a message.
It's kept as an account file, capped as those are: 10 MB an image (JPEG,
PNG, WebP or GIF), 15 MB a document, 20 MB audio or video. voice=True
marks a recorded clip (audio), which may carry its waveform peaks.
message(files=[...]) uploads paths for you; upload first to send bytes
or a file object you name, or a voice clip.
NoneNoneFalseNoneThe attachment; send it with message(files=[...]). An upload is sent once; sent, it's one of
the account's files (noclick.attachments()), which the account sends again.
support = noclick.Agent.from_name("support")
clip = support.upload(recording, name="question.webm", voice=True)
reply = support.message("Here's my question.", files=[clip])sendasync
agent.send(text, *, conversation=None, files=(), user=...) -> OperationStart a turn without waiting; .wait() returns the AgentReply.
files go with the message, as message sends them.
None()Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The turn's operation, started. Keep its id to pick the reply up later, from anywhere.
support = noclick.Agent.from_name("support")
turn = support.send("Summarize this customer's open tickets")
print(turn.id) # save it; then, later, anywhere:
reply = noclick.Operation.from_id(turn.id).wait() # an AgentReply
print(reply.text)messageasync
agent.message(
text, *, conversation=None, files=(), timeout=None, user=...
) -> AgentReplySend text and wait for the reply. Pass the reply's
conversation back to continue the same thread. A channel's conversation
(channel:<its id>) raises noclick.InvalidRequest: only the channel starts
turns there, so post in the channel instead. files (at most 100) are
paths, which are uploaded first, attachments from upload, or (for the
account) its files from noclick.attachments(): the turn reads them as
the agent's media digest does, a document's text, a voice clip's
transcript, an image as an image.
None()NoneWho the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The reply: its text, the conversation it belongs to and the tool_calls the turn made.
Ask, then keep talking in the same thread by passing its conversation back:
support = noclick.Agent.from_name("support")
reply = support.message("Why was I charged twice in March?")
print(reply.text)
follow_up = support.message("Can you refund one of them?", conversation=reply.conversation)Or name the thread yourself, with your own ticket id, say (1 to 128 letters,
digits and . _ : @ + -):
support = noclick.Agent.from_name("support")
reply = support.message("Where's my refund?", conversation="ticket-1841")A shared account agent talks to each of your users in threads of their own:
support = noclick.Agent.from_name("support")
reply = support.message("Where's my invoice?", user="user_42")With files:
analyst = noclick.Agent.from_name("analyst")
reply = analyst.message("What stands out this quarter?", files=["q3-report.pdf", "chart.png"])historyasync
agent.history(conversation, *, user=...) -> list[Message]The messages of one conversation, oldest first.
Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
Its messages, each with its role, text, created_at, the files it carried, how it
arrived (via, for a text, a call or an email) and, a reply, the tool_calls its turn made; a
note has its data.
support = noclick.Agent.from_name("support")
for message in support.history("ticket-1841"):
print(message.role, message.via or "api", message.text)noteasync
agent.note(conversation, text, *, data=None, key=None, user=...) -> boolAdd a line to a conversation without starting a turn: it shows in
history and watch as a note with its data, and the agent
reads both when its next turn starts, once.
Args:
conversation: The current user's conversation (the account's, outside as_user).
text: The line, 1-4,000 characters.
data: JSON your app reads back with it (at most 4,096 bytes).
key: Adds it once: a second note with the same key adds nothing.
NoneNoneWho the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
True when it was added, False when a note with key already was.
support = noclick.Agent.from_name("support")
support.note("ticket-1841", "Ada upgraded to the Pro plan.", data={"plan": "pro"}, key="plan:pro")conversationsasync
agent.conversations(*, limit=None, user_filter=None, user=...) -> list[Conversation]Its conversations, most recent first (the current user's, or those user_filter covers).
user_filter reads across users without the user header, which only the signed-in owner
may do on a user's own agent; with a secret key, read such an agent's threads without it.
Return at most this many.
None)Which users a read covers: noclick.ALL_USERS, noclick.NO_USER, a User, or your id for one. It filters without acting as that user.
None)Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
Every matching conversation (or the first limit), fetched page by page. cost_usd
is None for a thread with no recorded turns.
Read the current user's threads (the account's, outside as_user):
support = noclick.Agent.from_name("support")
for convo in support.conversations(limit=10):
print(convo.conversation, convo.title, convo.turns, convo.cost_usd)Or every user's threads with a shared agent, each naming its user_id
(None for the account's), and read one:
support = noclick.Agent.from_name("support")
for convo in support.conversations(user_filter=noclick.ALL_USERS, limit=20):
messages = support.history(convo.conversation, user=convo.user_id)conversations_pageasync
agent.conversations_page(*, cursor=None, limit=None, user_filter=None, user=...) -> PageOne page of its conversations; pass next_cursor back as cursor.
The next_cursor of the page before; omit it for the first page.
None)Return at most this many.
None)Which users a read covers: noclick.ALL_USERS, noclick.NO_USER, a User, or your id for one. It filters without acting as that user.
None)Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
A page whose data holds the conversations and whose next_cursor is None on the last page.
support = noclick.Agent.from_name("support")
page = support.conversations_page(limit=50)
while True:
for convo in page.data:
print(convo.conversation, convo.last_activity)
if page.next_cursor is None:
break
page = support.conversations_page(limit=50, cursor=page.next_cursor)hand_backasync
agent.hand_back(conversation, *, user=...) -> NoneGive a conversation back to the agent after it handed it to a person (its
hand_off tool). The person's replies stay in its history, and the agent answers
again from the next message. The person can hand it back from their page too.
Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
support = noclick.Agent.from_name("support")
for convo in support.conversations(user_filter=noclick.ALL_USERS):
if convo.status == "handed_off":
support.hand_back(convo.conversation, user=convo.user_id)spawnasync
agent.spawn(
task, *, updates=None, conversation=None, instructions=None, tools=None,
budget_usd=None, ttl_s=None, model=None, harness=None, model_connection=None,
name=None, user=...
) -> AgentMake a child agent and start task (its first message, up to 16,000
characters) at once. child.job is that task: a Job that runs while the
child works on it, across as many turns as it takes, and finishes when the child
stops with nothing left to continue it.
The child lives where its parent does and runs on the parent's harness, model and
model connection unless harness, model or model_connection say otherwise.
It gets only tools its parent has: tools names some of them (a narrower
only(...) of one of the parent's connections is fine, and "card" gives it the
card its parent pays with; anything the parent lacks raises noclick.InvalidRequest),
and without tools it has none of them (nor its parent's can_request). Like every agent it has its shell (none when its parent
has shell=False) and wake_me, and as a spawned child tell_parent, to reach its
parent whenever its instructions say to. A person among the parent's tools passes down
only with the tools the parent has of them, and a bound operation only as bound or
narrower. instructions are its
standing instructions: what its updates should say and when to reach out on its
own; NoClick adds a short brief saying it works on one task for its parent.
budget_usd is carved from the parent's monthly budget, and ttl_s makes it
temporary. name defaults to <parent>-<random>.
updates is only when NoClick makes it report: "done" (always on: the job
finished, any outcome), "blocked" (it started waiting on a person),
noclick.Before(*tools) (before a matching tool call runs) and
noclick.Every(interval) (at least that often while it works). Each update is a
wake-up for the parent in conversation, the parent's conversation it belongs to
(user= says whose, as for message: your key acting for a user spawns from an
account agent in that user's thread, and the child is the account's; a user's own
session can't); updates that come while the parent is mid-turn arrive together in its
next turn. Without conversation, updates wake nobody and wait on the job
(job.updates(), and your webhooks' agent.job.update event).
A coordinator spawns the same way with its agents tools, its children's updates
waking it in the conversation it spawned from. An agent holds up to 20 live
children, and children spawn children up to 3 levels below an agent with no parent
(noclick.QuotaExceeded). Agent.create(..., parent_id=) still makes a child
without starting anything.
NoneNoneNoneNoneNoneNoneNoneNoneNoneNoneWho the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The child, loaded, with parent_id set to this agent and job its running task.
Hand a coordinator's research to a helper and hear back in the launch conversation:
planner = noclick.Agent.from_name("planner")
researcher = planner.spawn(
"Find three recent launches in our category and summarize what worked.",
instructions="Report each launch as you finish it with tell_parent. Cite your sources.",
tools=["search", "fetch"],
updates=["blocked", noclick.Every(900)],
conversation="launch-plan",
budget_usd=0.50,
ttl_s=3600,
)
print(researcher.job.status) # runningFrom your own code, wait for the task the way you'd wait for a message:
planner = noclick.Agent.from_name("planner")
reviewer = planner.spawn("Review the draft in /workspace/launch.md and list what's missing.")
reply = reviewer.job.wait(timeout=600)
print(reply.text)Hear before money moves (the call still runs; a policy rule is what holds one):
support = noclick.Agent.from_name("support")
refunder = support.spawn(
"Refund order 1841 if it's within 30 days, then tell the customer.",
tools=["stripe.create_refund", "gmail.send_email_message"],
updates=[noclick.Before("stripe.create_refund")],
conversation="ticket-1841",
user="user_42",
)childrenasync
agent.children(*, status=None, limit=None) -> list[Agent]Its children, any status (an expired one for its 30 days), newest first:
Agent.list(parent=self), read where this agent lives.
NoneReturn at most this many.
None)Each child with its status, last_active_at, running_turns and, if spawn
made it, the job it was started with.
planner = noclick.Agent.from_name("planner")
for child in planner.children(status="working"):
print(child.name, child.running_turns, child.last_active_at)jobsasync
agent.jobs(
*, status=None, kind=None, agent=None, conversation=None, limit=None
) -> list[Job]The jobs it started: the tasks of the children it spawned (kind="task") and
the messages it sent with its agents tools (kind="message", whose only update
is done). status takes operation statuses, or "live" for
every unfinished one; agent (an Agent or id) keeps the ones one agent does;
conversation the ones started from one of its conversations. Read where this agent
lives: an account agent's are every user's.
NoneNoneNoneNoneReturn at most this many.
None)Every matching job (or the first limit), newest first, each with its status, step and
latest_update; wait() on one gives its reply.
coordinator = noclick.Agent.from_name("coordinator")
for job in coordinator.jobs(status="live"):
update = job.latest_update
print(job.agent_id, job.status, job.step, update.text if update else "")askasync
agent.ask(
question, *, conversation=None, interject=False, timeout=None, user=...
) -> AgentReplyAsk it something without stopping it. By default its model answers from a
snapshot of conversation as it stands: its instructions, the conversation, and
what a running turn has done so far, with no tools. The question and answer aren't
added to the conversation and the agent never sees them; a running turn carries on
untouched. The ask is recorded in its activity as an agent.ask operation and
billed like a short turn. On a coding harness the snapshot is what NoClick holds:
the conversation up to its last finished turn, plus the NoClick tool calls the
running turn has made; the harness's own shell commands and file edits only arrive
when its turn ends, and its model answers on its connection's key. An agent that
thinks with a Claude or ChatGPT plan raises noclick.NotAvailable: a plan runs
only in its harness, and answering in a one-off harness session is in invite-only
beta (interject=True works).
interject=True puts the question into the running turn instead (the harness's
own steering on a coding harness; before its next model call on sdk): the
agent sees it, may change course, and answers in that turn, which stays in the
conversation. With no turn running, it's an ordinary message. conversation
defaults to the one it last worked in for the acting user (a spawned child's: its
task's).
NoneFalseNoneWho the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The answer as an AgentReply; with interject, the reply the turn it joined ended with.
Check on a child without interrupting it:
planner = noclick.Agent.from_name("planner")
for child in planner.children(status="working"):
print(child.name, child.ask("In one line: where are you, and what's left?").text)Steer a coding agent mid-task:
fixer = noclick.Agent.from_name("fixer")
reply = fixer.ask("Skip the flaky e2e suite; the unit tests are enough.", conversation="issue-1841",
interject=True)a2a_cardasync
agent.a2a_card() -> dict[str, Any]The agent's A2A agent card (the JSON other agents discover it by).
The card as a dict: the agent's name, its JSON-RPC url and the skills it offers.
planner = noclick.Agent.from_name("planner")
card = planner.a2a_card()
print(card["url"], [skill["name"] for skill in card["skills"]])updateasync
agent.update(
*, instructions=..., model=..., tools=..., name=..., harness=...,
model_connection=..., budget_usd_per_month=..., can_request=..., timezone=...,
repos=..., network=..., shell=..., card=..., machine=..., memories=...
) -> AgentChange fields. Grants its user gave it are kept. A new harness without
a model takes that harness's default model, and each conversation carries
on on the new harness, with its own memory and the memories it attaches, from
its next turn. memories are as in create ([] detaches them all).
model_connection=None removes
an sdk agent's own model key. timezone is as in create, and
timezone=None goes back to the acting user's zone. repos are as in
create, from its next turn (a running sandbox restarts with them; a
clone already in a conversation's workspace stays as it is). shell is as in
create, and network too, from its next turn (a coding harness's running
sandbox restarts with it). card (invite-only beta) gives it a card in
place of the one it holds, as in create, and card=None takes its card back.
machine (invite-only beta) moves it onto a
Machine, or with None back to a sandbox per conversation; each
conversation moves between turns and keeps its files.
This agent, updated. A field you leave out is unchanged, and changes apply from its next turn.
Change what it does and what it can use:
support = noclick.Agent.from_name("support")
support.update(
instructions="You answer billing questions and can refund a charge.",
tools=["stripe.list_invoices", "stripe.create_refund", "search"],
)Move it to Codex, on an OpenAI connection:
openai = noclick.Connection.from_name("openai", integration="openai", create_if_missing=True)
support = noclick.Agent.from_name("support")
support.update(harness="codex", model_connection=openai)
print(support.model) # codex's default modelTell it the time in New York:
support = noclick.Agent.from_name("support")
support.update(timezone="America/New_York")Keep its shell off the open internet:
support = noclick.Agent.from_name("support")
support.update(network=["api.stripe.com"])Let it read what another agent remembers:
sales = noclick.Agent.from_name("sales")
noclick.Agent.from_name("support").update(memories=[sales.memory])grantsasync
agent.grants() -> list[AgentGrant]The connections its user gave it at run time (requested or active), oldest first.
Each grant with the connection it names, its integration, its status and the
operations it covers (None: every one).
assistant = noclick.Agent.from_name("assistant", user="user_42")
for grant in assistant.grants():
print(grant.connection, grant.integration, grant.status, grant.operations)
# google_calendar google_calendar active Nonerevoke_grantasync
agent.revoke_grant(grant) -> NoneTake back a grant (an AgentGrant or its id): its tools leave the
agent; the connection stays the user's.
assistant = noclick.Agent.from_name("assistant", user="user_42")
for grant in assistant.grants():
if grant.integration == "slack":
assistant.revoke_grant(grant)deleteasync
agent.delete() -> TrashItemDelete the agent and the sub-agents under it: they go to the trash, restorable for 30 days. Its pending wake-ups and jobs end now and stay ended if it's restored.
noclick.Agent.from_name("support").delete()wakeasync
agent.wake(
conversation, *, note, at=None, after=None, when=None, filters=None, every=None,
user=...
) -> WakeupHave it take a turn later in conversation, told note (up to 4,000
characters) and what woke it. Exactly one of: at (an aware datetime),
after (seconds, or a timedelta), when: an Operation (a Job,
a HumanRequest, a ConnectLink, ...: it wakes when that settles, with its
outcome; one already settled wakes it at once), or a NoClick event type
("connection.connected", "human.answered"; * matches the rest of a name),
narrowed by filters, dotted paths into the event's data and the values they
must have: the first matching event after it was set wakes it, with the event; or
every, a noclick.Cron: a routine that wakes the conversation at each of its
times until it's canceled (tz="user" follows the conversation's user). A
user's conversation hears that user's events; one of your account's own hears every
event, your users' included. user= says whose conversation it is, as for
message.
A turn already running in the conversation goes first: the wake-up waits for it,
never Conflict. A channel's conversation raises noclick.InvalidRequest, as for
message. It survives restarts, starts its turn once, and is billed and
capped like any turn; when the turn can't be paid for, the wake-up fails with the
reason. A recurring one stays pending, due at its schedule's next time after each
turn, so times it missed (held, or late) fold into one turn. Every agent can also wake
itself with its wake_me tool, and list, change and cancel its own (list_wakeups,
cancel_wakeup). An agent holds up to 100 pending wake-ups and 20 recurring ones
(noclick.QuotaExceeded); a time is 1 minute to 365 days ahead, a schedule comes
round at most every minute (every 15 for one the agent sets itself), and one
conversation's wake-ups are delivered at least a minute apart.
NoneNoneNoneNoneNoneWho the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The wake-up, pending; update() changes it, cancel() calls it off.
Pick a conversation up tomorrow morning, Berlin time:
from datetime import datetime
from zoneinfo import ZoneInfo
support = noclick.Agent.from_name("support")
support.wake("ticket-1841", at=datetime(2026, 10, 2, 9, 0, tzinfo=ZoneInfo("Europe/Berlin")),
note="Check whether the replacement shipped, and tell the customer.")Carry on once the user has connected their calendar:
with noclick.as_user("user_42"):
assistant = noclick.Agent.from_name("assistant")
calendar = noclick.Connection.from_name("calendar", integration="google_calendar", create_if_missing=True)
link = calendar.connect_link(reason="to book your flights")
assistant.wake("trip-planning", when=link, note="The calendar is connected: book the flights now.")Or when the user's connection named gmail connects:
with noclick.as_user("user_42"):
assistant = noclick.Agent.from_name("assistant")
assistant.wake("inbox-zero", when="connection.connected", filters={"name": "gmail"},
note="Gmail is connected: sort the inbox as we discussed.")Keep a routine: every weekday at 8, in the user's own timezone:
with noclick.as_user("user_42"):
assistant = noclick.Agent.from_name("assistant")
briefing = assistant.wake("briefing", every=noclick.Cron("0 8 * * 1-5", tz="user"),
note="Brief me on today's calendar and anything urgent in my inbox.")wakeupsasync
agent.wakeups(
conversation=None, *, status=None, limit=None, user=..., user_filter=None
) -> list[Wakeup]Its wake-ups in the acting user's conversations (or one conversation), or in
those of the users user_filter covers: those your code set and those it set itself
with wake_me, and NoClick's own: a child's update (update), the link it asked its
user to connect an account with being done (answer) and a decided approval
(approval). status keeps pending, delivered,
canceled or failed ones. user_filter (noclick.ALL_USERS,
noclick.NO_USER, a User or your id for one) reads across users without the user
header, as conversations does: an account agent's wake-ups in every user's threads.
NoneNoneReturn at most this many.
None)Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
Which users a read covers: noclick.ALL_USERS, noclick.NO_USER, a User, or your id for one. It filters without acting as that user.
None)Every matching wake-up (or the first limit), newest first, each with its source, when it's
due (at) or what it waits for (when), whose thread it's in (user_id) and, once delivered,
the turn it started. cancel() acts in that wake-up's own thread.
What will wake it in the acting user's threads:
support = noclick.Agent.from_name("support")
for wakeup in support.wakeups(status="pending"):
print(wakeup.conversation, wakeup.source, wakeup.at or wakeup.when, wakeup.note)Or what will wake a shared agent in any of its users' threads:
support = noclick.Agent.from_name("support")
for wakeup in support.wakeups(status="pending", user_filter=noclick.ALL_USERS):
print(wakeup.user_id, wakeup.conversation, wakeup.at, wakeup.note)triggersasync
agent.triggers() -> list[AgentTrigger]What wakes it besides a message: its schedules, webhooks, app events, NoClick events and queues, and the inboxes and phone numbers that hand it mail and calls.
Each trigger with its kind, its spec, its status (paused while the agent is),
the url a webhook listens at, when a schedule's next_run is and when it
last_fired_at. An error says why one can't fire.
support = noclick.Agent.from_name("support")
for trigger in support.triggers():
print(trigger.kind, trigger.status, trigger.url or trigger.spec)add_triggerasync
agent.add_trigger(trigger) -> AgentTriggerAdd a trigger: a noclick.OnWebhook, OnEvent, OnPlatformEvent, Cron
or OnQueue. An agent holds up to 20. (An agent
defined with from_name(create_if_missing=True, triggers=...) gets its list
from your code instead, and loses one added here the next time that code runs.)
The trigger as added; a webhook's url is where to send its POSTs. An event
trigger whose connection isn't connected yet is error until it is.
Give an agent a URL your systems can wake it at:
fulfilment = noclick.Agent.from_name("fulfilment")
orders = fulfilment.add_trigger(noclick.OnWebhook(name="orders"))
print(orders.url)Or wake it on each new HubSpot contact:
sales = noclick.Agent.from_name("sales")
sales.add_trigger(noclick.OnEvent("hubspot", "contact_created", message="Research this contact."))Or put it on a schedule, which makes it an automation:
support = noclick.Agent.from_name("support")
support.add_trigger(noclick.Cron("0 17 * * 5", tz="America/New_York", message="Send the weekly ticket report"))remove_triggerasync
agent.remove_trigger(trigger) -> NoneRemove a trigger: an AgentTrigger (or its id), or the definition you
added (noclick.NotFound if the agent has none like it): an OnWebhook
by its name alone, as names are unique on an agent. Mail and call
triggers are changed on their inbox or number, not here.
support = noclick.Agent.from_name("support")
for trigger in support.triggers():
if trigger.kind == "webhook":
support.remove_trigger(trigger)inboxesasync
agent.inboxes() -> list[Inbox]The inboxes whose mail wakes it (created or updated with agent= it).
Every such inbox, newest first, each with its address.
support = noclick.Agent.from_name("support")
print([inbox.address for inbox in support.inboxes()])pauseasync
agent.pause() -> AgentStop every trigger from waking it: scheduled runs are skipped (not made up later), webhook calls are refused with 409 so their sender can retry, and app events, mail and calls that arrive meanwhile don't wake it. Messages your code sends still run.
This agent, with paused true.
digest = noclick.Agent.from_name("digest")
digest.pause()
print(digest.paused) # Trueresumeasync
agent.resume() -> AgentLet its triggers wake it again, including one that stopped after failing the same way five times; a schedule picks up at its next run.
This agent, with paused false.
digest = noclick.Agent.from_name("digest")
digest.resume()workspace
agent.workspace(conversation, *, user=...) -> AgentWorkspaceThe files it works on in one conversation (AgentWorkspace). Sends nothing yet.
Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The conversation's workspace, whose files, read, write and url reach its files.
support = noclick.Agent.from_name("support")
workspace = support.workspace(conversation="ticket-1841", user="user_42")
for file in workspace.files():
print(file.path, file.size_bytes)streamiterator
agent.stream(text, *, conversation=None, files=(), user=...) -> Iterator[AgentEvent]Send text (with any files, as message sends them) and watch
the turn as it happens: the reply as it's written, each tool call and its
result, each file, approval and long action, then the whole reply. The
same turn as message, billed the same.
None()Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
AgentEvent values in order, ending with one whose type is done and whose reply is the
AgentReply. A turn that fails raises its error instead.
Print the reply as it's written:
support = noclick.Agent.from_name("support")
for event in support.stream("Where's my refund?", conversation="ticket-1841"):
if event.type == "text":
print(event.text, end="", flush=True)
elif event.type == "tool_call":
print(f"\n[{event.tool}]")In async code, relay it to your own UI:
support = noclick.Agent.from_name("support")
async for event in support.stream.aio("Where's my refund?", user="user_42"):
await websocket.send_json(event.raw)watchiterator
agent.watch(conversation, *, user=...) -> Iterator[AgentEvent]Follow one conversation live, whoever moves it: turns a trigger, an answered
ask or an approved held call started, messages sent from another client or
session, and a person's replies after a hand-off. Your own message and
stream turns show up too; tell them apart by operation_id. It runs until
you stop reading (break) or the conversation is deleted.
Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
AgentEvent values as they happen: message (a whole message, with its role:
user, assistant or person, and its files), text as a reply is written,
tool_call (held names the approval when policy holds it), tool_result, file,
approval and operation (from its "noclick" tools), and status when the
conversation starts waiting on a person, is handed off or becomes active.
Push what happens in a support thread to your own UI:
support = noclick.Agent.from_name("support")
for event in support.watch("ticket-1841", user="user_42"):
if event.type == "message":
print(event.role, event.text)
elif event.type == "tool_call" and event.held:
print("Waiting for approval:", event.held["url"])In async code:
support = noclick.Agent.from_name("support")
async for event in support.watch.aio("ticket-1841"):
await websocket.send_json(event.raw)rehearseasync
agent.rehearse(message, *, scenario=None, user=...) -> OperationA test run: a real turn of this agent (its model, instructions and tools)
where a simulated world answers every tool call, so nothing is sent, no
account is touched and its connections needn't be connected yet (its model
connection must be). scenario says what that world is like; the world
keeps to it, and without one it's an ordinary busy day. The run is billed
like a turn, counts toward the agent's budget, and is no turn in any of its
conversations. An account has up to 10 runs queued or running at once
(noclick.QuotaExceeded past that).
NoneWho the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The run's operation (kind agent.rehearse); .wait() gives the Rehearsal: the reply, and
the tool_calls it made with what the world answered.
support = noclick.Agent.from_name("support")
run = support.rehearse(
"Order 1841 arrived broken. I want my money back.",
scenario="Order 1841 was delivered on Sep 2 and paid by card; the customer has asked once before.",
).wait()
print(run.reply)
for call in run.tool_calls:
print(call["tool"], call["arguments"])AgentGrant
class AgentGrantA connection the agent's user gave it at run time (it asked with its
request_connection tool, within can_request). status is
requested until the user connects, then active; operations None
means every operation.
| id | str | |
| connection | str | |
| integration | str | |
| operations | list[str] | None | |
| status | str | |
| reason | str | None | |
| conversation | str | None | |
| created_at | Any | |
| granted_at | Any | |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |
AgentReply
class AgentReplyThe agent's answer to one message.
| text | str | |
| conversation | str | None | |
| tool_calls | list[dict[str, Any]] | The turn's tool calls, each |
| operation_id | str | None | |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |
| model_usage | ModelUsage | None | The turn's model calls on your own key or plan ( |
Conversation
class ConversationOne thread with an agent; pass conversation back to continue it or to history().
| conversation | str | |
| user_id | str | None | |
| title | str | None | |
| preview | str | None | |
| turns | int | None | |
| cost_usd | float | None | |
| last_activity | Any | |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |
| status | str | None |
|
| handed_off_to | str | None | Who a |
| model_usage | ModelUsage | None | Its model calls on your own key or plan, all turns together ( |
Message
class MessageOne message of a conversation: its role is user, assistant,
person (someone it was handed off to) or note (a line added with
agent.note, with its data).
| role | str | |
| text | str | |
| created_at | Any | |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |
| files | list[Attachment] | The files it carried (a |
| tool_calls | list[dict[str, Any]] | A reply's tool calls: each |
| via | str | None | How a |
| data | dict[str, Any] | None | A |
| trigger | dict[str, Any] | None | What started the turn of a |
| reasoning | list[dict[str, Any]] | A reply's reasoning: each |
HarnessInfo
class HarnessInfoWhat can run an agent: one of Agent.harnesses(). sdk is NoClick's own
agent loop; the others are coding agents that run in a sandbox of their own, with
a shell and files, and think with the key or plan of a model_connection.
models() searches the models it offers.
| name | str | What |
| label | str | None | |
| version | str | None | The version NoClick runs ( |
| default_model | str | The |
| custom_models | bool | Whether |
| model_connection | str |
|
| integrations | list[str] | The model providers ( |
| streaming | str |
|
| shell | str |
|
| notes | list[str] | What to know before choosing it (a model that needs a particular key, say). |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |
modelsasync
harness_info.models(search=None, *, limit=50) -> list[HarnessModel]The models it offers, its default first: the first limit (at most 200), or
those whose id or label contains search (ignoring case). A search after a
few quiet minutes reads the providers' lists afresh, so it can take seconds.
NoneReturn at most this many.
50)Each model's id (what model= takes) and label. A harness whose
custom_models is True also runs ids it doesn't list.
Find a Claude model for an agent on OpenCode:
[opencode] = [h for h in noclick.Agent.harnesses() if h.name == "opencode"]
for model in opencode.models(search="claude", limit=10):
print(model.id, model.label)Check that a model is one Claude Code offers before creating the agent:
[claude_code] = [h for h in noclick.Agent.harnesses() if h.name == "claude_code"]
offered = [m.id for m in claude_code.models(search="opus")]
print("opus" in offered)HarnessModel
class HarnessModelA model a harness offers, by the id its model= takes.
| id | str | |
| label | str | None |
ModelUsage
class ModelUsageWhat an agent's model calls on your own key or plan (its model_connection) used:
tokens in (cached_tokens of them read from the provider's cache), tokens out, and what
they come to in US dollars at the provider's list price. NoClick doesn't bill these calls (the
key or plan pays); this is how you see them. A coding harness on a plan counts what it reports,
priced as if it ran on a key.
| input_tokens | int | |
| output_tokens | int | |
| cached_tokens | int | |
| usd | float | |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |
AgentMemory
class AgentMemoryagent.memory: named notes the agent keeps across conversations.
Every turn starts with each entry's name and description; the agent reads a
body when it needs one, and saves what it learns. Your code can seed,
read, correct and forget entries the same way.
An account agent keeps two layers: entries saved outside as_user are
the account's, read in every user's conversations (your refund policy),
and what it learns while talking to a user is that user's alone. A
user's own agent has one memory, the user's. The agent changes the
account's entries only in conversations your code has with it outside
as_user, and only reads its memory when it answers someone outside
your account (an inbox's sender, a caller, a visitor to its share link).
A long conversation also keeps its checkpoint here, in that conversation's
layer (MemoryEntry.conversation).
It's a noclick.Memory (its id is agent.memory_id) that goes with
the agent: another agent of its scope can read it too, with
memories=[agent.memory], and named memories (noclick.Memory) are
attached the same way.
support = noclick.Agent.from_name("support")
support.memory.save("refunds", "Refund within 30 days of purchase.", description="Our refund rules")
print([entry.name for entry in support.memory.list()])listasync
agent.memory.list(*, user=...) -> list[MemoryEntry]The entries of one layer: the account's outside as_user, else the user's.
Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
Each entry's name, description, version and when it last changed, by name; content is
None here, so read an entry for its body.
support = noclick.Agent.from_name("support")
for entry in support.memory.list():
print(entry.name, entry.description, entry.updated_by)searchasync
agent.memory.search(query, *, limit=None, user=...) -> list[MemoryEntry]Search what the agent remembers in a scope, the way the agent does: a
user's entries and the account's. An entry matches when its name,
description or content holds any of the query's words (a word also
finds the longer words it starts: refund finds refunds), or its
name or description holds the query; the more words, the better.
Return at most this many.
None)Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The best matches first (10 unless limit says otherwise, at most 100), each with its content
and user_id (None for the account's entries).
support = noclick.Agent.from_name("support")
for entry in support.memory.search("refund policy", user="user_42"):
print(entry.name, entry.content)readasync
agent.memory.read(name, *, user=...) -> MemoryEntryOne entry in full; a name with no entry raises noclick.NotFound.
Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The entry with its content and the version to pass back when you change it.
support = noclick.Agent.from_name("support")
refunds = support.memory.read("refunds")
print(refunds.version, refunds.content)saveasync
agent.memory.save(
name, content, *, description=None, version=None, user=...
) -> MemoryEntryWrite an entry, creating it if the name is new. description is the
line the agent sees every turn (required for a new entry, kept when you
leave it out). With version, the save only lands if the entry is
still at that version, else noclick.Conflict; without it, it replaces
whatever is there.
NoneNoneWho the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
The entry as saved, with its new version.
Seed what every conversation should know:
support = noclick.Agent.from_name("support")
support.memory.save(
"refunds", "Refund within 30 days of purchase, to the original card.",
description="Our refund rules",
)Correct an entry without losing an edit the agent made meanwhile:
support = noclick.Agent.from_name("support")
entry = support.memory.read("refunds")
try:
support.memory.save("refunds", entry.content + "\nNo refunds on gift cards.", version=entry.version)
except noclick.Conflict:
print("The agent changed it since; read it again.")deleteasync
agent.memory.delete(name, *, version=None, user=...) -> NoneForget an entry. With version, only if it is still at that version
(else noclick.Conflict). The agent can save the name again later.
NoneWho the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
support = noclick.Agent.from_name("support")
entry = support.memory.read("old-pricing")
support.memory.delete("old-pricing", version=entry.version)entriesasync
agent.memory.entries(*, user=...) -> list[MemoryEntry]list, as noclick.Memory calls it, so code reads any memory the same way.
Who the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
Each entry's name, description and version, by name, without its content.
for memory in (noclick.Agent.from_name("support").memory, noclick.Memory.from_name("company")):
print([entry.name for entry in memory.entries()])forgetasync
agent.memory.forget(name, *, version=None, user=...) -> Nonedelete, as noclick.Memory calls it, so code changes any memory the same way.
NoneWho the call acts for. Defaults to the user set by noclick.as_user (none: the account itself).
support = noclick.Agent.from_name("support")
support.memory.forget("old-pricing")MemoryEntry
class MemoryEntryOne named note in an agent's memory. version goes up with every
save; pass the one you read back to save or delete so you never
overwrite a change the agent made since. content is None in a
listing (read loads it). updated_by is agent, api or
console.
conversation is set on a conversation's checkpoint: a long
conversation keeps its older history folded into one entry, named
context. and a hash, which its turns read in place of that history.
Read it to see what the agent carries forward; an edit wins over the next
summary, and deleting it forgets that history for good. The agent never
sees it outside that conversation.
| name | str | |
| description | str | |
| content | str | None | |
| version | int | |
| updated_at | Any | |
| updated_by | str | None | |
| user_id | str | None | |
| conversation | str | None | |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |
AgentWorkspace
class AgentWorkspaceThe files an agent works on in one conversation: what it writes with its
shell or its tools, and what you put there for it. It lasts as long as the
conversation does, and every turn of that conversation sees the same files
under /workspace. Each user's thread with an account agent has its own.
(A noclick.Workspace is something else: a home for channels and their
members.)
| conversation | str | The conversation whose files these are. |
filesasync
workspace.files() -> list[WorkspaceFile]Every file in the workspace.
Each file's path (relative to /workspace), size, when it last changed and a download url.
An empty list for a conversation whose agent hasn't written anything.
support = noclick.Agent.from_name("support")
for file in support.workspace(conversation="ticket-1841").files():
print(file.path, file.size_bytes, file.url)readasync
workspace.read(path) -> bytesRead one file; a missing file raises noclick.NotFound.
The file's bytes, exactly as stored; decode text yourself.
analyst = noclick.Agent.from_name("analyst")
report = analyst.workspace(conversation="q3-review").read("report.csv")
print(report.decode()[:200])writeasync
workspace.write(path, data) -> WorkspaceFilePut a file in the workspace (bytes, or text as UTF-8), replacing one at that path, so the agent finds it on its next turn. At most 100 MB. A coding harness's sandbox stays up between turns and sees the file once it starts again (it stops a few minutes after its last turn), so write there before the conversation's first turn or between its sessions.
The file as stored, with its download url.
analyst = noclick.Agent.from_name("analyst")
workspace = analyst.workspace(conversation="q3-review")
workspace.write("sales.csv", "region,total\nnorth,1200\nsouth,950\n")
reply = analyst.message("Chart sales.csv by region.", conversation="q3-review")urlasync
workspace.url(path, *, expires_in_s=None) -> strA link that downloads one file without a key, for your user to click.
NoneThe link; it works for expires_in_s seconds (an hour by default, a day at most).
analyst = noclick.Agent.from_name("analyst")
link = analyst.workspace(conversation="q3-review").url("chart.png", expires_in_s=600)
print(link)WorkspaceFile
class WorkspaceFileA file in an agent's workspace for one conversation (AgentWorkspace).
url downloads it without a key for about an hour.
| path | str | |
| size_bytes | int | |
| modified_at | Any | |
| url | str | None | |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |
AgentEvent
class AgentEventOne moment of a streamed turn or a watched conversation. type is text
(text is the next piece of the reply), tool_call (tool, its wire
name like gmail__send_email_message, and its arguments; held when
policy holds it for a person), tool_result (tool, its result: the
start of what it returned, up to 500 characters, or its error; and whether it
is_error), file (a file the message carried, role user, or
the turn made, assistant), reasoning (text is one short line of
what the model is thinking, the title of a section of its reasoning, sent as
it completes), status (compacting while a long conversation's older
history is summarized, then compacted), approval (its "noclick" tools held a
purchase or a deletion for the account's owner: the approval), operation
(they started a long action: the operation, which comes again with its
progress each time the work reports some and once more when it ends) or
done (reply is the
whole AgentReply, as message returns it). agent.watch() also yields
message (a whole message: its role, text, files, via or trigger and, a reply, its
tool_calls and reasoning; a note, its data) and
status (the conversation's new status). operation_id names the turn
an event belongs to (None for a message no turn sent, like a person's
reply). history() reads messages in the same shape. The type call is
reserved for a phone call's moments.
| type | str | |
| text | str | None | |
| tool | str | None | |
| arguments | dict[str, Any] | None | |
| result | Any | |
| is_error | bool | |
| reply | AgentReply | None | |
| role | str | None | A |
| held | dict[str, Any] | None | A held |
| status | str | None | A |
| operation_id | str | None | |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |
| file | Attachment | None | A |
| files | list[Attachment] | A |
| tool_calls | list[dict[str, Any]] | A |
| approval | dict[str, Any] | None | An |
| operation | dict[str, Any] | None | An |
| via | str | None | A |
| data | dict[str, Any] | None | A |
| trigger | dict[str, Any] | None | A |
| reasoning | list[dict[str, Any]] | A |
Rehearsal
class RehearsalA test run: a real turn of the agent (its model, instructions and tools)
where a simulated world answers every tool call, so nothing is sent and no
account is touched. tool_calls are the calls it made, each with the
tool (its tool name, like gmail__search_emails), its arguments
and the result the world answered (None if the world couldn't);
transcript is the turn step by step: role assistant steps (its
text between calls, then the reply) and tool steps. On a coding
harness, its own shell and file steps are tool steps with source
harness: they ran for real, in the run's own sandbox. cost_usd is
what the run cost you.
| reply | str | |
| tool_calls | list[dict[str, Any]] | |
| transcript | list[dict[str, Any]] | |
| scenario | str | None | |
| cost_usd | float | None | |
| operation_id | str | None | |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |
Wakeup
class WakeupA turn an agent will take later, in one of its conversations: at a time, when an
operation settles, on a NoClick event, or at each time of a schedule (agent.wake,
or its own wake_me tool). At its moment the conversation takes a turn whose
message is the note, followed by what woke it.
A turn already running in that conversation goes first: the wake-up waits behind
it, one turn at a time. It survives restarts, starts its turn once, and is billed
and capped like any turn. A recurring one (every) stays pending after each
turn, due at its schedule's next time; times it missed fold into one turn. NoClick
sets its own too: for a child's update, the link the agent asked its user to connect
an account with being done, and a decided approval of a held call. Your webhooks
hear each one delivered or failed (agent.wakeup.delivered, agent.wakeup.failed),
and each time a recurring one missed because your account couldn't pay
(agent.wakeup.missed).
- Like every live object, it also has hydrate(), refresh() and raw.
| id | str | |
| agent_id | str | |
| user_id | str | None | Whose conversation it wakes ( |
| conversation | str | The conversation that takes the turn. |
| note | str | What the agent is told when it wakes. |
| at | Any | When it's due (a time wake-up), or when what it waited for happened. |
| when | dict[str, Any] | None | What it waits for besides a time: |
| every | Cron | None | Its schedule, for a recurring wake-up ( |
| fired | int | How many turns it has started (a recurring one counts up). |
| source | str | What set it: |
| status | str |
|
| woken_by | dict[str, Any] | None | What woke it: |
| operation_id | str | None | The turn it started (an |
| depth | int | How many |
| error | dict[str, Any] | None | Why it failed: |
| missed | dict[str, Any] | None | A recurring one's last missed time: |
| created_at | Any | |
| delivered_at | Any | When its turn started ( |
| operation | Operation | None | The turn it started, as an |
cancelasync
wakeup.cancel() -> WakeupCall it off before it's due. A wake-up that already started its turn, or was
canceled or failed, raises noclick.Conflict.
This wake-up, canceled.
digest = noclick.Agent.from_name("digest")
for wakeup in digest.wakeups("weekly-report", status="pending"):
wakeup.cancel()updateasync
wakeup.update(*, note=None, at=None, after=None, every=None) -> WakeupChange a pending wake-up: its note, and its time with one of at,
after and every (a Cron; a one-off time makes a recurring one come
once more). A delivery under way of what it said before starts no turn. One
waiting on an operation or an event keeps that; one already delivered,
canceled or failed raises noclick.Conflict.
NoneNoneNoneNoneThis wake-up, as it is now.
digest = noclick.Agent.from_name("digest")
[weekly] = digest.wakeups("weekly-report", status="pending")
weekly.update(every=noclick.Cron("0 8 * * 1", tz="Europe/London"))Job
class Job(Operation)Work one agent gave another: a spawned child's task (kind agent.task), or a
message a coordinator sent with its agents tools (kind agent.message), whose
only update is done. It's an Operation: wait() returns the AgentReply.
A task runs while the child works on it, across as many turns as it takes, and
finishes when its conversation goes quiet with nothing left to continue it: no turn
running, no wake-up pending, nothing waiting on a person. While it runs, step is
working (a turn is running) or waiting (only a wake-up is pending); it's
awaiting_human (step blocked) while it waits on a person: a held call, or a
connection it asked its user for. It succeeded once the child stopped, its last reply the result (a child
that needs a decision says so and stops; the parent's message to it starts a new
job in the same conversation), failed when a turn failed, canceled when
canceled (its running turn stops and its pending wake-ups are called off), and
expired when the child's ttl_s ran out first.
- Inherits from Operation.
| started_by | str | None | The agent whose job it is: a task's parent, whether your code spawned it or the parent did. |
| conversation | str | None | The conversation the worker does it in. |
| parent_conversation | str | None | The parent's conversation its updates wake ( |
| required_updates | list[Any] | The updates its parent required, as on the wire: |
| latest_update | JobUpdate | None | Its newest update ( |
| agent | Agent | The agent doing the work, lazy. |
listclassmethodasync
noclick.Job.list(*, limit=None, **filters) -> list[Job]Spawned children's tasks across your agents (kind agent.task), newest first.
Takes Operation.list's filters except kind (agent= keeps the ones one
agent does). agent.jobs() reads the ones one agent started, its message jobs
(kind agent.message) too.
Return at most this many.
None)Every matching job (or the first limit), each with its status, step and latest_update.
for job in noclick.Job.list(status="awaiting_human", user_filter=noclick.ALL_USERS):
print(job.agent_id, job.started_by, job.latest_update)list_pageclassmethodasync
noclick.Job.list_page(*, cursor=None, limit=None, **filters) -> PageOne page of jobs; pass next_cursor back as cursor.
The next_cursor of the page before; omit it for the first page.
None)Return at most this many.
None)A page whose data holds the jobs and whose next_cursor is None on the last page.
page = noclick.Job.list_page(status="failed", limit=50)
for job in page.data:
print(job.agent_id, job.error)summaryclassmethodasync
noclick.Job.summary(**filters) -> OperationSummaryHow many jobs match, by status; takes Operation.summary's filters except kind.
The number that match in total, and per operation status in by_status.
counts = noclick.Job.summary(status="live")
print(counts.total, counts.by_status["awaiting_human"])updatesasync
job.updates() -> list[JobUpdate]Every update it has sent its parent, oldest first.
Each JobUpdate with its kind, the child's text and, by kind, the call it was about to
make, what it waits on, or the result.
planner = noclick.Agent.from_name("planner")
for job in planner.jobs(kind="task"):
for update in job.updates():
print(update.created_at, update.kind, update.text)JobUpdate
class JobUpdateOne update a job sent its parent. kind is done (the job finished, any
outcome: result or error), blocked (it started waiting on a person:
waiting_on), before (it's about to make the call in tool with
arguments), every (it was due one) or message (the child's own
tell_parent). text is what the child wrote (for blocked, NoClick's line
saying what it waits on); status is the job's status then, and wakeup_id
the parent's wake-up it set (None when nothing was there to wake).
| id | str | |
| job_id | str | |
| agent_id | str | |
| kind | str | |
| text | str | None | |
| tool | str | None | |
| arguments | dict[str, Any] | None | |
| status | str | None | |
| result | Any | |
| error | dict[str, Any] | None | |
| waiting_on | dict[str, Any] | None | A |
| wakeup_id | str | None | |
| created_at | Any | |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |
Before
noclick.Before(*tools)An update the parent requires before its child makes a tool call: Before with
tool patterns written as policy writes them ("gmail.send_email_message",
"stripe.*", "code"). When the child calls a matching tool, NoClick sends the
parent an update holding the call and what the child wrote before it (its reason),
then the call runs: the parent hears first but doesn't hold it (a call that must
wait for a yes is a policy require_approval rule). A coding harness's own
shell commands and file edits aren't tool calls NoClick sees, so Before can't
match them, and its words reach NoClick only when its turn ends, so the update
carries the call without a reason.
support = noclick.Agent.from_name("support")
refunder = support.spawn(
"Refund order 1841 if it's within our 30 days, then email the customer.",
tools=["stripe.create_refund", "gmail.send_email_message"],
updates=[noclick.Before("stripe.create_refund")],
conversation="ticket-1841",
)| tools | list[str] | The tool patterns it covers. |
| tools |
Every
noclick.Every(interval)An update the parent requires at least this often while its child works:
Every with an interval in seconds (or a timedelta), 60 seconds or more.
When that long has passed since the child's last update, NoClick asks it for one
(a coding harness by steering its running turn, an sdk agent before its next
model call), and the tell_parent it answers with is the every update. A
child that's only waiting (for a wake-up, for a person) isn't asked.
planner = noclick.Agent.from_name("planner")
migrator = planner.spawn(
"Move every customer from the old CRM to the new one, in batches of 200.",
tools=["hubspot", "salesforce"],
updates=[noclick.Every(1800)],
conversation="crm-move",
)| interval_s | int | How often, at most, the parent hears while the child works. |
| interval_s |
Agents
noclick.Agents()The agents capability, for an agent's tools: the same as the string
"agents", narrowed with Agents.only(...). It gives a coordinator six tools,
within its own scope, in its own conversations (in a channel, agents work by
mentions instead): spawn (a child of its own, whose updates wake it in the
conversation it spawned from), message (any agent it can reach: a user's agent
reaches that user's and the account's, an account agent the account's; it waits up
to 50 seconds for the reply, and a reply that takes longer, or one it doesn't wait
for, is a job whose done update wakes it), ask (as agent.ask),
status (an agent's or a job's), list (as Agent.list filters) and
cancel (a job it started). Its work runs in the threads of the user it's acting
for, and its children get the capability only when it lists it for them. Policy
names them agents.spawn ... agents.cancel (agents.*); the model sees
agents__spawn, agents__message and so on, and its tools count toward an
agent's 64.
coordinator = noclick.Agent.from_name(
"coordinator", create_if_missing=True,
instructions="You run our launches. Spawn a researcher for each competitor, then write the plan.",
tools=["search", noclick.Agents()],
)| tools | list[str] | The capability's tools this gives. |
| tools |
onlyclassmethod
noclick.Agents.only(*tools) -> AgentsSome of the capability's tools: spawn, message, ask, status,
list and cancel.
The capability narrowed to those tools, for tools= (on the wire, "agents.spawn", ...).
A supervisor that watches and asks but never starts work:
supervisor = noclick.Agent.from_name(
"supervisor", create_if_missing=True,
instructions="You check on our agents every morning and report anything stuck.",
tools=[noclick.Agents.only("list", "status", "ask")],
)Platform
noclick.Platform()NoClick itself, for an agent's tools: the same as the string "noclick",
narrowed with Platform.only(...). It gives the agent what a connected app
gets on NoClick's MCP server: search and fetch (NoClick's guides and your
account's objects), api_describe and api_call (any API operation), and
quote, buy and delete. They act as the turn's key and the user it acts
for, never more than that key may, judged by the same gate as any connected app:
a purchase or a deletion waits for the account's owner, who approves it on NoClick
with its exact price or what goes with it (the turn waits on them, and the call's
outcome is the conversation's next turn); a secret's value never comes from the
conversation; and a turn answering someone outside the agent's scope (an email, a
caller) asks a person before acting outside the account. A long action it starts
is an operation, which wake_me waits on. Policy names them noclick.search
... noclick.delete; the model sees noclick__search and so on, and they
count toward an agent's 64 tools. Watchers see an approval and an
operation event for them (agent.stream, agent.watch).
ops = noclick.Agent.from_name(
"ops", create_if_missing=True, tools=[noclick.Platform()],
instructions="You run our NoClick account: numbers, domains and agents.",
)
ops.message("Get us a phone number in the 415 area code.") # buys once you approve| names | list[str] | What it's narrowed to: capability groups and API areas (empty: all of NoClick). |
| names |
onlyclassmethod
noclick.Platform.only(*names) -> PlatformPart of NoClick: capability groups (read, build, deploy, spend,
delete) bound what kind of call it makes, and API areas (a key's scopes, like
domains, phone or agents) which part of the API it reaches; each
narrows only its own kind. Without spend it isn't offered quote and
buy, without delete not delete. Names are checked when the agent is saved.
NoClick narrowed to those names, for tools= (on the wire, "noclick.read", ...).
An agent that keeps your domains, and buys new ones once you approve:
registrar = noclick.Agent.from_name(
"registrar", create_if_missing=True,
instructions="You look after our domains and their DNS.",
tools=[noclick.Platform.only("domains")],
)One that only reads, to answer questions about the account:
analyst = noclick.Agent.from_name(
"analyst", create_if_missing=True, instructions="You explain our usage and spend.",
tools=[noclick.Platform.only("read")],
)Attachment
class AttachmentA file on a message, kept as an account file at url. kind is
image, audio, video or file; a voice clip has its
duration_ms and its waveform's peaks (loudness from 0 to 1). Audio
is kept as transcription reads it (a browser's WebM recording as MP3). A
file an agent's conversation shows (in history, or a file event)
has its name, url and mime_type, and no id: it was sent. The
account's files (noclick.attachments()) have their id, to send again.
| name | str | |
| url | str | |
| mime_type | str | |
| id | str | None | |
| size_bytes | int | None | |
| kind | str | |
| voice | bool | |
| duration_ms | int | None | |
| peaks | list[float] | None | |
| raw | dict[str, Any] | The response JSON exactly as the API sent it. |
attachments
noclick.attachments(*, query=None, limit=30, client=None) -> list[Attachment]The account's files, newest first: what its messages carried, the images
and videos it made, its calls' recordings. A message to an agent sends any of
them again in its files. The account's alone: read as a user, it's refused.
Args: query: Keeps the files whose names contain it (any case). limit: How many, 1 to 100.
NoneReturn at most this many.
30)The client to send with; defaults to the module client built from NOCLICK_API_KEY.
None)Each file as an Attachment with its id, name, url, mime_type and kind.
[report] = noclick.attachments(query="q3")
analyst.message("What changed since this?", files=[report])