A skills-driven, multi-provider LLM agent client you can use as a template.
Give it a task (optionally with files), and it will:
- Retrieve relevant skills via vector search and inject them into the prompt.
- Read attached files (pdf / xlsx / docx / pptx / jpg / png / zip / tar / tar.gz, plus any plain-text or source file) into context.
- Route across LLM providers (OpenAI / Anthropic / Google / Ollama) with difficulty-tiered, token-aware selection and automatic failover.
- Run an iterative
done/continueloop until the task is complete, compressing context memory when it grows too large and splitting prompts that exceed the largest model window. - Optionally execute Python it writes (opt-in) to compute real results.
- Persist each session to
history/as JSON.
- Architecture
- Project structure
- Installation
- Configuration
- Research-grounded skill design
- Usage
- How it works
- Testing
- Changelog
- Notes & limitations
- References
Four modules, orchestrated by AgentClient:
| Module | Class | Responsibility |
|---|---|---|
disesfgewuAgent/agent.py |
AgentClient |
Orchestration: prompt building, token accounting, splitting, the iteration loop, context compression, history. |
disesfgewuAgent/llmRouter.py |
llmRouter |
Multi-provider routing across the openai / anthropic / google / ollama protocols, with token-aware selection and failover. |
disesfgewuAgent/skillLoader.py |
skillLoader |
Skill RAG: parse skills/*.md, embed descriptions, and run FAISS similarity search. |
disesfgewuAgent/inputFileManager.py |
inputFileManager |
Extract text/metadata from txt / md / pdf / xlsx / docx / pptx / jpg / png / zip / tar / tar.gz. |
agent-client-template/
├── disesfgewuAgent/
│ ├── agent.py # AgentClient orchestrator
│ ├── llmRouter.py # multi-provider router
│ ├── skillLoader.py # skill embedding + FAISS search
│ └── inputFileManager.py # file -> text/metadata/archive extraction
├── config/
│ ├── api.example.json # template: copy to api.local.json
│ ├── api.local.json # YOUR models + keys (gitignored)
│ ├── skills.example.json # template: copy to skills.json
│ └── skills.json # skill index + cached embeddings (gitignored)
├── skills/
│ ├── code-review.md # a skill = markdown + YAML frontmatter
│ ├── code-editing.md # lets the agent modify files (with code exec)
│ ├── shell-commands.md # search/navigate a codebase via grep/find/...
│ ├── debugging.md
│ └── testing.md
├── history/ # per-session JSON logs (gitignored)
├── tests/ # unit + live integration tests
├── example.py # runnable end-to-end demo (scripted)
├── demo.py # interactive agent TUI (rich): shows each step
├── pyproject.toml # packaging metadata (pip install .)
├── .env.example # template: copy to .env
└── requirements.txt
Straight from GitHub (no PyPI account needed — this is the recommended way to install it as a dependency):
pip install git+https://github.com/Disesfgewu/agent-client-template.git
# a specific release:
pip install git+https://github.com/Disesfgewu/agent-client-template.git@v1.5.0Or, from a local checkout (exposes the disesfgewuAgent import):
pip install .
# or, for development:
pip install -e .Optional extras: pip install ".[demo]" (the rich CLI TUI) and
pip install ".[web]" (the ddgs search library used by the web-research
skill).
Or just install the runtime dependencies and run from a checkout:
pip install -r requirements.txtRequires Python 3.9+. Configuration (models, keys, skills) is never bundled
into the package — you inject it at runtime (see below), so the same install
works from a checkout or from site-packages. The default skills are bundled
in the package under disesfgewuAgent/default_skills/. When you construct an
agent without skill paths, it uses that installed skills directory and creates only
a private local registry/cache at ./.agent/skills.json.
There are three things to configure: models, an embedding provider, and
optionally your project-local skills. Bundled default skills are available without any project-local skills/ directory. The config/*.local/*.json, .agent/, and .env files are gitignored,
so your keys never get committed.
cp config/api.example.json config/api.local.json
cp config/skills.example.json config/skills.json
cp .env.example .envconfig/api.local.json is a JSON array of model entries. Each entry:
| Field | Meaning |
|---|---|
provider |
Free-form label for your own reference. |
protocol |
One of openai, anthropic, google, ollama. Determines the request/response shape. Defaults to openai. |
endpointUrl |
Full chat/messages endpoint URL (see per-protocol notes below). |
modelName |
Model identifier sent to the provider; also the lookup key in this client. |
maxInputToken |
Input window. Used for token-aware routing — prompts larger than this skip the model. |
maxOutputToken |
Max output tokens (sent as max_tokens / num_predict / maxOutputTokens). |
tier |
Optional capability tier (integer, higher = more capable; default 2). Drives difficulty-based routing — see below. |
apiKey |
Provider key. Leave "" for local Ollama. |
Routing strategy. AgentClient's routingStrategy (default "taskComplex")
decides the order in which models are tried; connect() then skips any whose
window can't hold the input and uses the first that succeeds:
"taskComplex"(default) — difficulty tiering: easy tasks try the lowest-tier(cheapest) model first to save cost; hard tasks try the highest-tier(most capable) first. Difficulty is a free, composite heuristic scored over the raw task — its token length, size of attached files (reviewing/redesigning a big file or project escalates), number of attached files, matched skills, and reasoning keywords (EN/CN) — so injected skill text doesn't skew it. Ties broken by window size. No extra API call.
For the multi-step action loop, the router requests JSON output mode from
providers that support it (OpenAI response_format, Google responseMimeType,
Ollama format), so even smaller models return valid, properly-escaped JSON and
the agentic protocol doesn't break on quotes/newlines inside generated code.
"maxTokens"— always prefer the largest context window.""— use config order as-is (the list is the priority).
So with deepseek-v4-flash at tier 1 and deepseek-v4-pro at tier 3, a
quick question routes to Flash while a large/complex one routes to Pro.
Per-protocol endpointUrl conventions:
- openai —
.../v1/chat/completions - anthropic —
.../v1/messages - google — base URL only (e.g.
https://generativelanguage.googleapis.com); the client appends/v1beta/models/<modelName>:generateContent. - ollama —
http://localhost:11434/api/chat
Add as many models as you like. The router tries them in order and fails over to
the next when one errors or cannot fit the input. See
config/api.example.json for a full four-protocol example.
Skill search needs an embedding model, configured in .env. It uses an
OpenAI-compatible embeddings API (the default points at NVIDIA NIM):
EMBEDDING_URL=https://integrate.api.nvidia.com/v1
EMBEDDING_API=your-embedding-key
EMBEDDING_MODEL=nvidia/nv-embed-v1Any OpenAI-compatible embeddings endpoint works — just point EMBEDDING_URL,
EMBEDDING_API, and EMBEDDING_MODEL at it.
Embedding is only needed when the agent has skills. An agent configured with an empty registry (no skills) never embeds anything:
load()andsearch()short-circuit to "no matches", soEMBEDDING_APIcan be left unset. Skill search is best-effort throughout — if the embedding endpoint is unreachable, oversized, or errors out, the agent degrades to running on the base prompt instead of crashing (see the Changelog).
A skill is a Markdown file in skills/ with YAML frontmatter. Its
description is what gets embedded and matched against incoming tasks; its body
is injected into the prompt when selected.
---
name: code-review
description: Reviews code for bugs, security issues, and style violations. Use when reviewing pull requests or auditing code.
version: 1.0.0
tags: [development, review]
---
# Code Review Best Practices
...body that will be injected into the prompt when this skill matches...To add a new skill:
- Create
skills/my-skill.mdwith frontmatter (include a gooddescription). - Register it in
config/skills.json:{ "my-skill": { "relativePath": "my-skill.md" } } - On the next run, its embedding is computed once and cached back into
config/skills.json.
disesfgewuAgent supports synchronizing skills directly from a remote GitHub repository such as Disesfgewu/skill-library.
Option A: Pass skillSource during initialization
from disesfgewuAgent import AgentClient
client = AgentClient(
apiConfig=api_config,
skillSource="Disesfgewu/skill-library", # Syncs skills from remote GitHub repo
)Option B: Explicitly trigger syncDefaultSkills() or sync_skills()
from disesfgewuAgent import AgentClient, sync_skills
# Standalone sync helper
config_path, skill_folder = sync_skills(source="Disesfgewu/skill-library")
# Or via AgentClient instance method
client.syncDefaultSkills(source="Disesfgewu/skill-library")Offline Fallback Guarantee: If the network is unavailable, GitHub rates limits, or you are running offline, skill synchronization gracefully falls back to local bundled default skills (
bootstrap_default_skills()) without interrupting agent initialization.config/skills.json(so subsequent runs are fast).
The skills.json index is the source of truth for which skills exist; the
embedding field is filled in automatically.
Bundled default skills. The package includes disesfgewuAgent/default_skills/*.md. If AgentClient is created without skillConfigPath and skillFolderPath, it calls bootstrap_default_skills(), keeps the bundled skill Markdown files in the installed package location, and creates a private local registry at ./.agent/skills.json. That registry is where embeddings are cached, so runtime config does not need to be committed or exposed. config/skills.example.json remains a shareable template; config/skills.json and .agent/ are local-only. To use your own project skills, pass both skillConfigPath and skillFolderPath.
agent = AgentClient(apiConfig=models) # uses package default skills; creates ./.agent/skills.jsonCode-editing skill. The bundled code-editing skill turns the agent into a
Codex-style code editor: when a task asks to modify a file (and
enableCodeExecution=True), it reads the file, applies a minimal targeted patch,
writes it back to disk, and verifies — all via execute steps. Attached files
now carry their path in the [FILES] section so the agent knows where to write.
The bundled skill set follows a lightweight Metadata + Instructions + Resources model. In this repository, the metadata that matters most for retrieval is the YAML description, because skillLoader embeds that field and uses FAISS similarity search before the Markdown body is injected.
The practical design target is CP-grounding: maximize useful task coverage and grounded answers per injected token. A good skill should be easy to retrieve, specific enough to avoid overlap, short enough to fit prompt budgets, explicit about evidence, and conservative about state-changing tools.
| Design concern | Local rule |
|---|---|
| Demand fit | Prioritize high-demand workflows such as web research, code generation/review, data analysis, document/file work, and debugging. |
| Retrieval fit | Put realistic trigger terms in description; body-only trigger wording is too late for selection. |
| Grounding fit | Require evidence collection, citations, local file inspection, calculation checks, or validation steps where the task depends on facts. |
| Token cost | Keep operational instructions concise; move long references, schemas, templates, or scripts into resources when needed. |
| Redundancy cost | Split skills only when they serve distinct intents; merge near-duplicates that compete for the same query. |
| Risk cost | Classify operations as L0-L3 and require approval/dry-run for writes, execution, external export, deployment, database mutation, or financial actions. |
The skill-creator skill is the default entry point for creating or revising skills in this repo: it defines frontmatter, body structure, resource boundaries, registry updates, and validation steps. The skills-optimize skill then encodes the CP-grounding audit model and exposes an API-shaped operation contract for future tooling: audit, register, lint, retrieve_test, suggest_rewrite, apply_rewrite, and score_cp_grounding.
Two security skills add a stricter control layer: agent-skill-security-audit classifies skills into the 6-category / 20-subcategory taxonomy and assigns L0-L3 risk under worst-case interpretation; agent-action-safety-control gates runtime actions with an action manifest, deny-by-default L3 rules, scoped confirmation, dry-run preference, rollback planning, and audit logging. This design is intentionally stricter than per-layer or lexical allowlists because OpenClaw security studies show cross-layer composition, malicious skill distribution, prompt injection, and command identity ambiguity can bypass local-only checks.
For an interactive agent TUI (a rich-rendered terminal UI that shows the agent
reasoning, running code, and observing output step by step — not just a chat
bot), run:
python demo.pyIt streams each step of the agent loop, executes Python the agent writes, keeps
conversation memory, and supports /file <path> (inline too), /reset, and
/exit. See example.py for a scripted end-to-end example. The
programmatic essentials:
import asyncio
from disesfgewuAgent import AgentClient
async def main():
# All config is injected explicitly — nothing is read from the install dir.
async with AgentClient(
"config/skills.json", # skill index (omit to auto-bootstrap bundled default skills)
"skills", # skills directory
"config/api.local.json", # model config: a path OR a list of dicts
contextWindowSize=32000,
historyDir="history", # optional; omit to disable session logging
) as agent:
# Simple task
print(await agent.ask("What is 2 + 2? Reply with just the number."))
# Reusable: per-conversation state resets on every ask()
print(await agent.ask("List three code-review best practices."))
# Task over local files
print(await agent.ask(
"Summarize this document.",
inputFiles=["report.pdf"],
))
asyncio.run(main())AgentClient(skillConfigPath, skillFolderPath, apiConfig, contextWindowSize=32000, historyDir=None, routingStrategy="taskComplex", complexityScoreThreshold=3, maxTurnsInContext=6, enableCodeExecution=False, codeExecutionTimeout=30)
skillConfigPath— path toconfig/skills.json.skillFolderPath— path to theskills/directory.apiConfig— your model config: a path to a JSON file or an in-memorylistof model dicts (same shape asconfig/api.example.json).contextWindowSize— soft token threshold at which context memory is compressed.historyDir— directory for per-session JSON logs;None(default) disables disk writes, which is usually what you want when used as a library.routingStrategy— model-selection strategy:"taskComplex"(default, difficulty tiering),"maxTokens", or""(config order). See Configuration.complexityScoreThreshold— score at/above which a task is treated as complex (default3). Raise it to keep more tasks on the cheaper models.maxTurnsInContext— how many past turnschat()feeds back as context.mode— role/profile label (default"general"); tags the result and frames the prompt. Useful as an agent's role in a crew.enableCodeExecution— allow the agent to run Python it writes (see below). Off by default.enableShell— allow the agent to run shell commands (grep/find/...). Off by default.enableFileEdit— allow the agent to applyedit_filepatches (returns a diff in the result). Off by default.codeExecutionTimeout— per-run timeout (seconds) for executed code/commands.onEvent(event)— observe each step of the loop (planning / reasoning / execute / output) for a live UI.onApprove(action) -> bool— safety gate called before any execution; return falsy to block it.
Whatever the order, the router always skips models whose context window is too small for the input and uses the first one that both ranks well and fits — so a long task never gets sent to a model that can't hold it.
Passing config in-memory (no files needed):
models = [{
"provider": "openai", "protocol": "openai",
"endpointUrl": "https://api.openai.com/v1/chat/completions",
"modelName": "gpt-4o-mini", "maxInputToken": 128000,
"maxOutputToken": 16384, "apiKey": "sk-...",
}]
agent = AgentClient("config/skills.json", "skills", models)Key methods:
await agent.ask(inputStr=None, inputFiles=None, systemPrompt="", userPrompt=None) -> dict— run one independent task.ask("task")remains supported; API integrations should prefer explicitsystemPromptanduserPrompt.await agent.chat(message=None, inputFiles=None, systemPrompt="", userPrompt=None) -> dict— likeask()but remembers prior turns;agent.resetConversation()clears it.await agent.aclose()— close the underlying HTTP client (or useasync with).
systemPrompt is trusted caller policy for the current run. userPrompt is untrusted user content and is scanned before prompt construction for prompt-injection indicators such as instruction override, system prompt extraction, secret exfiltration, unsafe execution, destructive actions, and data/instruction boundary confusion. If flagged, the prompt includes a [USER PROMPT SECURITY CHECK] section based on the prompt-injection-guard safety skill and instructs the model to treat conflicting content as data.
Both return a fixed-schema result dict (JSON-serialisable). A stable core is always present; capability-specific fields appear only when that capability is enabled, so a generic agent never carries coding fields:
{
"mode": "general", # the agent's role/profile label (see `mode` arg)
"status": "done", # "done" | "max_iterations"
"answer": "...", # the final text
"reasoning": "...",
"steps": [ ... ], # every step the agent took (reasoning / execute / edit / ...)
"error": "",
"iterations": 2,
# only when enableCodeExecution/enableShell:
"commands": [ {"language","code","stdout","stderr","exit_code"} ],
# only when enableFileEdit:
"files_changed": ["path"],
"diffs": [ {"path", "diff"} ], # unified diff of each edit
}Use mode (default "general"; e.g. "coding", "sql", "research") to label
the agent's role — handy when composing several agents into a crew. It frames the
result and prompt; the coding fields above are gated by capabilities, not by mode.
Lifecycle:
ask()does not close the router, so the same client can be reused across independent tasks. Manage the connection withasync withor by callingaclose()when done.
The package provides the environment; skills teach which commands to use.
The response protocol gains an execute action —
{"status": "execute", "language": "python|shell", "code": "..."} — and the agent
runs it, feeds stdout/stderr back, then returns a done answer with the output
and an explanation. This is how the agent actually computes / searches / edits
instead of guessing.
enableCodeExecution=True— run Python the agent writes (subprocess +codeExecutionTimeout).enableShell=True— run shell commands (grep,find,ls,cat,sed, ...), viabashwhen available so POSIX syntax works cross-platform (Git Bash on Windows).- Bundled skills drive these:
shell-commands(search/navigate a codebase) andcode-editing(Codex-style read → patch → write → verify).
Safety gate. Pass onApprove(action) -> bool; the agent calls it before
running anything and skips the action if it returns falsy. The embedding app owns
the policy — an interactive prompt (see demo.py: y / N / always), an
allow-list, or auto-approve. onEvent(event) streams each step (planning,
reasoning, execute, output) so a UI can render the loop live.
⚠️ Security: execution runs LLM-generated code/commands on your machine. Both flags are off by default; there is no sandbox beyond subprocess + timeout, so combine them withonApprove(or an allow-list) for untrusted use. As an embeddable package (not a standalone product), the host application decides what is permitted.
A single ask() runs this pipeline:
- Reset state — clears per-conversation memory/history so the client is reusable.
- Separate prompt channels — keep trusted
systemPromptseparate from untrusteduserPrompt; scanuserPromptfor prompt-injection and unsafe-action indicators before building the final prompt. - Gather inputs — load the skill index, extract any attached files, and run
vector search to pick the top matching skills (cosine similarity, default
min_score=0.3,top_k=3). - Iteration loop (up to 2000 by default, configurable with
maxIterations):- Build the prompt from
[SKILLS] / [CONTEXT MEMORY] / [INFORMATIONS FROM LAST] / [FILES] / [TASK]. - If context memory exceeds
contextWindowSize, compress it (summarize, chunking the summary itself if needed; keep the original if it didn't shrink). - If the prompt exceeds the largest model window, split the task into overlapping chunks, process them in parallel, and merge.
- Route to a model via
llmRouter.connect()(token-aware, with failover). - Parse the response as JSON (tolerant of
```jsonfences and prose).status: "done"returns the answer;status: "continue"feeds progress into the next iteration.
- Build the prompt from
- Persist the session to
history/session_<timestamp>.json.
The agent expects models to answer in this JSON envelope:
{"status": "done", "answer": "final answer", "reasoning": "..."}
{"status": "continue", "answer": "current progress", "reasoning": "...", "next_action": "..."}Tests are split into offline unit tests (no network, always run) and live integration tests (real API calls, skipped unless configured).
# Everything (live tests auto-skip if config/api.local.json or EMBEDDING_API is missing)
python -m unittest discover -s tests -v
# Offline unit tests only — fast, deterministic, no keys needed
python -m unittest \
tests.test_agent.TestAgentLogic \
tests.test_llmRouter.TestLLMRouterLogic \
tests.test_llmRouter.TestLLMRouterTokenGuard \
tests.test_skillLoader.TestSkillLoaderLogic \
tests.test_inputFileManager -v
# Live end-to-end integration tests (needs a working api.local.json + EMBEDDING_API)
python -m unittest tests.test_integration -vThe live suites (tests.test_integration, and the *Real / *AsyncAPI classes)
are guarded by @unittest.skipUnless(live_api_available(), ...), so a fresh
clone without configuration still gets a green run. They are not mocked — they
exercise the full pipeline against your configured endpoints and consume real API
quota, so they require the endpoints to be reachable and within rate limits.
Versioning follows SemVer. Install a specific release with
pip install git+https://github.com/Disesfgewu/agent-client-template.git@vX.Y.Z.
- Multi-Domain Knowledge Skills System: Full support for a 3-tier Knowledge Skill Architecture (
Global Router,Domain Router,Topic Skills,Shared Guidelines). - DAG Dependency Resolution (
requires): Topic skills (e.g.forwarding) automatically resolve required prerequisite skills (e.g.datapath,pipeline_hazard) in topological order with circular dependency protection. - Shared System Guidelines Auto-Injection: System-level guidelines with
global: trueor located inshared/(teaching methodology, step-by-step reasoning rules, ASCII diagram formatting, citation rules) are automatically prepended to prompts. - Recursive Skill Discovery (
discover_skills_in_dir): Recursively discovers nested Markdown skills across multi-level domain directories (knowledge/shared/,knowledge/domains/...). - GitHub Remote Skill Library Sync (
Disesfgewu/skill-library): IntegratedskillSyncmodule.AgentClientdefaults to syncing skills fromDisesfgewu/skill-librarywith seamless offline fallback (bootstrap_default_skills()).
- Skill search is now fully optional. An agent with no skills (an empty
skills.json) no longer crashes and no longer requiresEMBEDDING_APIto be set. Previouslysearch()raisedValueError("Skills not loaded")for a loaded-but-empty registry, which took down the wholeask()call. It now distinguishes "never loaded" (still a programming error → raises) from "loaded, but no skills to search" (returns no matches). No embedding call is made when there is nothing to embed. - Added offline regression tests for the no-skills and embedding-failure paths.
- Fixed skill search crashing the whole agent request on long queries.
Skill search embeds the full query via the embedding endpoint; two problems
made it crash entire
ask()calls:_embed()senttruncate: "NONE", so any query longer than the embedding model's input limit (nv-embed caps at a few hundred tokens) was rejected with HTTP 500. A long prompt — e.g. one carrying thousands of characters of source/paper text — took down the whole request. Now lets the server truncate instead (truncate: "END").search()called_embed()unguarded, so any embedding failure (size, 5xx, network) propagated out and crashed the caller. Skill search is best-effort, so it now degrades to "no skills" on any embedding error.- Short queries were unaffected, which is why this only showed up on large inputs.
These papers inform the default skill design, retrieval metadata, CP-grounding objective, and safety gates:
- George Ling, Shanshan Zhong, and Richard Huang. 2026. Agent Skills: A Data-Driven Analysis of Claude Skills for Extending Large Language Model Functionality. arXiv:2602.08004. DOI: https://doi.org/10.48550/arXiv.2602.08004
- Zhiyuan Li, Jingzheng Wu, Xiang Ling, Xing Cui, and Tianyue Luo. 2026. Towards Secure Agent Skills: Architecture, Threat Taxonomy, and Security Analysis. arXiv:2604.02837. DOI: https://doi.org/10.48550/arXiv.2604.02837
- Yi Liu, Weizhe Wang, Ruitao Feng, Yao Zhang, Guangquan Xu, Gelei Deng, Yuekang Li, and Leo Zhang. 2026. Agent Skills in the Wild: An Empirical Study of Security Vulnerabilities at Scale. arXiv:2601.10338. DOI: https://doi.org/10.48550/arXiv.2601.10338
- Haoyu Gao, Jai Lal Lulla, Hong Yi Lin, Sebastian Baltes, Christoph Treude, and Mansooreh Zahedi. 2026. From Registry to Repository: How AI Agent Skills Are Written, Adapted, and Maintained. arXiv:2607.00911. DOI: https://doi.org/10.48550/arXiv.2607.00911
- Yuntao Wang, Jianle Ba, Han Liu, Yanghe Pan, Jintao Wei, Zhou Su, Tom H. Luan, and Linkang Du. 2026. Security of OpenClaw Agents: Fundamentals, Attacks, and Countermeasures. arXiv:2605.25435. DOI: https://doi.org/10.48550/arXiv.2605.25435
- Surada Suwansathit, Yuxuan Zhang, and Guofei Gu. 2026. A Security Analysis of the OpenClaw AI Agent Framework. arXiv:2603.27517. DOI: https://doi.org/10.48550/arXiv.2603.27517
- Bowen Wei, Yunbei Zhang, Jinhao Pan, Kai Mei, Xiao Wang, Jihun Hamm, Ziwei Zhu, and Yingqiang Ge. 2026. ClawSafety: "Safe" LLMs, Unsafe Agents. arXiv:2604.01438. DOI: https://doi.org/10.48550/arXiv.2604.01438
- Provider compatibility:
max_tokensis sent on the OpenAI protocol; some gateways expectmax_completion_tokensinstead. Verify against your endpoint. - Text encoding: non-UTF-8
.txtfiles fall back to latin-1 (never crashes, but CJK encodings such as Big5/GBK may render as mojibake). Use a charset detector if you expect those. - Secrets:
config/api.local.json,config/skills.json,.env, andhistory/are gitignored. Never commit real keys. - JSON contract: models that don't return the
status/answerenvelope are treated as a completed answer (best-effort fallback).