@tokensapi/dsh-progressive-tools 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +124 -0
- package/CONTRIBUTING.md +32 -0
- package/LICENSE +22 -0
- package/README.md +246 -0
- package/README.zh-CN.md +216 -0
- package/SECURITY.md +22 -0
- package/THIRD_PARTY_NOTICES.md +8 -0
- package/cordis.patch.yml +3 -0
- package/docs/architecture.md +225 -0
- package/docs/configuration.md +238 -0
- package/docs/progressive-disclosure.md +82 -0
- package/lib/catalog.d.ts +12 -0
- package/lib/catalog.js +267 -0
- package/lib/defaults.d.ts +16 -0
- package/lib/defaults.js +122 -0
- package/lib/index.d.ts +50 -0
- package/lib/index.js +908 -0
- package/lib/state.d.ts +16 -0
- package/lib/state.js +110 -0
- package/lib/types.d.ts +120 -0
- package/lib/types.js +1 -0
- package/package.json +96 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
## Goals and invariants
|
|
4
|
+
|
|
5
|
+
The default architecture has four invariants:
|
|
6
|
+
|
|
7
|
+
1. The first AgentLoop request already carries the small surface.
|
|
8
|
+
2. Discovery never changes the top-level tool list or generated SDK.
|
|
9
|
+
3. Deferred calls still traverse the complete DSH execution pipeline.
|
|
10
|
+
4. A caller cannot bypass discovery by naming a hidden tool directly.
|
|
11
|
+
|
|
12
|
+
The plugin uses only public Harness APIs and extension points:
|
|
13
|
+
|
|
14
|
+
- `ctx.systemPrompt.section()` for stable discovery guidance;
|
|
15
|
+
- `system-prompt/assemble` for the authoritative request projection;
|
|
16
|
+
- `ctx.tools.register()` for search and dispatch;
|
|
17
|
+
- `ctx.tools.guard()` for monotonic routing enforcement;
|
|
18
|
+
- `ctx.tools.execute()` for nested real-tool execution;
|
|
19
|
+
- `tools/result` for authoritative discovery commits;
|
|
20
|
+
- `tools/change` for in-process catalog invalidation;
|
|
21
|
+
- `agent/session-start` and durable session events for state initialization;
|
|
22
|
+
- `agent/disposed` and Cordis effects for cleanup.
|
|
23
|
+
|
|
24
|
+
## The DSH lifecycle boundary
|
|
25
|
+
|
|
26
|
+
DSH assembles prompt sections and tool schemas before `agent/pre-step`:
|
|
27
|
+
|
|
28
|
+
```text
|
|
29
|
+
claim inbox input
|
|
30
|
+
│
|
|
31
|
+
▼
|
|
32
|
+
systemPrompt.assemble()
|
|
33
|
+
│
|
|
34
|
+
├── collect sections and tools
|
|
35
|
+
└── system-prompt/assemble waterfall
|
|
36
|
+
│
|
|
37
|
+
▼
|
|
38
|
+
agent/pre-step
|
|
39
|
+
│
|
|
40
|
+
▼
|
|
41
|
+
LLM request
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Version 0.1.0 installed a restriction in `agent/pre-step`, so it could affect
|
|
45
|
+
only the following assembly. Version 0.2.0 performs the authoritative
|
|
46
|
+
projection in `system-prompt/assemble`, after all providers have contributed
|
|
47
|
+
but before AgentLoop stores or sends the request.
|
|
48
|
+
|
|
49
|
+
`agent/session-start` eagerly initializes the catalog for normal creation and
|
|
50
|
+
resume. The assembly hook remains authoritative and also covers hot reload,
|
|
51
|
+
late registration, and callers that assemble without a normal startup event.
|
|
52
|
+
|
|
53
|
+
## Stable-proxy mode
|
|
54
|
+
|
|
55
|
+
### Request projection
|
|
56
|
+
|
|
57
|
+
On the first assembly for one agent, the plugin freezes that session's stable
|
|
58
|
+
name set:
|
|
59
|
+
|
|
60
|
+
- `tool_search`;
|
|
61
|
+
- `tool_dispatch`;
|
|
62
|
+
- registered names matching `alwaysVisible`;
|
|
63
|
+
- the reserved `run_code` transport when DSH exposes it.
|
|
64
|
+
|
|
65
|
+
The Tokens distribution defaults `alwaysVisible` to the common file, shell,
|
|
66
|
+
Skill, task, interaction, and file-return tools used on nearly every product
|
|
67
|
+
session. This keeps routine work direct while deferring plugin-specific and
|
|
68
|
+
low-frequency schemas.
|
|
69
|
+
|
|
70
|
+
The complete registry remains visible to in-process code. The assembly
|
|
71
|
+
waterfall filters only the final `PromptAssembly.tools` projection. In Code
|
|
72
|
+
Mode and `both` mode, the `tools:sdk` section is regenerated from the same
|
|
73
|
+
stable names, preserving one coherent presentation.
|
|
74
|
+
|
|
75
|
+
`deferToolGuidance` conservatively removes a section only when its name is an
|
|
76
|
+
exact hidden-tool guidance slot (`tool:<name>` or `tool:<name>:...`). General
|
|
77
|
+
guidance and sections that cannot be mapped safely remain untouched.
|
|
78
|
+
|
|
79
|
+
The stable name set does not grow after first assembly. A later registry change
|
|
80
|
+
refreshes only the searchable catalog, so installing a new deferred tool does
|
|
81
|
+
not silently change an active session's cache prefix.
|
|
82
|
+
|
|
83
|
+
### Discovery
|
|
84
|
+
|
|
85
|
+
The catalog stores detached model-facing definitions in process memory. Search
|
|
86
|
+
operates on individual tools, not whole families. A searchable document
|
|
87
|
+
contains:
|
|
88
|
+
|
|
89
|
+
- tool name and description;
|
|
90
|
+
- nested parameter keys, descriptions, constants, and enums;
|
|
91
|
+
- configured family ID, description, and aliases.
|
|
92
|
+
|
|
93
|
+
Ranking combines exact-name and contained-label bonuses with a deterministic
|
|
94
|
+
BM25-style lexical score. CJK text is additionally tokenized into character
|
|
95
|
+
bigrams, so queries without space-delimited words can match definitions and
|
|
96
|
+
family metadata without configured aliases. Search returns at most
|
|
97
|
+
`maxResults` exact definitions; larger `max_results` requests are clamped.
|
|
98
|
+
Those definitions enter the ordinary tool result and therefore extend history
|
|
99
|
+
append-only.
|
|
100
|
+
|
|
101
|
+
Each match also lists every member name of its family (`groupTools`), and the
|
|
102
|
+
whole family becomes discovered in the same call. One query therefore opens a
|
|
103
|
+
plugin's complete tool surface even when only its top-ranked members carry
|
|
104
|
+
full schemas; the remaining siblings dispatch by name and validate against
|
|
105
|
+
their original definitions, or can be schema-loaded first with one exact-name
|
|
106
|
+
search.
|
|
107
|
+
|
|
108
|
+
The `status` action lists every deferred family with its member tool names, so
|
|
109
|
+
the model can browse the catalog when a search query has no lexical overlap.
|
|
110
|
+
By default the listing is browse-only; `statusGrantsDiscovery` optionally
|
|
111
|
+
turns it into a catalog-wide discovery grant.
|
|
112
|
+
|
|
113
|
+
Successful `tools/result` observation commits the returned names to the
|
|
114
|
+
agent's discovered set. Failed or invalid searches do not mutate live state.
|
|
115
|
+
|
|
116
|
+
### Dispatch
|
|
117
|
+
|
|
118
|
+
`tool_dispatch` accepts an exact discovered name and one JSON object of
|
|
119
|
+
arguments. It then calls the ordinary registry:
|
|
120
|
+
|
|
121
|
+
```text
|
|
122
|
+
tool_dispatch root execution
|
|
123
|
+
│
|
|
124
|
+
├── check catalog membership and discovery state
|
|
125
|
+
├── preserve agent, rootCallId, signal, and arguments
|
|
126
|
+
└── ctx.tools.execute(real tool, parent = dispatcher token)
|
|
127
|
+
│
|
|
128
|
+
├── pre-execute / approval
|
|
129
|
+
├── monotonic guards
|
|
130
|
+
├── timeout and retry wrappers
|
|
131
|
+
├── original schema validation and body
|
|
132
|
+
├── post-execute and finalization
|
|
133
|
+
└── tools/result
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Nested contexts and a successful turn-conclusion marker are ferried back to
|
|
137
|
+
the outer result. The outer rendering uses the real tool's finalized content,
|
|
138
|
+
including non-text blocks. A nested failure is rethrown with the real tool's
|
|
139
|
+
structured error code preserved, and the dispatcher delegates its
|
|
140
|
+
parallel-scheduling classification to the target tool's own declaration, so
|
|
141
|
+
concurrency-safe deferred tools keep overlapping with sibling calls.
|
|
142
|
+
|
|
143
|
+
### Routing guard
|
|
144
|
+
|
|
145
|
+
Prompt filtering alone does not change registry lookup. Stable mode therefore
|
|
146
|
+
registers a monotonic guard:
|
|
147
|
+
|
|
148
|
+
- stable direct names are allowed;
|
|
149
|
+
- deferred direct names are denied;
|
|
150
|
+
- a nested execution whose parent token belongs to `tool_dispatch` is allowed;
|
|
151
|
+
- descendants of that authorized nested execution inherit the authorization
|
|
152
|
+
for the duration of their execution tree.
|
|
153
|
+
|
|
154
|
+
Tokens are registry-minted opaque identities, so a caller cannot manufacture
|
|
155
|
+
the parent capability. Authorized tokens are removed on result and plugin
|
|
156
|
+
cleanup. The guard prepares the agent's state on demand, so a call that
|
|
157
|
+
arrives before the first assembly or session-start event is still classified
|
|
158
|
+
against the deferred catalog instead of passing through unexamined.
|
|
159
|
+
|
|
160
|
+
The guard is not an authorization boundary for the underlying capability. It
|
|
161
|
+
enforces presentation-to-dispatch alignment while existing approval, sandbox,
|
|
162
|
+
and policy layers continue to own security decisions.
|
|
163
|
+
|
|
164
|
+
## Cache behavior
|
|
165
|
+
|
|
166
|
+
For an unchanged session composition:
|
|
167
|
+
|
|
168
|
+
```text
|
|
169
|
+
request 1: stable tools + stable system + user history
|
|
170
|
+
request 2: stable tools + stable system + prior history + search result
|
|
171
|
+
request 3: stable tools + stable system + prior history + dispatch result
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Only history grows. The tool/system prefix remains byte-identical. The
|
|
175
|
+
AgentLoop integration test captures actual `GenerateOptions` values and asserts
|
|
176
|
+
both the first request surface and post-search equality.
|
|
177
|
+
|
|
178
|
+
A real composition change can still alter the prefix: changing plugin config,
|
|
179
|
+
removing a stable tool, changing its definition, or replacing another system
|
|
180
|
+
section is outside the discovery invariant.
|
|
181
|
+
|
|
182
|
+
## Resume behavior
|
|
183
|
+
|
|
184
|
+
Discovery state is recorded asymmetrically. The rendered search result — the
|
|
185
|
+
text the model reads and re-reads in history — carries only the names newly
|
|
186
|
+
discovered by that call, so conversation growth stays bounded no matter how
|
|
187
|
+
much has been discovered. The result's presentation metadata, which never
|
|
188
|
+
reaches the model, carries the cumulative discovered list and the action kind.
|
|
189
|
+
|
|
190
|
+
On resume the plugin replays successful search results and skill bindings,
|
|
191
|
+
preferring the cumulative metadata when present and unioning per-call
|
|
192
|
+
increments otherwise. The metadata path restores full state from the latest
|
|
193
|
+
surviving entry even when older events were compacted. Nested Code Mode calls
|
|
194
|
+
have no top-level presentation metadata, so their standard
|
|
195
|
+
`tool/code-dispatch` content increments are folded instead. A replayed
|
|
196
|
+
`status` result also restores the catalog-listing grant used by
|
|
197
|
+
`statusGrantsDiscovery`.
|
|
198
|
+
|
|
199
|
+
The plugin introduces no custom session event vocabulary. Discovered names
|
|
200
|
+
survive registry refreshes such as provider reconnects; dispatch validates
|
|
201
|
+
catalog membership at call time, so a stale name fails the individual call
|
|
202
|
+
without losing the rest of the discovery state.
|
|
203
|
+
|
|
204
|
+
## Dynamic compatibility mode
|
|
205
|
+
|
|
206
|
+
`mode: dynamic` retains the v0.1 family activation design for deployments that
|
|
207
|
+
need provider-native definitions after search. Its lifecycle is corrected:
|
|
208
|
+
|
|
209
|
+
- initial restriction is installed at `agent/session-start`;
|
|
210
|
+
- turn expiry is reconciled at `agent/inbox/claimed`;
|
|
211
|
+
- successful search and skill results reinstall the restriction immediately;
|
|
212
|
+
- the assembly waterfall filters the already-collected current assembly and
|
|
213
|
+
regenerates Code Mode SDK text from the same visible set.
|
|
214
|
+
|
|
215
|
+
This mode aligns presentation, lookup, and execution through
|
|
216
|
+
`agent.ctx.tools.restrict()`, but changing families changes the tool prefix and
|
|
217
|
+
can reduce cache reuse. It cannot hide tools registered in the exact agent
|
|
218
|
+
scope because scoped restrictions intentionally preserve scope-local tools.
|
|
219
|
+
|
|
220
|
+
## Cleanup
|
|
221
|
+
|
|
222
|
+
Tool registrations, the prompt section, assembly listener, guard, and event
|
|
223
|
+
listeners are Cordis-owned effects. Agent disposal removes its state and lifts
|
|
224
|
+
any dynamic restriction. Plugin unload lifts all remaining restrictions and
|
|
225
|
+
clears authorization tokens before its registered tools disappear.
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# Configuration reference
|
|
2
|
+
|
|
3
|
+
Configuration is validated when the plugin loads. Invalid modes, names,
|
|
4
|
+
limits, duplicate family IDs, empty patterns, and bindings to unknown families
|
|
5
|
+
fail loudly.
|
|
6
|
+
|
|
7
|
+
## Options
|
|
8
|
+
|
|
9
|
+
| Option | Type | Default | Meaning |
|
|
10
|
+
| --- | --- | --- | --- |
|
|
11
|
+
| `mode` | `stable-proxy` \| `dynamic` | `stable-proxy` | Cache-stable dispatch or changing native family activation. |
|
|
12
|
+
| `toolName` | string | `tool_search` | Discovery tool name. |
|
|
13
|
+
| `dispatchToolName` | string | `tool_dispatch` | Stable dispatcher name. Must differ from `toolName`. |
|
|
14
|
+
| `alwaysVisible` | string[] | essential direct tools | Exact names or `*` patterns kept on the fixed direct surface. |
|
|
15
|
+
| `groups` | group[] | built-in rules | Ordered search and dynamic activation families; first match wins. |
|
|
16
|
+
| `skillBindings` | binding[] | `[]` | Successful Skill calls that discover or activate named families. |
|
|
17
|
+
| `maxResults` | integer | `5` | Maximum exact definitions returned by stable search, or group matches in dynamic mode. Caller `max_results` values outside `1..maxResults` are clamped, not rejected. |
|
|
18
|
+
| `requireDiscovery` | boolean | `true` | Require a search hit, a family-wide discovery, or a Skill binding before stable dispatch. |
|
|
19
|
+
| `statusGrantsDiscovery` | boolean | `false` | Let one `status` listing make every cataloged name dispatchable. Off by default so dispatch always follows a seen schema. |
|
|
20
|
+
| `deferToolGuidance` | boolean | `true` | Remove exact hidden `tool:<name>` prompt sections. |
|
|
21
|
+
| `activationGroupLimit` | integer | `1` | Dynamic mode: highest-ranked families activated by one search. |
|
|
22
|
+
| `maxActiveGroups` | integer | `3` | Dynamic mode: maximum retained active families. |
|
|
23
|
+
| `maxActiveToolTokens` | integer | `6000` | Dynamic mode: approximate active-schema budget. |
|
|
24
|
+
| `retentionTurns` | integer | `6` | Dynamic mode: inactive turns before expiry; `0` disables expiry. |
|
|
25
|
+
| `charactersPerToken` | integer | `4` | Compact schema characters represented by one estimate token. |
|
|
26
|
+
|
|
27
|
+
`activationGroupLimit` cannot exceed `maxActiveGroups`. Dynamic-only fields are
|
|
28
|
+
still validated in stable mode so switching modes cannot reveal a latent bad
|
|
29
|
+
configuration.
|
|
30
|
+
|
|
31
|
+
## Stable direct tools
|
|
32
|
+
|
|
33
|
+
The default patterns are:
|
|
34
|
+
|
|
35
|
+
```yaml
|
|
36
|
+
alwaysVisible:
|
|
37
|
+
- read
|
|
38
|
+
- write
|
|
39
|
+
- edit
|
|
40
|
+
- glob
|
|
41
|
+
- grep
|
|
42
|
+
- bash
|
|
43
|
+
- skill
|
|
44
|
+
- ask_user_question
|
|
45
|
+
- todo_write
|
|
46
|
+
- dsh_im_return_file
|
|
47
|
+
- report
|
|
48
|
+
- submit_*
|
|
49
|
+
- structured_output*
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`tool_search` and `tool_dispatch` are added automatically. A matching tool must
|
|
53
|
+
exist when the first assembly freezes the session surface. Tools registered
|
|
54
|
+
later enter the deferred catalog even when their names match an
|
|
55
|
+
`alwaysVisible` wildcard; this keeps the active session prefix stable. A new
|
|
56
|
+
session sees the new composition.
|
|
57
|
+
|
|
58
|
+
Keep this list small. Add only tools whose direct schema or completion role is
|
|
59
|
+
worth paying on every request.
|
|
60
|
+
|
|
61
|
+
## Wildcards
|
|
62
|
+
|
|
63
|
+
`alwaysVisible`, `groups[].include`, and `groups[].exclude` use anchored `*`
|
|
64
|
+
wildcards over complete tool names. Every other character is matched literally.
|
|
65
|
+
Matching is case-insensitive.
|
|
66
|
+
|
|
67
|
+
```yaml
|
|
68
|
+
alwaysVisible:
|
|
69
|
+
- approval_*
|
|
70
|
+
- submit_*
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Family rules
|
|
74
|
+
|
|
75
|
+
```yaml
|
|
76
|
+
groups:
|
|
77
|
+
- id: browser
|
|
78
|
+
description: Browser navigation and page interaction
|
|
79
|
+
aliases: [browser, web page, 浏览器]
|
|
80
|
+
include: [browser_*]
|
|
81
|
+
exclude: [browser_experimental_*]
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Rules are evaluated in list order. A tool belongs to at most one configured
|
|
85
|
+
family. Unmatched tools are grouped by the first underscore-delimited prefix
|
|
86
|
+
when at least two share it; otherwise each tool receives a singleton family.
|
|
87
|
+
Generic verb prefixes (`get`, `set`, `list`, `create`, `delete`, `update`,
|
|
88
|
+
`add`, `remove`, `cancel`, `run`, `start`, `stop`, `send`, `read`, `write`,
|
|
89
|
+
`new`, `check`) never merge, because unrelated plugins routinely share them;
|
|
90
|
+
such tools stay singleton families unless a configured rule claims them.
|
|
91
|
+
|
|
92
|
+
In stable mode, family metadata improves exact-tool search, the result
|
|
93
|
+
contains the highest-ranked individual definitions, and each match lists every
|
|
94
|
+
member name of its family so siblings become dispatchable from one query. In
|
|
95
|
+
dynamic mode, the highest-ranked whole family becomes natively visible.
|
|
96
|
+
Because family membership now also widens discovery, a wrong rule no longer
|
|
97
|
+
just skews ranking — it unlocks unrelated names. Keep custom rules precise.
|
|
98
|
+
|
|
99
|
+
The built-in order covers browser, vision, image generation, filesystem,
|
|
100
|
+
terminal, web, database, remote operations, memory, workbench, teams,
|
|
101
|
+
subagents, workflows, and generated interfaces. Explicit rules are recommended
|
|
102
|
+
when third-party packages use unrelated names for one capability.
|
|
103
|
+
|
|
104
|
+
## Skill bindings
|
|
105
|
+
|
|
106
|
+
Bindings inspect the `name` or `skill` argument of a successful tool named
|
|
107
|
+
`skill`:
|
|
108
|
+
|
|
109
|
+
```yaml
|
|
110
|
+
skillBindings:
|
|
111
|
+
- skill: browser-automation
|
|
112
|
+
groups: [browser]
|
|
113
|
+
- skill: database-operations
|
|
114
|
+
groups: [database]
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Stable mode marks bound tools as dispatchable. The Skill instructions should
|
|
118
|
+
describe the tools and their arguments; otherwise call `tool_search` to load
|
|
119
|
+
the exact definitions into history. Dynamic mode activates the bound native
|
|
120
|
+
families under its normal cap, budget, and LRU rules.
|
|
121
|
+
|
|
122
|
+
## Discovery gate
|
|
123
|
+
|
|
124
|
+
With `requireDiscovery: true`, `tool_dispatch` accepts names that were matched
|
|
125
|
+
by a successful search, listed as a family sibling of a match, or introduced
|
|
126
|
+
through a Skill binding. This catches guessed or stale tool names before
|
|
127
|
+
nested execution.
|
|
128
|
+
|
|
129
|
+
A `status` listing is browse-only by default: it shows every family and member
|
|
130
|
+
name but does not unlock dispatch, so the model must load a schema before
|
|
131
|
+
calling. The rejection message points to the deterministic recovery — search
|
|
132
|
+
the exact name once (exact matches always rank first) and dispatch. Set
|
|
133
|
+
`statusGrantsDiscovery: true` to let one status call unlock the whole catalog;
|
|
134
|
+
this trades away the seen-schema guarantee, so reserve it for deployments with
|
|
135
|
+
approval layers or read-only tool surfaces.
|
|
136
|
+
|
|
137
|
+
Set `requireDiscovery: false` only when another trusted catalog supplies exact
|
|
138
|
+
names and schemas. Direct calls to deferred names remain denied; they must
|
|
139
|
+
still pass through `tool_dispatch`.
|
|
140
|
+
|
|
141
|
+
## Guidance deferral
|
|
142
|
+
|
|
143
|
+
`deferToolGuidance: true` removes only sections whose names map exactly to a
|
|
144
|
+
deferred tool:
|
|
145
|
+
|
|
146
|
+
```text
|
|
147
|
+
tool:<deferred-name>
|
|
148
|
+
tool:<deferred-name>:<suffix>
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
It does not guess package ownership or delete general sections. This avoids
|
|
152
|
+
silently removing unrelated policy text. A tool package that registers one
|
|
153
|
+
shared family section should move large operational instructions into a Skill
|
|
154
|
+
or keep the section always visible.
|
|
155
|
+
|
|
156
|
+
## Onboarding an existing plugin ecosystem
|
|
157
|
+
|
|
158
|
+
Three configuration moves cover most friction when this plugin fronts an
|
|
159
|
+
already-installed tool ecosystem:
|
|
160
|
+
|
|
161
|
+
1. Add high-frequency small tools to `alwaysVisible`. Anything the model calls
|
|
162
|
+
nearly every turn (todo updates, reporting) should not pay the
|
|
163
|
+
search-then-dispatch round trip.
|
|
164
|
+
2. Bind plugin-shipped Skills with `skillBindings`. Loading a Skill should make
|
|
165
|
+
its tools dispatchable in the same turn; without a binding the model still
|
|
166
|
+
needs one search after the Skill call.
|
|
167
|
+
3. Write explicit `groups` rules, with multilingual aliases, for packages whose
|
|
168
|
+
tool names do not follow common prefixes. This fixes automatic-group
|
|
169
|
+
scatter, improves family search recall, and keeps family-wide discovery
|
|
170
|
+
from unlocking strangers.
|
|
171
|
+
|
|
172
|
+
## Token estimates
|
|
173
|
+
|
|
174
|
+
The estimate for one tool is:
|
|
175
|
+
|
|
176
|
+
```text
|
|
177
|
+
ceil(JSON.stringify(schema).length / charactersPerToken)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
It is deterministic diagnostic data, not provider billing. In stable mode,
|
|
181
|
+
`estimatedSavedTokens` is the complete deferred catalog estimate because none
|
|
182
|
+
of those definitions appears in the top-level request. Search results add only
|
|
183
|
+
the matched definitions to history.
|
|
184
|
+
|
|
185
|
+
## Dynamic-mode budgets
|
|
186
|
+
|
|
187
|
+
When a dynamic activation exceeds `maxActiveGroups` or
|
|
188
|
+
`maxActiveToolTokens`, the least recently used unprotected family is removed
|
|
189
|
+
first. Families activated by the current search are protected for that
|
|
190
|
+
operation. A requested family that alone exceeds the budget remains active and
|
|
191
|
+
reports the over-budget estimate.
|
|
192
|
+
|
|
193
|
+
Dynamic mode changes the request tool set on activation, eviction, and expiry.
|
|
194
|
+
Prefer stable mode when context-cache reuse is important.
|
|
195
|
+
|
|
196
|
+
## Full stable example
|
|
197
|
+
|
|
198
|
+
```yaml
|
|
199
|
+
- id: tokens-progressive-tools
|
|
200
|
+
config:
|
|
201
|
+
mode: stable-proxy
|
|
202
|
+
toolName: tool_search
|
|
203
|
+
dispatchToolName: tool_dispatch
|
|
204
|
+
maxResults: 5
|
|
205
|
+
requireDiscovery: true
|
|
206
|
+
statusGrantsDiscovery: false
|
|
207
|
+
deferToolGuidance: true
|
|
208
|
+
alwaysVisible: [read, write, edit, glob, grep, bash, skill, ask_user_question, todo_write, dsh_im_return_file, report, submit_*, structured_output*]
|
|
209
|
+
groups:
|
|
210
|
+
- id: browser
|
|
211
|
+
description: Browser navigation and page interaction
|
|
212
|
+
aliases: [browser, web page, 浏览器]
|
|
213
|
+
include: [browser_*]
|
|
214
|
+
- id: database
|
|
215
|
+
description: Database inspection and queries
|
|
216
|
+
aliases: [database, sql, 数据库]
|
|
217
|
+
include: [db_*, sql_*]
|
|
218
|
+
skillBindings:
|
|
219
|
+
- skill: browser-automation
|
|
220
|
+
groups: [browser]
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## v0.1 migration
|
|
224
|
+
|
|
225
|
+
Version 0.2 defaults to `stable-proxy`, so a prior configuration now returns
|
|
226
|
+
exact definitions and uses `tool_dispatch` instead of exposing a native family.
|
|
227
|
+
|
|
228
|
+
To retain v0.1 call semantics while taking the lifecycle fixes:
|
|
229
|
+
|
|
230
|
+
```yaml
|
|
231
|
+
- id: tokens-progressive-tools
|
|
232
|
+
config:
|
|
233
|
+
mode: dynamic
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Dynamic mode now affects the first request and makes a successful search
|
|
237
|
+
visible on the immediately following request, but it remains intentionally
|
|
238
|
+
cache-hostile when the active family set changes.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Progressive disclosure model
|
|
2
|
+
|
|
3
|
+
This plugin separates two related mechanisms that are often conflated:
|
|
4
|
+
|
|
5
|
+
- Agent Skills progressively load instructions and resources.
|
|
6
|
+
- Tool discovery progressively loads callable schemas.
|
|
7
|
+
|
|
8
|
+
DeepSeek Harness already owns the first mechanism. This plugin supplies a
|
|
9
|
+
cache-stable fallback for the second where the provider protocol has no native
|
|
10
|
+
deferred-tool content blocks.
|
|
11
|
+
|
|
12
|
+
## Skills layers
|
|
13
|
+
|
|
14
|
+
The open Agent Skills format defines three disclosure levels:
|
|
15
|
+
|
|
16
|
+
| Level | Session-visible material | Load boundary |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| Metadata | `name` and `description` | Skill catalog publication |
|
|
19
|
+
| Instructions | Complete `SKILL.md` body | `skill` activation |
|
|
20
|
+
| Resources | Referenced scripts, files, and assets | Explicit need |
|
|
21
|
+
|
|
22
|
+
DSH's Skills subsystem already publishes a compact name/description catalog,
|
|
23
|
+
loads the complete body through the `skill` tool, and resolves resources only
|
|
24
|
+
when needed. The plugin does not duplicate or replace that subsystem.
|
|
25
|
+
|
|
26
|
+
References:
|
|
27
|
+
|
|
28
|
+
- [Agent Skills specification](https://github.com/agentskills/agentskills/blob/main/docs/specification.mdx)
|
|
29
|
+
- [DSH Skills subsystem](https://deepseek-harness.github.io/deepseek-harness/reference/subsystems/skills)
|
|
30
|
+
|
|
31
|
+
## Tool layers
|
|
32
|
+
|
|
33
|
+
Stable proxy mode implements an analogous three-level tool path:
|
|
34
|
+
|
|
35
|
+
| Level | Session-visible material | Load boundary |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| Discovery entry | `tool_search`, `tool_dispatch`, common direct tools | First request |
|
|
38
|
+
| Exact definitions | Matching names, descriptions, and parameter schemas | `tool_search` result |
|
|
39
|
+
| Execution | Original tool body and result | `tool_dispatch` nested call |
|
|
40
|
+
|
|
41
|
+
The complete catalog and real executors stay in process memory. Only exact
|
|
42
|
+
matches enter conversation history, but each match also names every sibling in
|
|
43
|
+
its family, so one search opens a plugin's whole dispatchable surface. The
|
|
44
|
+
`status` action browses the complete family catalog when no lexical query
|
|
45
|
+
fits; by default it informs without unlocking dispatch.
|
|
46
|
+
|
|
47
|
+
## Native deferred tools versus stable proxy
|
|
48
|
+
|
|
49
|
+
A provider-native deferred-tool protocol can receive all definitions out of
|
|
50
|
+
band, initially render only non-deferred tools, and later return typed tool
|
|
51
|
+
references without changing its cache prefix. DSH's current generic
|
|
52
|
+
`ToolSchema` contains only `name`, `description`, and `parameters`; the DeepSeek
|
|
53
|
+
Chat Completions tool protocol also has no equivalent deferred-reference block.
|
|
54
|
+
|
|
55
|
+
Stable proxy therefore keeps two generic schemas fixed and performs real-tool
|
|
56
|
+
validation at dispatch time. This preserves prefix stability and ordinary DSH
|
|
57
|
+
policy, with two explicit trade-offs:
|
|
58
|
+
|
|
59
|
+
- the outer request has no provider-native grammar for a deferred tool;
|
|
60
|
+
- discovery and execution are separate calls.
|
|
61
|
+
|
|
62
|
+
When DSH adds provider capability negotiation and native deferred references,
|
|
63
|
+
a future native mode can use those features without changing the stable proxy
|
|
64
|
+
fallback.
|
|
65
|
+
|
|
66
|
+
References:
|
|
67
|
+
|
|
68
|
+
- [DSH tool subsystem](https://deepseek-harness.github.io/deepseek-harness/reference/subsystems/tools)
|
|
69
|
+
- [DeepSeek Chat Completions](https://api-docs.deepseek.com/api/create-chat-completion/)
|
|
70
|
+
|
|
71
|
+
## Cache contract
|
|
72
|
+
|
|
73
|
+
The stable request prefix is:
|
|
74
|
+
|
|
75
|
+
```text
|
|
76
|
+
fixed tool schemas → fixed system sections → append-only message history
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Discovery adds one tool result after the reusable prefix. It does not change
|
|
80
|
+
the fixed schemas or the generated Code Mode SDK. This is the central
|
|
81
|
+
difference from `dynamic` mode, whose native family activation intentionally
|
|
82
|
+
changes the request header.
|
package/lib/catalog.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DeferredToolMatch, SearchMatch, ToolCatalog, ToolGroupConfig, ToolSchemaView } from './types.js';
|
|
2
|
+
export declare function matchesToolName(name: string, patterns: readonly string[]): boolean;
|
|
3
|
+
export declare function estimateSchemaTokens(schema: ToolSchemaView, charactersPerToken: number): number;
|
|
4
|
+
export declare function buildCatalog(schemas: readonly ToolSchemaView[], configuredGroups: readonly ToolGroupConfig[], charactersPerToken: number, excludedNames?: ReadonlySet<string>): ToolCatalog;
|
|
5
|
+
export declare function searchCatalog(catalog: ToolCatalog, query: string, limit: number): SearchMatch[];
|
|
6
|
+
/**
|
|
7
|
+
* Rank individual tools with exact-name bonuses and a compact BM25-style score.
|
|
8
|
+
* Definitions, parameter descriptions, enums, and nested property names all
|
|
9
|
+
* participate in the searchable document.
|
|
10
|
+
*/
|
|
11
|
+
export declare function searchTools(catalog: ToolCatalog, query: string, limit: number): DeferredToolMatch[];
|
|
12
|
+
export declare function matchingToolNames(catalog: ToolCatalog, patterns: readonly string[]): string[];
|