Commit Graph
347 Commits
Author SHA1 Message Date
Turtle 1540e76598 docs: capitalize Service Provider across repository 2026-08-13 13:31:30 +08:00
Yichen Jiang a135df8be6 Merge remote-tracking branch 'origin/master' into fix/run-code-description-contract
# Conflicts:
#	examples/headless-agent/tests/snapshots/advanced-toolchain/session.1.jsonl
#	examples/headless-agent/tests/snapshots/advanced-toolchain/session.2.jsonl
#	examples/headless-agent/tests/snapshots/advanced-toolchain/session.jsonl
2026-08-13 10:13:53 +08:00
imccyu 11a55092e8 fix: rebase 5 2026-08-13 02:51:52 +08:00
imccyu 1ff62760d1 fix: rebase 3 2026-08-13 02:29:17 +08:00
imccyu a7d4cd8e1b fix: ci 2026-08-13 01:33:31 +08:00
imccyu 0367506471 feat: slot system + entries/priority/errorreport + typert generator 2026-08-13 01:24:12 +08:00
Tianyi Cui a2d0f7f411 refactor: apply repository naming contract
Apply the accepted pre-release package, service, type, directory, and role renames as one repository-wide change.
2026-08-13 00:54:38 +08:00
Yichen Jiang 63fecf2534 fix: name run_code's required description argument in its model-facing prose
The transport schema requires both `code` and `description`, but the tool
description and both SDK instruction flavors described the call as passing a
program. `description` was reachable only through the parameter schema, so a
model following the prose emitted `{code}` alone and lost the whole written
program to an INVALID_ARGS rejection.

The length and format guidance stays in RUN_CODE_DESCRIPTION_PARAM_DESCRIPTION
alone, so the schema and the prompt cannot drift.

Fixes #2426
2026-08-12 23:30:51 +08:00
Yichen Jiang e258cf7a2d fix(tools): resolve the collapse through the scope, not the deployment default
`collapses()` read `defaultMode`, so the collapse only applied when the
DEPLOYMENT was `code`. An agent handed `code` by an agent preset under a
native default announced `[run_code]` on the wire and still executed a
model-direct native call -- the bypass this collapse exists to close,
reopened for exactly the composition `dsh-agent-tool-mode` produces.

`modeFor(scope)` is the same resolution `wireSchemas` and the SDK section
already use, so presentation and execution cannot disagree, and a mode
inherited from a standing preset scope collapses like a declared one.

The per-agent and preset tests asserted only the wire, which is why the
regression passed them. They now assert through the executor: the body
never runs, the call resolves UNKNOWN_TOOL, and the native sibling beside
it still executes.
2026-08-11 22:53:17 +08:00
Yichen Jiang 5d2c943d38 fix(tools): state the code-mode collapse in the prompt and the denial
The executor collapse landed without telling the model it exists. Every
tool contributes its own guidance section naming its tool, none of them
qualify how that tool is reached, and they all render before the SDK
(orders 100-199 against SDK_SECTION_ORDER 150), so the prompt said "Use
the read tool" eleven times and never said only run_code is callable.

A real session shows the consequence: the model emitted a native call,
read `unknown tool "read"` for a tool the same prompt declares, and
concluded the deployment was inconsistent rather than routing through
run_code.

The registry now contributes `tools:code-only` at order 99 -- ahead of
the guidance band -- stating the rule, registered wherever `tools:sdk`
is and rendering empty outside an effective `code`. `both` renders it
empty because its native calls do execute, which is also why
both-mode-turn no longer shares code-mode-turn's expected prompt. The
denial itself now names the route back, since a bare UNKNOWN_TOOL for a
declared tool is what misled the model.
2026-08-11 22:51:16 +08:00
Chinesezjc 47f108bf50 test(tools): use type-safe error assertions instead of any casts
oxlint's no-unsafe-member-access rejects member access through an any
cast; the error info is reachable through the declared optional chain.
2026-08-11 22:40:36 +08:00
Chinesezjc 610dc74ea8 test(tools): fix error code assertions for collapsed call tests
The error code lives on ToolFailure.info.code, not ToolFailure.code.
2026-08-11 22:40:36 +08:00
Chinesezjc 6d0a7c12e1 test(tools): add coverage for collapsed model-direct call under code mode
Add two executor-level tests covering the previously uncovered branches
in createExecution:
- collapsed call (non-aborted signal) returns UNKNOWN_TOOL
- collapsed call (pre-aborted signal) returns ABORTED_BEFORE_DISPATCH
2026-08-11 22:40:36 +08:00
Chinesezjc 24dd48b133 fix(tools): collapse code-mode executor to run_code for model-direct calls
wireSchemas() already advertised only run_code under mode: 'code', but the
executor resolved every call through get(), which returns the full visible
map plus the reserved transport. A model could name a native tool directly
and bypass run_code entirely. Route the execution-path lookups through a
new private resolveExecution() that applies the mode collapse at the
operation boundary: model-direct calls under 'code' may only name run_code
(UNKNOWN_TOOL otherwise), while SDK sub-dispatches (parent token set) keep
every visible tool. get()/schemas() public semantics are unchanged.

The denial happens at createExecution, before the extensible policy
pipeline — pre-execute listeners, approval ask, and guards never observe
a call that is deterministically denied. A collapsed call honors the
pre-dispatch cancellation contract, routes aborted results through the
visible tool's finalizeContent, and captures the finalizer before
argument materialization.

Regenerated docs, catalogs, graphs, scoped events, re-recorded
translation pairs, and updated test assertions.

Fixes #1815
2026-08-11 22:40:36 +08:00
Chinesezjc 6f6defd64a fix(tools): collapse code-mode executor to run_code for model-direct calls
wireSchemas() already advertised only run_code under mode: 'code', but the
executor resolved every call through get(), which returns the full visible
map plus the reserved transport. A model could name a native tool directly
and bypass run_code entirely. Route the execution-path lookups through a
new private resolveExecution() that applies the mode collapse at the
operation boundary: model-direct calls under 'code' may only name run_code
(UNKNOWN_TOOL otherwise), while SDK sub-dispatches (parent token set) keep
every visible tool. get()/schemas() public semantics are unchanged.

The denial happens at createExecution, before the extensible policy
pipeline — pre-execute listeners, approval ask, and guards never observe
a call that is deterministically denied. A collapsed call honors the
pre-dispatch cancellation contract, routes aborted results through the
visible tool's finalizeContent, and captures the finalizer before
argument materialization.

Under code mode, a system-prompt/assemble listener filters out tool:*
guidance sections that told the model to call native tools directly.
The tools:sdk section and SDK types remain so programs can still use
all tools through run_code.

Regenerated docs, catalogs, graphs, scoped events, and re-recorded
translation pairs.

Fixes #1815
2026-08-11 22:40:20 +08:00
Chinesezjc 4806fdabab fix(tools): collapse code-mode executor to run_code for model-direct calls
wireSchemas() already advertised only run_code under mode: 'code', but the
executor resolved every call through get(), which returns the full visible
map plus the reserved transport. A model could name a native tool directly
and bypass run_code entirely. Route the execution-path lookups through a
new private resolveExecution() that applies the mode collapse at the
operation boundary: model-direct calls under 'code' may only name run_code
(UNKNOWN_TOOL otherwise), while SDK sub-dispatches (parent token set) keep
every visible tool. get()/schemas() public semantics are unchanged.

The denial happens at createExecution, before the extensible policy
pipeline — pre-execute listeners, approval ask, and guards never observe
a call that is deterministically denied. A collapsed call honors the
pre-dispatch cancellation contract, routes aborted results through the
visible tool's finalizeContent, and captures the finalizer before
argument materialization.

Under code mode, a system-prompt/assemble listener filters out tool:*
guidance sections that told the model to call native tools directly.
The tools:sdk section and SDK types remain so programs can still use
all tools through run_code.

Fixes #1815
2026-08-11 22:40:19 +08:00
Tianyi Cui 3f01ff8291 Merge remote-tracking branch 'origin/master' into worktree/schedule-conversational-after 2026-08-11 19:05:13 +08:00
Turtle 096586886a fix(review): address terminology feedback 2026-08-11 15:23:05 +08:00
Turtle 0c708cb10d refactor: replace overloaded surface terminology 2026-08-11 15:23:05 +08:00
creatixchu c1f0a6a683 Merge remote-tracking branch 'origin/master' into feat/read-image-context
# Conflicts:
#	apps/cli/tests/web-agent-presets.e2e.ts
#	packages/fs/tool-fs/package.json
2026-08-11 10:12:14 +08:00
imccyu ec601ca13d build(vendor): rescope the vendored Cordis packages into @deepseek-ai
Machine-produced by `pnpm run rescope-vendor --apply` plus the regeneration it
prints: `pnpm install` for the lockfile, `pnpm run gen-third-party-notices`,
`verify-translation-pairing --write` for the touched bilingual pairs,
`gen-doc-graphs`, and one typert snapshot whose ids embed character offsets.
`pnpm run rescope-vendor --check` verifies the result.

Renames nine vendored packages (cordis, cosmokit, schemastery and the six
@cordisjs plugins) and every reference that resolves them: manifest names and
dependency keys, module specifiers including declare-module merges, cordis.yml
plugin names, tsconfig paths, every Markdown fence, and `docs/` prose.
Directory names, upstream versions, and dependency ranges are unchanged, so
vendor/README.md still reads as an upstream snapshot; its manifest table gains
an upstream-name column so THIRD_PARTY_NOTICES keeps MIT attribution pointed
at each fork's origin.

The tutorial tier follows the rename end to end: its yaml fences named plugins
the Loader can no longer resolve, its `ts ignore-check` fences disagreed with
the compiled fences beside them, and its prose quoted both. The contracts that
told readers to keep upstream names — the root convention and the vendoring
cookbook's tree comment and manifest invariant — now say to rescope instead.

Two rules read `@deepseek-ai/` as "another workspace plugin": the client bundle
purity gate now names the vendored libraries a browser bundle inlines, and the
files where a bare `cordis` is an agent-preset id keep that product data.
2026-08-10 22:04:13 +08:00
Yichen Jiang 43f3324a7b fix(tools): restrict what a scope inherits, not just the global layer
A restriction was compiled against the global tool layer alone: only
global-layer tools were tested against `admits()`, and every chain-layer
tool was overlaid unfiltered afterward. That read the exempt set as "the
global layer" when what it means is "what this scope registers itself" —
two descriptions of the same set only while every model-facing tool sat in
the host composition.

Moving those rows onto the agent plane separated them. A preset's tools are
an ANCESTOR contribution to a joined agent, so a subagent's `toolFilter`
stopped constraining anything it was given; and with the global layer empty
`restrict()` rejected every name it received as unknown, failing the child
outright. With the same tools in the global layer the filter still admits
and applies normally, which is what makes this a regression of the move
rather than a standing limitation.

`view()` now filters everything a scope inherits — the global layer and
every ancestor layer on its chain — and exempts only the layer the scope
owns. That exemption is load-bearing rather than incidental: the delegation
runtime registers a child's `report` and structured-output tools into the
child's own layer, and a filter naming the capabilities the child may use
must not strip the machinery it answers through. Tool order, and with it
prefix-cache reuse, is unchanged: inherited names keep their global-then-
ancestor position and own-layer names still come last.

The diagnostic said "unknown global tool" while listing what is really the
inherited surface; it now names the surface it checks and says why an
own-layer name is not restrictable.

Fixes #2185
2026-08-10 20:34:45 +08:00
creatixchu b07844dd4c Merge branch 'master' into feat/read-image-context
# Conflicts:
#	docs/module-graph.i18n.yaml
#	docs/module-graph.md
#	docs/module-graph.zh.md
2026-08-10 19:01:47 +08:00
Turtle a27efdef36 docs: make technical prose concrete 2026-08-10 16:34:20 +08:00
creatixchu 1861a3fc7c feat(fs): add a minimal read_image tool over the attachment and fs seams
The model reads a PNG/JPEG/WebP/GIF file, the bytes commit through the
durable attachment lifecycle, and the tool result carries the real
ImageBlock so the image enters context from the next request onward.
FileSystem gains a bounded readBytes primitive (local + E2B providers);
registration is conditional on the attachment store, and a strict
execution gate refuses routes that do not declare image input, so a
text route's durable history stays free of image blocks. llm-replay
models may declare inputModalities, letting keyless ACP snapshots pin
both the sha256-referenced success and the verbatim refusal.

Supersedes the withdrawn route-scoped design of PR #598; the decision
record is .agents/notes/implemented/feature/2026-08-10-minimal-read-image-tool.md.
2026-08-10 15:09:07 +08:00
Yichen Jiang 2be0e6c522 Merge branch 'stack/agent-profiles-5-web-ui' into stack/agent-profiles-8-authoring
# Conflicts:
#	packages/preset/agent-presets/src/index.ts
#	packages/preset/agent-presets/tests/mount.spec.ts
2026-08-10 11:25:14 +08:00
Yichen Jiang 7ea8bad7ee Merge branch 'stack/agent-profiles-5-web-ui' into stack/agent-profiles-8-authoring
# Conflicts:
#	docs/module-graph.i18n.yaml
#	docs/module-graph.md
#	docs/module-graph.zh.md
#	docs/subsystems/tools.i18n.yaml
#	docs/subsystems/tools.md
#	docs/subsystems/tools.zh.md
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl
2026-08-09 21:38:26 +08:00
Tianyi Cui e2a9976fce Merge origin/master into worktree/schedule-conversational-after
# Conflicts:
#	docs/event-producer-consumer.i18n.yaml
#	docs/event-producer-consumer.md
#	docs/event-producer-consumer.zh.md
2026-08-09 21:07:50 +08:00
Yichen Jiang 8612f39759 Merge remote-tracking branch 'origin/stack/agent-profiles-5-web-ui' into stack/agent-profiles-8-authoring
# Conflicts:
#	packages/client/connection/README.i18n.yaml
#	packages/client/connection/README.md
#	packages/client/connection/README.zh.md
2026-08-09 20:41:55 +08:00
imccyu 522acc7432 test: update conversation assembly coverage and docs 2026-08-09 18:16:44 +08:00
Turtle dda02250f5 docs: reserve seam for complete capabilities 2026-08-09 17:26:57 +08:00
Yichen Jiang c0522f21d9 Merge branch 'stack/agent-profiles-5-web-ui' into stack/agent-profiles-8-authoring 2026-08-08 22:54:07 +08:00
Yichen Jiang 66e9b01fbd fix(tools,client): chain-mode resolution and a live preset label
modeFor read only the exact scope's layer, so a `code`-preset session
advertised the native catalog: the mode is declared on the preset's STANDING
scope, and the agent only parents to it. Nearest scope wins along the chain —
the mode decides what the model SEES, which is the class of fact the chain
inherits. Caught live (the model politely computed with bash and said
run_code was not in its list); the chain test pins it.

The client half of the label fix: the create echo and the session-added
frame's agentPreset now reach the session list (newest wins in the upsert —
every producer of the field reports the CURRENT composition), and a confirmed
blank-session switch publishes through the new ISessions.noteAgentPreset, so
the header label moves with the composition instead of waiting for a reload.
2026-08-08 20:19:00 +08:00
Hypatia May d769d3cbb7 feat(subagent): let ancestors interrupt descendants
interrupt_agent(agent_id) passes the calling agent as the ancestor
authority for ctx.subagents.interrupt(); the service verifies live
registry identity and recorded lineage, so a direct child or deeper
descendant stops with the same generic parameter while send_message keeps
its exact-direct-parent authority.

Discovery: list_agents gains an optional scope. descendants walks the new
SubagentService.listDescendants() — one lineage trace flattened in stable
pre-order across ordinary and one-shot intermediates, each entry carrying
its verified parentId and depth — and every status now comes from the
live Agent registry (running/idle/complete).

Refs #1535
2026-08-08 20:02:37 +08:00
Yichen Jiang 9b56349ba3 Merge branch 'stack/agent-profiles-8-authoring' into stack/agent-profiles-9-rename
# Conflicts:
#	docs/config-catalog.md
#	docs/cordis-catalog/services.md
#	packages/core/tools/README.i18n.yaml
#	packages/core/tools/README.md
#	packages/core/tools/README.zh.md
#	packages/core/tools/src/index.ts
2026-08-07 13:59:20 +08:00
Yichen Jiang 9eaa9d22a5 feat(tools): let one agent choose its tool presentation, and ship code
Code Mode was a deployment-wide field on the host `tools` row: a
deployment ran every session that way or none. The obvious product
shape — 代码模式 beside 标准/极简/创造 in the preset picker — had
nothing to hang on.

The registry itself cannot move into a preset; the agent loop's
scheduler, the api-proxy's presenters, and every tool plugin are its
consumers. So split the registry from its projection: `presentAs(mode)`
writes one cell on the calling agent's scope layer, exactly as
`restrict()` does, and the three reads that decided presentation take
that scope's mode instead of the service's. The config `mode` becomes
the default agents shadow rather than a process-wide fact.

Two consequences are load-bearing. `run_code` now enters a view only
for scopes whose own mode presents it — a native agent must not find it
dispatchable because another agent in the process does — and the
reserved name holds whatever the configured mode, since any agent may
select a code mode later.

`dsh-agent-tool-mode` is the row a preset carries to declare this. A
code mode waits for the host's `codeRuntime` rather than assuming it,
so a runtime-less deployment fails the preset at mount, naming the
row, instead of at the session's first request.

The shipped `code` preset is `standard` plus that row, ordered second.
2026-08-07 00:44:49 +08:00
pku-xht 8b69252664 feat(schedule): add durable after reminders 2026-08-06 03:47:37 +08:00
Chinesezjc 0de132b927 test(tools): drop the quantifier that miscounted its own enumeration
"Two emitted positions" was followed by an enumeration counting
occurrences — the field twice, the class statement once — so the two
halves of the sentence disagreed. The sentence now states what needs the
tables without a count.
2026-08-06 02:33:25 +08:00
Chinesezjc 631d3f930e test(tools): escape U+200C in the new case and name both carriers of the 15.1 requirement
The file's convention is a \uXXXX escape for a character with no visible
width (\u0301, \u1100, \u1161, \ud800 are all written that way) and a
literal only for a visible one; the new case wrote nine raw ZWNJ.

The comment also named only the field as needing 15.1 tables. Two emitted
code positions do: the bare field, once in each class, and the
Tool\u200CbArgs class name. The subscript comment is not one.
2026-08-06 02:22:51 +08:00
Chinesezjc 4f8ba6c190 test(tools): pin the U+200C tail/head split; qualify the identifier-equivalence measurement
The docstring names ZWNJ/ZWJ as a skew instance but nothing checked the
predicate's tail-position accept branch; this is its first test, and it
also covers camelCase's Tool-prefix branch for a head that is
XID_Continue but not XID_Start.

The equivalence sentence pinned its evidence to Node 22.23.1 against
CPython 3.9.6 without saying the samples sit inside those two versions'
shared tables, next to five named characters where that same pair
diverges.
2026-08-06 02:09:27 +08:00
Chinesezjc eb3b635796 docs(tools): widen the ungated language-prose list and correct three JSDoc claims 2026-08-06 01:38:26 +08:00
Chinesezjc e19740e7d0 docs(tools): bind wrapped em-dashes, widen the dict degrade note, bound the determinism claim 2026-08-06 00:51:09 +08:00
Chinesezjc b2c1872799 docs(tools): cite per-character Unicode ages and the ungated seam edit 2026-08-06 00:07:10 +08:00
Chinesezjc 99218ba41d docs(tools): qualify the last mode-dependent claim, in the spec
`keeps a non-ASCII field name…` asserted that Code Mode omits the native
schemas so nothing else carries a dropped field's name, requiredness, and
type. That holds under `mode: 'code'` only; under `both` the native schemas
ship alongside the SDK, as the module header says. Earlier rounds swept
`py-types.ts` for this family and qualified five sites there; the spec was
never in scope, so this is the family's last unqualified member rather than
residue from those fixes.
2026-08-05 23:09:06 +08:00
Chinesezjc 7d957bc799 docs(tools): name both raw routes, complete the evaluation-floor list
The previous commit's `UNPRINTABLE` sentence said the raw-reach point for all
three characters is `pyScalar`'s `JSON.stringify`, and the test comment said
that route is the only one. Both are exclusive claims and both are false: the
subscript tool-name comment calls `JSON.stringify` itself, and a tool name
carrying NEL, LS, or PS always lands there, none of the three being
`XID_Continue`. `pyScalar`'s own docstring already recorded that inheritance,
so the file contradicted itself. Both sentences now name the two call sites.

The note's evaluation axis was introduced as "the `typing` names the block
spells", which excludes one of its own members (`A | B` is operator syntax)
and omitted PEP 585 builtin generics — `dict[str, Any]` and `list[…]` appear
in nearly every render and need 3.9. The axis is now "the names and syntax the
block would evaluate at definition time", enumerated 3.8 through 3.11.

The test title covered two of the three characters it asserts; NEL is NEXT
LINE, neither a line nor a paragraph separator.
2026-08-05 22:56:32 +08:00
Chinesezjc b869a3b078 docs(tools): close the NEL half of the raw pass-through and reflow
Four non-blocking review suggestions, all prose plus one assertion.

`UNPRINTABLE`'s new sentence named three characters but only two raw-reach
points, leaving "and NEL?" open; it now says all three reach text through
`pyScalar`, and how the description path handles each. `pyScalar`'s
raw-pass-through list already covered NEL under "the C1 controls", and the
test now pins it alongside LS and PS, so the docstring's claim has a
mechanical check for every character it names.

The test title said "paragraph separators" for a pair whose first member is
LINE SEPARATOR. Two docstring paragraphs are reflowed to the file's ~80
columns after the earlier inserts left short lines.

The note's CPython-floor obligation gains a second axis: the `typing` names
the block spells (`TypedDict` 3.8, `NotRequired` 3.11, `A | B` annotations
3.10) are definition-time evaluation floors, not parse floors, so the floor PR
does not read "parseable on the supported range" as "executable on it".
2026-08-05 22:43:42 +08:00
Chinesezjc 05213ec85f Merge remote-tracking branch 'origin/master' into feat/py-types-code-mode
# Conflicts:
#	docs/event-producer-consumer.md
2026-08-05 22:27:13 +08:00
Chinesezjc c5b09c108f docs(tools): record LS/PS as tokenizer non-terminators, with a test
Review read `JSON.stringify`'s raw pass-through of U+0085/U+2028/U+2029 as a
parse hazard: an LS in a `Literal[...]` value or in a `# tools["..."]` comment
would end the physical line and take the SDK block down. Measured on CPython
3.9.6 (Unicode 13.0) and 3.12.13 (15.0): all three are accepted in both a
string literal and a `#` comment, value round-tripping, and only LF and CR
terminate either. The set is the tokenizer's, not `str.splitlines()`'.

Both existing claims were accurate, so nothing changes behaviorally. Name the
distinction where it was assumed: `UNPRINTABLE`'s terminator sentence now says
which set it means, and `pyScalar`'s raw-pass-through list, previously "DEL and
the C1 controls", now also names LS/PS, which are neither. A test pins the raw
form for `const` and `enum` so escaping them later cannot land as a silent
divergence from the TypeScript flavor.
2026-08-05 22:25:45 +08:00
Chinesezjc 0891556cb4 docs(tools): qualify the three remaining mode-dependent only-claims
The `mode: 'code'` qualification landed on the module header and
`isBareIdentifier` but not on the other three sites the same reviews
enumerated. `UNPRINTABLE`'s "only declaration of the tools", the open-object
comment's "only signal that extra keys are accepted", and the docstring
comment's "only description of what a tool does" are each false under
`mode: 'both'`, where the native schemas ship alongside the SDK.

Widen the note's predicate-path sentence past head and last position: a
character added to `XID_Continue` passes `IDENTIFIER`'s trailing quantifier
anywhere after the head, the middle of a name included.

Record the ƛ test's table provenance. U+A7DC and the U+019B mapping to it
both arrive in Unicode 16.0, and the engines floor sits exactly there:
Node 22.19.0 reports Unicode 16.0 (ICU 77.1) and produces the mapping.
2026-08-05 21:53:32 +08:00
Chinesezjc be98a0b978 docs(tools): record the case-mapping read point and narrow the class-name quantifier
`camelCase`'s `toUpperCase()` is a fourth reader of the engine's Unicode
tables, on a table distinct from XID membership and with a wider window: a
tool named U+019B passes `isBareIdentifier` and compiles as `async def` on
CPython 3.9.6, but Node maps the head to U+A7DC and the declared
`class ꟜArgs` fails there with `invalid non-printable character`. Record it
alongside the three XID read points in the renderer docs and in the note's
CPython-floor obligation, and pin the derivation with a test.

Correct three over-quantified sentences: a `camelCase`-derived class name is
evaluated for every tool but only reaches emitted text when some object shape
in the schema declares a `TypedDict`. Attribute the `str.isidentifier()`
equivalence to `IDENTIFIER` rather than to the predicate, which is
deliberately stricter, and restore the antecedent the mode qualification
dropped.
2026-08-05 21:33:30 +08:00