@fishwithcat/dsh-tool-ask-user 0.0.0-stage → 0.2.2-alpha.3
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/README.i18n.yaml +76 -0
- package/README.md +168 -2
- package/README.zh.md +169 -0
- package/lib/index.js +468 -0
- package/lib/types/index.d.ts +22 -0
- package/lib/types/presentation.d.ts +43 -0
- package/lib/types/timed.d.ts +13 -0
- package/locale/de.json +6 -0
- package/locale/en.json +5 -0
- package/locale/es.json +6 -0
- package/locale/fr.json +6 -0
- package/locale/ja.json +6 -0
- package/locale/pt.json +6 -0
- package/locale/ru.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +48 -4
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading
|
|
2
|
+
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
|
3
|
+
# After editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/interaction/tool-ask-user/README.md
|
|
5
|
+
/:
|
|
6
|
+
en: fd744e4beac0ee66
|
|
7
|
+
zh: 27288856740f5dbf
|
|
8
|
+
/deepseek-ai-dsh-tool-ask-user:
|
|
9
|
+
en: 2a561a70c4a900d7
|
|
10
|
+
zh: 3d12372992cf2634
|
|
11
|
+
/deepseek-ai-dsh-tool-ask-user/summary:
|
|
12
|
+
en: 1a9a38698a6cb1f3
|
|
13
|
+
zh: 35540765df5648e7
|
|
14
|
+
/deepseek-ai-dsh-tool-ask-user/table-of-contents:
|
|
15
|
+
en: d152484eb41ac6b4
|
|
16
|
+
zh: 09388d293f9be9cb
|
|
17
|
+
/deepseek-ai-dsh-tool-ask-user/use-this-package:
|
|
18
|
+
en: 547a3ea61708bc3e
|
|
19
|
+
zh: 5e251ea392940c1a
|
|
20
|
+
/deepseek-ai-dsh-tool-ask-user/use-this-package/when-to-call-the-tool:
|
|
21
|
+
en: 7c70d43d4cd9aa19
|
|
22
|
+
zh: 7597e972067ec5fe
|
|
23
|
+
/deepseek-ai-dsh-tool-ask-user/use-this-package/what-the-model-gets-back:
|
|
24
|
+
en: a81de874c72ebabb
|
|
25
|
+
zh: 5f6e0bd719ee7123
|
|
26
|
+
/deepseek-ai-dsh-tool-ask-user/use-this-package/when-the-call-fails:
|
|
27
|
+
en: c4ac3c584f900230
|
|
28
|
+
zh: bd8d227f0637b8f1
|
|
29
|
+
/deepseek-ai-dsh-tool-ask-user/understand-the-implementation:
|
|
30
|
+
en: e9b9d46be0b45ca2
|
|
31
|
+
zh: c02403486473cd80
|
|
32
|
+
/deepseek-ai-dsh-tool-ask-user/understand-the-implementation/source-map:
|
|
33
|
+
en: d3b5b1515b9aa2f9
|
|
34
|
+
zh: 0b74abcb4da857aa
|
|
35
|
+
/deepseek-ai-dsh-tool-ask-user/understand-the-implementation/consumer-role:
|
|
36
|
+
en: 7de88d25287b969f
|
|
37
|
+
zh: be36dc5a39aeac3b
|
|
38
|
+
/deepseek-ai-dsh-tool-ask-user/understand-the-implementation/result-rendering:
|
|
39
|
+
en: 361ec8067ee6a6e6
|
|
40
|
+
zh: 4ea4dab0da7f79f9
|
|
41
|
+
/deepseek-ai-dsh-tool-ask-user/further-exploration:
|
|
42
|
+
en: d66802e3991ece30
|
|
43
|
+
zh: fcd59f390f2ee690
|
|
44
|
+
/deepseek-ai-dsh-tool-ask-user/model-experience:
|
|
45
|
+
en: 215e7ba838619b7b
|
|
46
|
+
zh: a311da3843709f90
|
|
47
|
+
/deepseek-ai-dsh-tool-ask-user/model-experience/tool-schema:
|
|
48
|
+
en: e4c7376d4f29dbd5
|
|
49
|
+
zh: 6b7a9eaf69c9ea53
|
|
50
|
+
/deepseek-ai-dsh-tool-ask-user/model-experience/tool-schema/what-the-model-sees:
|
|
51
|
+
en: 577c8609a8bb915d
|
|
52
|
+
zh: 8607e0d39be0e492
|
|
53
|
+
/deepseek-ai-dsh-tool-ask-user/model-experience/tool-schema/token-effect:
|
|
54
|
+
en: 97cce067584eadff
|
|
55
|
+
zh: c376cab39ea9df68
|
|
56
|
+
/deepseek-ai-dsh-tool-ask-user/model-experience/tool-schema/kv-cache-effect:
|
|
57
|
+
en: b385b04c94496e5b
|
|
58
|
+
zh: 6bd8f2bc39e6f8c0
|
|
59
|
+
/deepseek-ai-dsh-tool-ask-user/model-experience/tool-call-history-and-result:
|
|
60
|
+
en: 9717b983b6a857e3
|
|
61
|
+
zh: 611a2dadf7c0c706
|
|
62
|
+
/deepseek-ai-dsh-tool-ask-user/model-experience/tool-call-history-and-result/what-the-model-sees:
|
|
63
|
+
en: c2ceed515656e10b
|
|
64
|
+
zh: 4e9b6768e4914718
|
|
65
|
+
/deepseek-ai-dsh-tool-ask-user/model-experience/tool-call-history-and-result/token-effect:
|
|
66
|
+
en: 3092d4afd4ba8678
|
|
67
|
+
zh: cb7a7bbefdbc3a1e
|
|
68
|
+
/deepseek-ai-dsh-tool-ask-user/model-experience/tool-call-history-and-result/kv-cache-effect:
|
|
69
|
+
en: f5d5f97a153f7c70
|
|
70
|
+
zh: 4db81881ebdcfcc4
|
|
71
|
+
/deepseek-ai-dsh-tool-ask-user/known-limitations-and-deferred-work:
|
|
72
|
+
en: c8df1368c2959465
|
|
73
|
+
zh: b14fb07a60e75ebf
|
|
74
|
+
/deepseek-ai-dsh-tool-ask-user/known-limitations-and-deferred-work/dev-note:
|
|
75
|
+
en: 0b06d843322610c1
|
|
76
|
+
zh: a1e7cadd4f03c384
|
package/README.md
CHANGED
|
@@ -1,3 +1,169 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
description: "The model-facing ask_user_question tool over the user-questions seam, for users and maintainers composing or debugging interactive agent surfaces."
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
# @deepseek-ai/dsh-tool-ask-user
|
|
7
|
+
|
|
8
|
+
English | [中文](README.zh.md)
|
|
9
|
+
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
`ask_user_question` asks the user for confirmation, a choice, or missing information. It waits for an answer by default. With `mode: timed`, a deadline can release the model to continue independent work while the question stays answerable; `timeout: -1` waits indefinitely. A live child agent cannot call the tool. Callers provide the answer UI.
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
|
|
16
|
+
- [Use this package](#use-this-package)
|
|
17
|
+
- [Understand the implementation](#understand-the-implementation)
|
|
18
|
+
- [Further Exploration](#further-exploration)
|
|
19
|
+
- [Model Experience](#model-experience)
|
|
20
|
+
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
21
|
+
- [Dev Note](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## Use this package
|
|
27
|
+
|
|
28
|
+
Compose this plugin with `ctx.userQuestions` when the model needs a user decision. Without an answerer, blocking calls fail; finite timed calls return pending at their deadline.
|
|
29
|
+
|
|
30
|
+
Shipped presets use blocking mode. To enable timed mode, set `mode: timed` on the `tool-ask-user` row in the active preset's `config.plugins` list:
|
|
31
|
+
|
|
32
|
+
```yaml
|
|
33
|
+
- id: tool-ask-user
|
|
34
|
+
name: '@deepseek-ai/dsh-tool-ask-user'
|
|
35
|
+
config:
|
|
36
|
+
mode: timed
|
|
37
|
+
timeout: 120
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
This is a plugin row, not a top-level `--patch` entry. In the Web profile, change the `preset-standard` plugin list or use the Agent Preset editor. `mode: legacy` or omitted config keeps the blocking schema. The row's `timeout` applies to each timed call by default; `-1` waits indefinitely unless the model supplies a positive timeout. A model-supplied `-1` applies only to that call, including all its questions.
|
|
41
|
+
|
|
42
|
+
### When to call the tool
|
|
43
|
+
|
|
44
|
+
Send one or more questions with ids unique within the call; the answer echoes those ids. Put a recommended option first and append `(Recommended)` to its label. The optional per-call `timeout` is in seconds; use `-1` when work cannot safely continue without an answer. A timeout is never approval. In the Web card, editing or choosing Take time also holds that Client's wait until submission or cancellation.
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"questions": [
|
|
49
|
+
{
|
|
50
|
+
"id": "cleanup",
|
|
51
|
+
"question": "Proceed with the destructive cleanup?",
|
|
52
|
+
"header": "Confirm",
|
|
53
|
+
"options": [
|
|
54
|
+
{ "label": "Yes, delete them (Recommended)", "description": "Removes the three stale files." },
|
|
55
|
+
{ "label": "No, keep them", "description": "Aborts the cleanup." }
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### What the model gets back
|
|
63
|
+
|
|
64
|
+
An answer returns one item per question. `selected` holds option labels; `custom` supplements a multi-select answer or replaces a single-select choice. A skipped item has empty `selected` and no `custom`; both tool descriptions tell the model how to handle it.
|
|
65
|
+
|
|
66
|
+
For a finite timed call, `{ "pending": true, "callId": "…" }` means the foreground wait ended without an answer. The question remains answerable, and the model may continue independent work. A later answer arrives as a user message identified by `kind`, `tool`, and `callId`. Web chat and the tool's presentation view pair its questions and answers; consumers that render the raw result receive compact JSON text.
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{ "answers": [{ "id": "cleanup", "selected": ["Yes, delete them (Recommended)"] }] }
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### When the call fails
|
|
73
|
+
|
|
74
|
+
Legacy and `timeout: -1` calls wait for an answer or cancellation; without an accepting answerer, they return an error. A finite timed call without an answerer returns pending at its deadline. Cancellation and a caller that is not the exact live runtime root also return errors. A live child agent is rejected with `DELEGATED_CALLER` and must include the unresolved decision in its final result.
|
|
75
|
+
|
|
76
|
+
-----
|
|
77
|
+
|
|
78
|
+
<a id="understand-the-implementation"></a>
|
|
79
|
+
## Understand the implementation
|
|
80
|
+
|
|
81
|
+
<details>
|
|
82
|
+
<summary>Implementation internals — click to expand</summary>
|
|
83
|
+
|
|
84
|
+
The observable behavior is covered in [Use this package](#use-this-package); this section explains the tool definition and its relationship to the seam.
|
|
85
|
+
|
|
86
|
+
### Source map
|
|
87
|
+
|
|
88
|
+
| File | Role |
|
|
89
|
+
|---|---|
|
|
90
|
+
| [`src/index.ts`](src/index.ts) | Default blocking tool definition and mode selection |
|
|
91
|
+
| [`src/timed.ts`](src/timed.ts) | Opt-in timed tool definition and result rendering |
|
|
92
|
+
| [`src/presentation.ts`](src/presentation.ts) | Question-and-answer views for a host's call and result rows |
|
|
93
|
+
|
|
94
|
+
### Consumer role
|
|
95
|
+
|
|
96
|
+
The plugin registers one tool definition. Legacy mode calls `ask()`; timed mode calls `askTimed()` for positive timeouts and `ask()` for `-1`. Both forward the calling agent and turn signal to `ctx.userQuestions`. Timed requests include the tool call id in `wait`, so a Client can reopen their card. The projection identifies timed native calls from the `timeout` field in the recorded tool schema, even when a call omits that argument.
|
|
97
|
+
|
|
98
|
+
### Result rendering
|
|
99
|
+
|
|
100
|
+
The `render` output projects the structured value to a single text block via `JSON.stringify`, which is why the model-facing result is compact JSON rather than a richer content-block vocabulary. `presentCall` and `presentResult` separately declare Host views that list each question with the labels the user chose, or with a skip marker, so a terminal or editor row never has to show the answer JSON to its reader.
|
|
101
|
+
|
|
102
|
+
</details>
|
|
103
|
+
|
|
104
|
+
-----
|
|
105
|
+
|
|
106
|
+
<a id="further-exploration"></a>
|
|
107
|
+
## Further Exploration
|
|
108
|
+
|
|
109
|
+
Read these pages when the package-level contract is not enough. They move from the tool surface to the seam contract and its answerer waterfall.
|
|
110
|
+
|
|
111
|
+
- [User interaction subsystem reference](../../../docs/subsystems/user-questions.md) — the service contract, question vocabulary, and answerer waterfall behind this tool.
|
|
112
|
+
- [Tool catalog](../../../docs/tool-catalog.md#deepseek-aidsh-tool-ask-user) — the generated `ask_user_question` schema.
|
|
113
|
+
- [user-questions package](../user-questions/README.md) — the seam this tool consumes.
|
|
114
|
+
- [Interaction group map](../README.md) — adjacent approval and command surfaces.
|
|
115
|
+
|
|
116
|
+
-----
|
|
117
|
+
|
|
118
|
+
<a id="model-experience"></a>
|
|
119
|
+
## Model Experience
|
|
120
|
+
|
|
121
|
+
### Tool schema
|
|
122
|
+
|
|
123
|
+
#### What the model sees
|
|
124
|
+
|
|
125
|
+
The shipped presets expose the original blocking [`ask_user_question` schema](../../../docs/tool-catalog.md#deepseek-aidsh-tool-ask-user). A custom Cordis row with `mode: timed` switches to the alternate schema, including question ids, prompts, headings, options, multi-select flags, `timeout`, and the pending result; the model sees only the selected definition.
|
|
126
|
+
|
|
127
|
+
#### Token effect
|
|
128
|
+
|
|
129
|
+
Fixed schema cost on every request where the tool is visible.
|
|
130
|
+
|
|
131
|
+
#### KV Cache effect
|
|
132
|
+
|
|
133
|
+
Prefix-stable while the definition and visibility are unchanged. Plugin lifecycle or scoped restrictions may invalidate reuse from this schema.
|
|
134
|
+
|
|
135
|
+
### Tool-call history and result
|
|
136
|
+
|
|
137
|
+
#### What the model sees
|
|
138
|
+
|
|
139
|
+
The assistant tool call retains the questions. An answer during the wait appears in the next step as compact `{"answers":[{"id":"<id>","selected":["<label>"],"custom":"<text>"}]}` JSON; unused `custom` is omitted. A timed-out call returns `{"pending":true,"callId":"<pending-call-id>","message":"<instruction>"}`. A later answer arrives as a user message with `kind: "answer_to_pending_question"`, `tool: "ask_user_question"`, the call id, the original questions, and the answers. UI activity before submission is not model context.
|
|
140
|
+
|
|
141
|
+
#### Token effect
|
|
142
|
+
|
|
143
|
+
Arguments and answer JSON are data-dependent retained tokens; there is no token cost while waiting for the human.
|
|
144
|
+
|
|
145
|
+
#### KV Cache effect
|
|
146
|
+
|
|
147
|
+
Append-only; newly visible content follows the reusable request prefix and does not invalidate existing KV-cache entries.
|
|
148
|
+
|
|
149
|
+
## Known Limitations and Deferred Work
|
|
150
|
+
|
|
151
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
These limits define when the tool is a poor fit. They are current package constraints, not a UI backlog.
|
|
155
|
+
|
|
156
|
+
- **The legacy tool never reports pending** — it returns an answer or an error. Native legacy calls do not enter the `userQuestions` projection, and interrupted calls cannot take late answers.
|
|
157
|
+
- **An interrupted PTC wait may lose its question** — if the `run_code` process ends before an `ask_user_question` sub-call records its `tool/ptc-dispatch` result, the projection cannot reconstruct that sub-call for a late answer.
|
|
158
|
+
- **Runtime-owned subagents cannot ask the user** — `ask_user_question` rejects a live child owned by another agent with `DELEGATED_CALLER`; the child must include the unresolved question or decision in its final result. Durable lineage does not decide this boundary, so a lineage-bearing session resumed as a runtime root may ask normally.
|
|
159
|
+
- **Native answers render as JSON text** — the canonical value remains structured, but the model-facing result uses compact JSON rather than a richer content-block vocabulary.
|
|
160
|
+
|
|
161
|
+
<a id="dev-note"></a>
|
|
162
|
+
### Dev Note
|
|
163
|
+
|
|
164
|
+
<details>
|
|
165
|
+
<summary>Working context for maintainers — click to expand</summary>
|
|
166
|
+
|
|
167
|
+
The [`userQuestions` projection](../user-questions/src/projection.ts) reads each recorded tool schema rather than the call arguments: timed calls may omit `timeout`, and timed `-1` calls still use the timed schema. Changes to the projection fold require a `stateVersion` bump so stored caches refold from the log.
|
|
168
|
+
|
|
169
|
+
</details>
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "基于 user-questions seam 的模型侧 ask_user_question 工具;供组合或排查交互式 agent(智能体)表面的用户与维护者阅读。"
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @deepseek-ai/dsh-tool-ask-user
|
|
7
|
+
|
|
8
|
+
[English](README.md) | 中文
|
|
9
|
+
|
|
10
|
+
## 概述
|
|
11
|
+
|
|
12
|
+
`ask_user_question` 向用户请求确认、选择或缺失的信息。默认情况下,它会等待回答。设置 `mode: timed` 后,期限到达时模型可以继续独立工作,而问题仍可回答;`timeout: -1` 则无限期等待。存活的子 agent 不能调用此工具。调用方需提供回答界面。
|
|
13
|
+
|
|
14
|
+
## 目录
|
|
15
|
+
|
|
16
|
+
- [使用本包](#use-this-package)
|
|
17
|
+
- [理解实现](#understand-the-implementation)
|
|
18
|
+
- [进一步探索](#further-exploration)
|
|
19
|
+
- [模型体验](#model-experience)
|
|
20
|
+
- [已知限制与延期工作](#known-limitations-and-deferred-work)
|
|
21
|
+
- [开发备注](#dev-note)
|
|
22
|
+
|
|
23
|
+
-----
|
|
24
|
+
|
|
25
|
+
<a id="use-this-package"></a>
|
|
26
|
+
## 使用本包
|
|
27
|
+
|
|
28
|
+
当模型需要用户决定时,将此插件与 `ctx.userQuestions` 组合。没有 answerer 时,阻塞式调用返回错误;有限时长的 timed 调用到期后返回 pending。
|
|
29
|
+
|
|
30
|
+
随附 preset 使用阻塞模式。要启用 timed 模式,请在当前 preset 的 `config.plugins` 列表中,将 `tool-ask-user` 条目设为 `mode: timed`:
|
|
31
|
+
|
|
32
|
+
```yaml
|
|
33
|
+
- id: tool-ask-user
|
|
34
|
+
name: '@deepseek-ai/dsh-tool-ask-user'
|
|
35
|
+
config:
|
|
36
|
+
mode: timed
|
|
37
|
+
timeout: 120
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
以上片段是插件条目,不是顶层 `--patch` 条目。在 Web profile 中,请修改 `preset-standard` 的插件列表,或使用 Agent Preset 编辑器。`mode: legacy` 或省略配置会保留阻塞式 schema。条目中的 `timeout` 默认适用于每次 timed 调用;设为 `-1` 时会无限期等待,除非模型传入正数期限。模型传入的 `-1` 只适用于该次调用,包括其中的所有问题。
|
|
41
|
+
|
|
42
|
+
### 何时调用该工具
|
|
43
|
+
|
|
44
|
+
发送一个或多个问题,每个问题的 `id` 在本次调用内必须唯一;回答会带回这些 id。推荐选项放在首位,并在标签末尾追加 `(Recommended)`。可选的单次调用 `timeout` 以秒为单位;如果没有回答就无法安全继续,请使用 `-1`。超时绝不表示批准。在 Web 卡片中,开始编辑或选择“慢慢回答”也会让该 Client 的等待持续到提交或取消。
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"questions": [
|
|
49
|
+
{
|
|
50
|
+
"id": "cleanup",
|
|
51
|
+
"question": "Proceed with the destructive cleanup?",
|
|
52
|
+
"header": "Confirm",
|
|
53
|
+
"options": [
|
|
54
|
+
{ "label": "Yes, delete them (Recommended)", "description": "Removes the three stale files." },
|
|
55
|
+
{ "label": "No, keep them", "description": "Aborts the cleanup." }
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 模型得到什么
|
|
63
|
+
|
|
64
|
+
收到回答时,工具为每个问题返回一项。`selected` 保存选项标签;`custom` 可以补充多选答案,或替代单选选项。跳过的回答项具有空 `selected`,且没有 `custom`;两种工具描述都告诉模型如何处理这种回答项。
|
|
65
|
+
|
|
66
|
+
对于有限时长的 timed 调用,`{ "pending": true, "callId": "…" }` 表示前台等待结束时仍没有回答。问题仍可回答,模型可以继续独立工作。之后的回答以用户消息送达,由 `kind`、`tool` 和 `callId` 标识。Web 对话与工具声明的呈现视图会配对展示问题与回答;直接渲染原始结果的消费方收到紧凑 JSON 文本。
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{ "answers": [{ "id": "cleanup", "selected": ["Yes, delete them (Recommended)"] }] }
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 调用何时失败
|
|
73
|
+
|
|
74
|
+
Legacy 调用和 `timeout: -1` 调用会等待回答或取消;没有 answerer 接受时返回错误。有限时长的 timed 调用若没有 answerer,则在到期后返回 pending。取消调用,或调用方不是确切的存活运行时根时,也会返回错误。存活的子 agent 会以 `DELEGATED_CALLER` 被拒绝,必须在最终结果中包含尚未解决的决定。
|
|
75
|
+
|
|
76
|
+
-----
|
|
77
|
+
|
|
78
|
+
<a id="understand-the-implementation"></a>
|
|
79
|
+
## 理解实现
|
|
80
|
+
|
|
81
|
+
<details>
|
|
82
|
+
<summary>实现细节——点击展开</summary>
|
|
83
|
+
|
|
84
|
+
可观察行为已在[使用本包](#use-this-package)中说明;本节解释工具定义及其与 seam 的关系。
|
|
85
|
+
|
|
86
|
+
### 源码地图
|
|
87
|
+
|
|
88
|
+
| 文件 | 职责 |
|
|
89
|
+
|---|---|
|
|
90
|
+
| [`src/index.ts`](src/index.ts) | 默认阻塞式工具定义与模式选择 |
|
|
91
|
+
| [`src/timed.ts`](src/timed.ts) | 显式启用的计时工具定义与结果渲染 |
|
|
92
|
+
| [`src/presentation.ts`](src/presentation.ts) | 供宿主调用行与结果行使用的问题与回答视图 |
|
|
93
|
+
|
|
94
|
+
### 消费方角色
|
|
95
|
+
|
|
96
|
+
插件只注册一个工具定义。Legacy 模式调用 `ask()`;timed 模式对正数期限调用 `askTimed()`,对 `-1` 调用 `ask()`。两者都向 `ctx.userQuestions` 传递调用方 agent 和轮次信号。Timed 请求在 `wait` 中包含工具调用 id,Client 因此可以重新打开对应卡片。投影从记录的工具 schema 的 `timeout` 字段识别 timed 原生调用,即使某次调用省略该参数也一样。
|
|
97
|
+
|
|
98
|
+
### 结果渲染
|
|
99
|
+
|
|
100
|
+
`render` 输出把结构化值经 `JSON.stringify` 投影为单个文本块,因此模型侧结果是紧凑 JSON,而非更丰富的内容块词汇。
|
|
101
|
+
|
|
102
|
+
</details>
|
|
103
|
+
|
|
104
|
+
-----
|
|
105
|
+
|
|
106
|
+
<a id="further-exploration"></a>
|
|
107
|
+
## 进一步探索
|
|
108
|
+
|
|
109
|
+
当包级约定不够用时阅读以下页面。它们从工具表面逐步进入 seam 约定及其 answerer waterfall。
|
|
110
|
+
|
|
111
|
+
- [用户交互子系统参考](../../../docs/subsystems/user-questions.zh.md)——此工具背后的服务约定、问题词汇与 answerer waterfall。
|
|
112
|
+
- [工具目录](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-ask-user)——生成的 `ask_user_question` schema。
|
|
113
|
+
- [user-questions 包](../user-questions/README.zh.md)——本工具消费的 seam。
|
|
114
|
+
- [交互组映射](../README.zh.md)——相邻的审批与命令表面。
|
|
115
|
+
|
|
116
|
+
-----
|
|
117
|
+
|
|
118
|
+
<a id="model-experience"></a>
|
|
119
|
+
## 模型体验
|
|
120
|
+
|
|
121
|
+
### 工具 schema
|
|
122
|
+
|
|
123
|
+
#### 模型看到的内容
|
|
124
|
+
|
|
125
|
+
随附 preset 会公开原有的阻塞式 [`ask_user_question` schema](../../../docs/tool-catalog.zh.md#deepseek-aidsh-tool-ask-user)。自定义 Cordis 行设置 `mode: timed` 后,会切换到包含问题 id、提示语、标题、选项、多选标志、`timeout` 与待处理结果的异步 schema;模型只会看到被选中的定义。
|
|
126
|
+
|
|
127
|
+
#### Token 影响
|
|
128
|
+
|
|
129
|
+
工具可见时,每个请求都会产生固定的 schema token 开销。
|
|
130
|
+
|
|
131
|
+
#### KV Cache 影响
|
|
132
|
+
|
|
133
|
+
只要定义和可见性保持不变,前缀即可稳定复用。插件生命周期变化或作用域限制可能会使从此 schema 起的缓存复用失效。
|
|
134
|
+
|
|
135
|
+
### 工具调用历史与结果
|
|
136
|
+
|
|
137
|
+
#### 模型看到的内容
|
|
138
|
+
|
|
139
|
+
assistant 工具调用保留问题。等待期间收到的回答会在下一步显示为紧凑的 `{"answers":[{"id":"<id>","selected":["<label>"],"custom":"<text>"}]}` JSON;未使用的 `custom` 会省略。计时到期的调用返回 `{"pending":true,"callId":"<pending-call-id>","message":"<instruction>"}`。之后的回答以用户消息送达,包含 `kind: "answer_to_pending_question"`、`tool: "ask_user_question"`、调用 id、原问题和答案。提交之前的 UI 活动不属于模型上下文。
|
|
140
|
+
|
|
141
|
+
#### Token 影响
|
|
142
|
+
|
|
143
|
+
参数和回答 JSON 是依数据而定的保留 token;等待用户时不会产生 token 开销。
|
|
144
|
+
|
|
145
|
+
#### KV Cache 影响
|
|
146
|
+
|
|
147
|
+
仅追加;新出现的可见内容位于可复用请求前缀之后,不会使现有 KV Cache 条目失效。
|
|
148
|
+
|
|
149
|
+
## 已知限制与延期工作
|
|
150
|
+
|
|
151
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
这些限制说明该工具何时不合适。它们是当前包约束,不是 UI 积压事项。
|
|
155
|
+
|
|
156
|
+
- **Legacy 工具不会报告 pending**:它只返回回答或错误。原生 legacy 调用不进入 `userQuestions` 投影;中断的调用不能接受迟到回答。
|
|
157
|
+
- **中断的 PTC 等待可能丢失问题**:若 `run_code` 进程在 `ask_user_question` 子调用记录 `tool/ptc-dispatch` 结果前结束,投影无法重建该子调用以接受迟到回答。
|
|
158
|
+
- **运行时中归属于其他 agent 的 subagent 不能向用户提问**:`ask_user_question` 会以 `DELEGATED_CALLER` 拒绝归属于另一个 agent 的存活子级;该子级必须在最终结果中包含尚未解决的问题或决定。持久谱系不能决定这一边界,因此带有谱系的会话恢复为运行时根后可以正常提问。
|
|
159
|
+
- **Native 回答渲染为 JSON 文本**:规范值仍为结构化数据,但模型侧结果使用紧凑 JSON,而非更丰富的内容块词汇。
|
|
160
|
+
|
|
161
|
+
<a id="dev-note"></a>
|
|
162
|
+
### 开发备注
|
|
163
|
+
|
|
164
|
+
<details>
|
|
165
|
+
<summary>维护者的工作上下文——点击展开</summary>
|
|
166
|
+
|
|
167
|
+
[`userQuestions` 投影](../user-questions/src/projection.ts)读取记录的工具 schema,而不是调用参数:timed 调用可以省略 `timeout`,timed `-1` 调用也仍使用 timed schema。改变投影折叠语义时必须提高 `stateVersion`,让持久化缓存从日志重新折叠。
|
|
168
|
+
|
|
169
|
+
</details>
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
2
|
+
import z from "@deepseek-ai/schemastery";
|
|
3
|
+
import { TIMED_WAIT_PARAMETER } from "@deepseek-ai/dsh-user-questions";
|
|
4
|
+
//#region lib/types/presentation.js
|
|
5
|
+
/**
|
|
6
|
+
* Host presentation for `ask_user_question`: the question batch and its answer
|
|
7
|
+
* batch as readable lines, instead of the model-facing JSON arguments and
|
|
8
|
+
* result. A host that renders tool presentation views shows the questions and
|
|
9
|
+
* what the user chose; the model-facing result stays compact JSON.
|
|
10
|
+
* @module @deepseek-ai/dsh-tool-ask-user/presentation
|
|
11
|
+
*/
|
|
12
|
+
/** Marker that names a question the user moved past without answering. */
|
|
13
|
+
const SKIPPED = " → (skipped)";
|
|
14
|
+
/**
|
|
15
|
+
* One question's display label: its header when it has one, then its text.
|
|
16
|
+
* @param question - the question to label.
|
|
17
|
+
* @returns the label on one line.
|
|
18
|
+
*/
|
|
19
|
+
function questionLabel(question) {
|
|
20
|
+
return question.header === void 0 ? question.question : `${question.header} · ${question.question}`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The questions as one text block, one question per line.
|
|
24
|
+
* @param questions - the questions to list.
|
|
25
|
+
* @returns the view content.
|
|
26
|
+
*/
|
|
27
|
+
function questionContent(questions) {
|
|
28
|
+
return [{
|
|
29
|
+
type: "text",
|
|
30
|
+
text: questions.map(questionLabel).join("\n")
|
|
31
|
+
}];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Concatenate one tool result's text blocks.
|
|
35
|
+
* @param blocks - the result content.
|
|
36
|
+
* @returns the visible text.
|
|
37
|
+
*/
|
|
38
|
+
function resultText(blocks) {
|
|
39
|
+
let text = "";
|
|
40
|
+
for (const block of blocks)
|
|
41
|
+
/* v8 ignore next -- the tool's render declares exactly one text block */
|
|
42
|
+
if (block.type === "text") text += block.text;
|
|
43
|
+
return text;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Whether a decoded JSON value is a non-null, non-array object.
|
|
47
|
+
* @param value - the decoded value.
|
|
48
|
+
* @returns true when named fields can be read from it.
|
|
49
|
+
*/
|
|
50
|
+
function isRecord(value) {
|
|
51
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Read the answer entries out of a successful result's JSON object.
|
|
55
|
+
*
|
|
56
|
+
* The result is a durable payload, so every field is checked before the row can
|
|
57
|
+
* present it as a choice the user made.
|
|
58
|
+
* @param parsed - the decoded result object.
|
|
59
|
+
* @returns the answer entries, or undefined when the payload is not one.
|
|
60
|
+
*/
|
|
61
|
+
function answerEntries(parsed) {
|
|
62
|
+
if (!Array.isArray(parsed.answers)) return void 0;
|
|
63
|
+
const answers = [];
|
|
64
|
+
for (const answer of parsed.answers) {
|
|
65
|
+
if (!isRecord(answer) || typeof answer.id !== "string" || !Array.isArray(answer.selected)) return void 0;
|
|
66
|
+
if (!answer.selected.every((label) => typeof label === "string")) return void 0;
|
|
67
|
+
if (answer.custom !== void 0 && typeof answer.custom !== "string") return void 0;
|
|
68
|
+
answers.push({
|
|
69
|
+
id: answer.id,
|
|
70
|
+
selected: answer.selected,
|
|
71
|
+
...answer.custom === void 0 ? {} : { custom: answer.custom }
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return answers;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The presentation view for a pending `ask_user_question` call.
|
|
78
|
+
*
|
|
79
|
+
* A batch renders as its questions, one per line, no matter how many arrived;
|
|
80
|
+
* the title stays the same so a reader recognizes the Tool across states.
|
|
81
|
+
* @param questions - the questions the call asks.
|
|
82
|
+
* @returns the generic call view.
|
|
83
|
+
*/
|
|
84
|
+
function presentAskCall(questions) {
|
|
85
|
+
return {
|
|
86
|
+
card: "generic",
|
|
87
|
+
title: "Ask user question",
|
|
88
|
+
content: questionContent(questions)
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The presentation view for a settled `ask_user_question` call.
|
|
93
|
+
*
|
|
94
|
+
* Each question is followed by the labels the user chose, or by a skip marker.
|
|
95
|
+
* A timed call whose window closed without an answer shows that it is still
|
|
96
|
+
* waiting; a failed call declares no view so the ordinary error row remains.
|
|
97
|
+
* @param questions - the questions the call asked.
|
|
98
|
+
* @param result - the settled result projection.
|
|
99
|
+
* @returns the generic result view, or undefined when the result is an error or
|
|
100
|
+
* carries no readable batch.
|
|
101
|
+
*/
|
|
102
|
+
function presentAskResult(questions, result) {
|
|
103
|
+
if (result.isError) return void 0;
|
|
104
|
+
const text = resultText(result.content);
|
|
105
|
+
let parsed;
|
|
106
|
+
try {
|
|
107
|
+
parsed = JSON.parse(text);
|
|
108
|
+
} catch {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (!isRecord(parsed)) return void 0;
|
|
112
|
+
if (parsed.pending === true) return {
|
|
113
|
+
card: "generic",
|
|
114
|
+
title: "Ask user question · waiting for an answer",
|
|
115
|
+
content: questionContent(questions)
|
|
116
|
+
};
|
|
117
|
+
const answers = answerEntries(parsed);
|
|
118
|
+
if (answers === void 0) return void 0;
|
|
119
|
+
const byId = new Map(answers.map((answer) => [answer.id, answer]));
|
|
120
|
+
const lines = [];
|
|
121
|
+
let answered = 0;
|
|
122
|
+
for (const question of questions) {
|
|
123
|
+
const answer = byId.get(question.id);
|
|
124
|
+
if (answer === void 0) return void 0;
|
|
125
|
+
const chosen = [...answer.selected, ...answer.custom === void 0 || answer.custom === "" ? [] : [answer.custom]];
|
|
126
|
+
if (chosen.length > 0) answered += 1;
|
|
127
|
+
lines.push(questionLabel(question), chosen.length === 0 ? SKIPPED : ` → ${chosen.join(", ")}`);
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
card: "generic",
|
|
131
|
+
title: `Ask user question · ${String(answered)} of ${String(questions.length)} answered`,
|
|
132
|
+
content: [{
|
|
133
|
+
type: "text",
|
|
134
|
+
text: lines.join("\n")
|
|
135
|
+
}]
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
//#endregion
|
|
139
|
+
//#region lib/types/timed.js
|
|
140
|
+
/**
|
|
141
|
+
* Opt-in timed `ask_user_question` tool definition.
|
|
142
|
+
*/
|
|
143
|
+
function validateTimeout(timeout) {
|
|
144
|
+
if (timeout !== -1 && (!Number.isInteger(timeout) || timeout < 1 || timeout > 2147483)) throw new Error("timeout must be -1 or a positive integer up to 2147483 seconds");
|
|
145
|
+
return timeout;
|
|
146
|
+
}
|
|
147
|
+
function validateQuestionIds(questions) {
|
|
148
|
+
const ids = /* @__PURE__ */ new Set();
|
|
149
|
+
for (const question of questions) {
|
|
150
|
+
if (ids.has(question.id)) throw new Error(`question id ${JSON.stringify(question.id)} must be unique within this call`);
|
|
151
|
+
ids.add(question.id);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const description$1 = `Ask brief, direct, self-contained questions about missing information, preferences, or decisions. Use user-facing terms; assume no knowledge of background work or internal names. Use distinct stable question IDs. An answer item with empty selected and no custom means the user skipped that question: continue without it, do not ask it again, and do not treat the skip as permission. A pending result instead means no answer batch arrived before the timeout and the user can still answer.`;
|
|
155
|
+
/**
|
|
156
|
+
* Instruction the pending result carries in its `message` field. It is a field
|
|
157
|
+
* of the result value rather than prose beside it because the recorded result
|
|
158
|
+
* text is read back as one JSON object: the `userQuestions` projection decides
|
|
159
|
+
* from it that the call stays answerable, and the Client question row decides
|
|
160
|
+
* from it that the recorded result is the timeout, not an answer batch.
|
|
161
|
+
*/
|
|
162
|
+
const pendingNotice = "No answer batch arrived before the timeout. This is pending, not a skipped answer. Continue useful independent work. The user can still answer; their reply will be a user message identified as answer_to_pending_question with this callId and the original questions. Do not treat this as permission.";
|
|
163
|
+
/**
|
|
164
|
+
* Translate the model schema into the service request without changing ownership.
|
|
165
|
+
* @param questions - Model-supplied questions in tool-schema form.
|
|
166
|
+
* @param exec - Execution context that owns the agent and cancellation signal.
|
|
167
|
+
* @returns The corresponding user-question service request.
|
|
168
|
+
*/
|
|
169
|
+
function questionRequest(questions, exec) {
|
|
170
|
+
return {
|
|
171
|
+
questions: questions.map((question) => ({
|
|
172
|
+
id: question.id,
|
|
173
|
+
question: question.question,
|
|
174
|
+
...question.header !== void 0 ? { header: question.header } : {},
|
|
175
|
+
...question.options !== void 0 ? { options: question.options.map((option) => ({ ...option })) } : {},
|
|
176
|
+
...question.multi_select !== void 0 ? { multiSelect: question.multi_select } : {}
|
|
177
|
+
})),
|
|
178
|
+
...exec.agent !== void 0 ? { agent: exec.agent } : {},
|
|
179
|
+
signal: exec.signal
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Copy service-owned answer arrays into the model-facing result.
|
|
184
|
+
* @param result - Answer returned by the user-question service.
|
|
185
|
+
* @returns A detached model-facing answer payload.
|
|
186
|
+
*/
|
|
187
|
+
function answerResult(result) {
|
|
188
|
+
return { answers: result.answers.map((answer) => ({
|
|
189
|
+
id: answer.id,
|
|
190
|
+
selected: [...answer.selected],
|
|
191
|
+
...answer.custom !== void 0 ? { custom: answer.custom } : {}
|
|
192
|
+
})) };
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Register the opt-in timed tool definition.
|
|
196
|
+
* @param ctx - Agent-scoped context receiving the tool.
|
|
197
|
+
* @param timeout - Default foreground wait in seconds.
|
|
198
|
+
*/
|
|
199
|
+
function registerTimedAskUser(ctx, timeout = 120) {
|
|
200
|
+
const defaultTimeout = validateTimeout(timeout);
|
|
201
|
+
ctx.tools.register(defineTool({
|
|
202
|
+
name: "ask_user_question",
|
|
203
|
+
description: description$1,
|
|
204
|
+
parameters: {
|
|
205
|
+
questions: {
|
|
206
|
+
type: "array",
|
|
207
|
+
required: true,
|
|
208
|
+
description: "Questions to ask the user.",
|
|
209
|
+
items: {
|
|
210
|
+
type: "object",
|
|
211
|
+
additionalProperties: true,
|
|
212
|
+
properties: {
|
|
213
|
+
id: {
|
|
214
|
+
type: "string",
|
|
215
|
+
required: true,
|
|
216
|
+
description: "Stable id for this question; echoed in the answer."
|
|
217
|
+
},
|
|
218
|
+
question: {
|
|
219
|
+
type: "string",
|
|
220
|
+
required: true,
|
|
221
|
+
description: "The specific question to ask the user."
|
|
222
|
+
},
|
|
223
|
+
header: {
|
|
224
|
+
type: "string",
|
|
225
|
+
description: "Optional short heading for the question, such as \"Confirm\" or \"Choose Mode\"."
|
|
226
|
+
},
|
|
227
|
+
options: {
|
|
228
|
+
type: "array",
|
|
229
|
+
description: "Optional choices to show the user. If you recommend one, put it first and append \"(Recommended)\" to that label.",
|
|
230
|
+
items: {
|
|
231
|
+
type: "object",
|
|
232
|
+
additionalProperties: true,
|
|
233
|
+
properties: {
|
|
234
|
+
label: {
|
|
235
|
+
type: "string",
|
|
236
|
+
required: true,
|
|
237
|
+
description: "Short user-facing option label."
|
|
238
|
+
},
|
|
239
|
+
description: {
|
|
240
|
+
type: "string",
|
|
241
|
+
description: "One sentence explaining the tradeoff or impact."
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
multi_select: {
|
|
247
|
+
type: "boolean",
|
|
248
|
+
description: "Whether the user may select more than one option. Defaults to false."
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
[TIMED_WAIT_PARAMETER]: {
|
|
254
|
+
type: "integer",
|
|
255
|
+
description: `Wait seconds for the entire batch (default ${defaultTimeout}); omit unless the user specifies a duration. Use -1 only when an answer is required before proceeding.`
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
output: {
|
|
259
|
+
schema: { oneOf: [{
|
|
260
|
+
type: "object",
|
|
261
|
+
additionalProperties: false,
|
|
262
|
+
properties: {
|
|
263
|
+
pending: {
|
|
264
|
+
type: "boolean",
|
|
265
|
+
enum: [true],
|
|
266
|
+
required: true,
|
|
267
|
+
description: "True when the foreground wait expired before the user submitted an answer batch. The questions remain answerable; this is not a skipped answer."
|
|
268
|
+
},
|
|
269
|
+
callId: {
|
|
270
|
+
type: "string",
|
|
271
|
+
required: true,
|
|
272
|
+
description: "Tool call whose unanswered questions remain pending."
|
|
273
|
+
},
|
|
274
|
+
message: {
|
|
275
|
+
type: "string",
|
|
276
|
+
required: true,
|
|
277
|
+
description: "How to continue while the questions stay answerable."
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}, {
|
|
281
|
+
type: "object",
|
|
282
|
+
additionalProperties: false,
|
|
283
|
+
properties: { answers: {
|
|
284
|
+
type: "array",
|
|
285
|
+
required: true,
|
|
286
|
+
description: "Submitted answer batch with one item per question. A skipped question has empty selected and no custom; unlike pending, the user has completed the batch.",
|
|
287
|
+
items: {
|
|
288
|
+
type: "object",
|
|
289
|
+
additionalProperties: false,
|
|
290
|
+
properties: {
|
|
291
|
+
id: {
|
|
292
|
+
type: "string",
|
|
293
|
+
required: true,
|
|
294
|
+
description: "Stable question id echoed from the request."
|
|
295
|
+
},
|
|
296
|
+
selected: {
|
|
297
|
+
type: "array",
|
|
298
|
+
required: true,
|
|
299
|
+
items: { type: "string" },
|
|
300
|
+
description: "Selected option labels. Empty with no custom means the user explicitly skipped this question."
|
|
301
|
+
},
|
|
302
|
+
custom: {
|
|
303
|
+
type: "string",
|
|
304
|
+
description: "Optional free-form answer; omitted for a skipped question."
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
} }
|
|
309
|
+
}] },
|
|
310
|
+
render: (_args, value) => [{
|
|
311
|
+
type: "text",
|
|
312
|
+
text: JSON.stringify(value)
|
|
313
|
+
}]
|
|
314
|
+
},
|
|
315
|
+
presentCall: (args) => presentAskCall(args.questions),
|
|
316
|
+
presentResult: (args, result) => presentAskResult(args.questions, result),
|
|
317
|
+
async execute(args, exec) {
|
|
318
|
+
const timeout = validateTimeout(args.timeout ?? defaultTimeout);
|
|
319
|
+
validateQuestionIds(args.questions);
|
|
320
|
+
const request = questionRequest(args.questions, exec);
|
|
321
|
+
if (timeout !== -1) {
|
|
322
|
+
if (exec.agent === void 0) throw new Error("timed questions require a live agent");
|
|
323
|
+
const result = await ctx.userQuestions.askTimed({
|
|
324
|
+
...request,
|
|
325
|
+
agent: exec.agent
|
|
326
|
+
}, exec.callId, timeout * 1e3);
|
|
327
|
+
return "pending" in result ? {
|
|
328
|
+
...result,
|
|
329
|
+
message: pendingNotice
|
|
330
|
+
} : answerResult(result);
|
|
331
|
+
}
|
|
332
|
+
return answerResult(await ctx.userQuestions.ask({
|
|
333
|
+
...request,
|
|
334
|
+
wait: { callId: exec.callId }
|
|
335
|
+
}));
|
|
336
|
+
}
|
|
337
|
+
}));
|
|
338
|
+
}
|
|
339
|
+
//#endregion
|
|
340
|
+
//#region lib/types/index.js
|
|
341
|
+
/**
|
|
342
|
+
* Model-facing Consumer of the `ctx.userQuestions` capability seam.
|
|
343
|
+
* The tool pauses until a UI provider returns a human answer, then feeds that
|
|
344
|
+
* answer back into the agent loop as an ordinary tool result.
|
|
345
|
+
*
|
|
346
|
+
* @module @deepseek-ai/dsh-tool-ask-user
|
|
347
|
+
*/
|
|
348
|
+
const Config = z.object({
|
|
349
|
+
mode: z.union(["legacy", "timed"]).default("legacy"),
|
|
350
|
+
timeout: z.union([-1, z.number().step(1).min(1).max(2147483)]).default(120)
|
|
351
|
+
});
|
|
352
|
+
const name = "tool-ask-user";
|
|
353
|
+
const inject = ["tools", "userQuestions"];
|
|
354
|
+
const description = "Ask the user a concise question when you need confirmation, a choice, or missing information before proceeding. An answer item with empty selected and no custom means the user skipped that question: continue without it, do not ask it again, and do not treat the skip as permission.";
|
|
355
|
+
function apply(ctx, config = {}) {
|
|
356
|
+
if (config.mode === "timed") {
|
|
357
|
+
registerTimedAskUser(ctx, config.timeout);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
ctx.tools.register(defineTool({
|
|
361
|
+
name: "ask_user_question",
|
|
362
|
+
description,
|
|
363
|
+
parameters: { questions: {
|
|
364
|
+
type: "array",
|
|
365
|
+
required: true,
|
|
366
|
+
description: "Questions to ask the user before continuing.",
|
|
367
|
+
items: {
|
|
368
|
+
type: "object",
|
|
369
|
+
additionalProperties: true,
|
|
370
|
+
properties: {
|
|
371
|
+
id: {
|
|
372
|
+
type: "string",
|
|
373
|
+
required: true,
|
|
374
|
+
description: "Stable id for this question; echoed in the answer."
|
|
375
|
+
},
|
|
376
|
+
question: {
|
|
377
|
+
type: "string",
|
|
378
|
+
required: true,
|
|
379
|
+
description: "The specific question to ask the user."
|
|
380
|
+
},
|
|
381
|
+
header: {
|
|
382
|
+
type: "string",
|
|
383
|
+
description: "Optional short heading for the question, such as \"Confirm\" or \"Choose Mode\"."
|
|
384
|
+
},
|
|
385
|
+
options: {
|
|
386
|
+
type: "array",
|
|
387
|
+
description: "Optional choices to show the user. If you recommend one, put it first and append \"(Recommended)\" to that label.",
|
|
388
|
+
items: {
|
|
389
|
+
type: "object",
|
|
390
|
+
additionalProperties: true,
|
|
391
|
+
properties: {
|
|
392
|
+
label: {
|
|
393
|
+
type: "string",
|
|
394
|
+
required: true,
|
|
395
|
+
description: "Short user-facing option label."
|
|
396
|
+
},
|
|
397
|
+
description: {
|
|
398
|
+
type: "string",
|
|
399
|
+
description: "One sentence explaining the tradeoff or impact."
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
multi_select: {
|
|
405
|
+
type: "boolean",
|
|
406
|
+
description: "Whether the user may select more than one option. Defaults to false."
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
} },
|
|
411
|
+
output: {
|
|
412
|
+
schema: {
|
|
413
|
+
type: "object",
|
|
414
|
+
additionalProperties: false,
|
|
415
|
+
properties: { answers: {
|
|
416
|
+
type: "array",
|
|
417
|
+
required: true,
|
|
418
|
+
description: "Submitted answer batch with one item per question. A skipped question has empty selected and no custom.",
|
|
419
|
+
items: {
|
|
420
|
+
type: "object",
|
|
421
|
+
additionalProperties: false,
|
|
422
|
+
properties: {
|
|
423
|
+
id: {
|
|
424
|
+
type: "string",
|
|
425
|
+
required: true
|
|
426
|
+
},
|
|
427
|
+
selected: {
|
|
428
|
+
type: "array",
|
|
429
|
+
required: true,
|
|
430
|
+
items: { type: "string" },
|
|
431
|
+
description: "Selected option labels. Empty with no custom means the user explicitly skipped this question."
|
|
432
|
+
},
|
|
433
|
+
custom: {
|
|
434
|
+
type: "string",
|
|
435
|
+
description: "Optional free-form answer; omitted for a skipped question."
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
} }
|
|
440
|
+
},
|
|
441
|
+
render: (_args, value) => [{
|
|
442
|
+
type: "text",
|
|
443
|
+
text: JSON.stringify(value)
|
|
444
|
+
}]
|
|
445
|
+
},
|
|
446
|
+
presentCall: (args) => presentAskCall(args.questions),
|
|
447
|
+
presentResult: (args, result) => presentAskResult(args.questions, result),
|
|
448
|
+
async execute(args, exec) {
|
|
449
|
+
return { answers: (await ctx.userQuestions.ask({
|
|
450
|
+
questions: args.questions.map((question) => ({
|
|
451
|
+
id: question.id,
|
|
452
|
+
question: question.question,
|
|
453
|
+
...question.header !== void 0 ? { header: question.header } : {},
|
|
454
|
+
...question.options !== void 0 ? { options: question.options } : {},
|
|
455
|
+
...question.multi_select !== void 0 ? { multiSelect: question.multi_select } : {}
|
|
456
|
+
})),
|
|
457
|
+
...exec.agent !== void 0 ? { agent: exec.agent } : {},
|
|
458
|
+
signal: exec.signal
|
|
459
|
+
})).answers.map((answer) => ({
|
|
460
|
+
id: answer.id,
|
|
461
|
+
selected: [...answer.selected],
|
|
462
|
+
...answer.custom !== void 0 ? { custom: answer.custom } : {}
|
|
463
|
+
})) };
|
|
464
|
+
}
|
|
465
|
+
}));
|
|
466
|
+
}
|
|
467
|
+
//#endregion
|
|
468
|
+
export { Config, apply, inject, name };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-facing Consumer of the `ctx.userQuestions` capability seam.
|
|
3
|
+
* The tool pauses until a UI provider returns a human answer, then feeds that
|
|
4
|
+
* answer back into the agent loop as an ordinary tool result.
|
|
5
|
+
*
|
|
6
|
+
* @module @deepseek-ai/dsh-tool-ask-user
|
|
7
|
+
*/
|
|
8
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
9
|
+
import z from '@deepseek-ai/schemastery';
|
|
10
|
+
import '@deepseek-ai/dsh-user-questions';
|
|
11
|
+
/** Cordis row selecting the tool schema and its default foreground wait. */
|
|
12
|
+
export interface Config {
|
|
13
|
+
/** Tool definition selected by this Cordis row. Defaults to the blocking legacy tool. */
|
|
14
|
+
mode?: 'legacy' | 'timed';
|
|
15
|
+
/** Foreground wait before automatic continuation. Defaults to 120 seconds. */
|
|
16
|
+
timeout?: number;
|
|
17
|
+
}
|
|
18
|
+
export declare const Config: z<Config>;
|
|
19
|
+
export declare const name = "tool-ask-user";
|
|
20
|
+
export declare const inject: string[];
|
|
21
|
+
export declare function apply(ctx: Context, config?: Config): void;
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host presentation for `ask_user_question`: the question batch and its answer
|
|
3
|
+
* batch as readable lines, instead of the model-facing JSON arguments and
|
|
4
|
+
* result. A host that renders tool presentation views shows the questions and
|
|
5
|
+
* what the user chose; the model-facing result stays compact JSON.
|
|
6
|
+
* @module @deepseek-ai/dsh-tool-ask-user/presentation
|
|
7
|
+
*/
|
|
8
|
+
import type { ContentBlock } from '@deepseek-ai/dsh-llm';
|
|
9
|
+
import type { ToolCallView, ToolResultView } from '@deepseek-ai/dsh-tools';
|
|
10
|
+
/** One question the presenter reads from the tool's call arguments. */
|
|
11
|
+
export interface PresentedQuestion {
|
|
12
|
+
/** Stable id linking the question to its answer. */
|
|
13
|
+
readonly id: string;
|
|
14
|
+
/** The question text. */
|
|
15
|
+
readonly question: string;
|
|
16
|
+
/** Optional short label above the question. */
|
|
17
|
+
readonly header?: string | undefined;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The presentation view for a pending `ask_user_question` call.
|
|
21
|
+
*
|
|
22
|
+
* A batch renders as its questions, one per line, no matter how many arrived;
|
|
23
|
+
* the title stays the same so a reader recognizes the Tool across states.
|
|
24
|
+
* @param questions - the questions the call asks.
|
|
25
|
+
* @returns the generic call view.
|
|
26
|
+
*/
|
|
27
|
+
export declare function presentAskCall(questions: readonly PresentedQuestion[]): ToolCallView;
|
|
28
|
+
/**
|
|
29
|
+
* The presentation view for a settled `ask_user_question` call.
|
|
30
|
+
*
|
|
31
|
+
* Each question is followed by the labels the user chose, or by a skip marker.
|
|
32
|
+
* A timed call whose window closed without an answer shows that it is still
|
|
33
|
+
* waiting; a failed call declares no view so the ordinary error row remains.
|
|
34
|
+
* @param questions - the questions the call asked.
|
|
35
|
+
* @param result - the settled result projection.
|
|
36
|
+
* @returns the generic result view, or undefined when the result is an error or
|
|
37
|
+
* carries no readable batch.
|
|
38
|
+
*/
|
|
39
|
+
export declare function presentAskResult(questions: readonly PresentedQuestion[], result: {
|
|
40
|
+
readonly content: readonly ContentBlock[];
|
|
41
|
+
readonly isError: boolean;
|
|
42
|
+
}): ToolResultView | undefined;
|
|
43
|
+
//# sourceMappingURL=presentation.d.ts.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opt-in timed `ask_user_question` tool definition.
|
|
3
|
+
*/
|
|
4
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
5
|
+
/** Model-facing meaning of a skipped answer item, shared by the blocking and timed descriptions. */
|
|
6
|
+
export declare const skippedAnswerGuidance: string;
|
|
7
|
+
/**
|
|
8
|
+
* Register the opt-in timed tool definition.
|
|
9
|
+
* @param ctx - Agent-scoped context receiving the tool.
|
|
10
|
+
* @param timeout - Default foreground wait in seconds.
|
|
11
|
+
*/
|
|
12
|
+
export declare function registerTimedAskUser(ctx: Context, timeout?: number): void;
|
|
13
|
+
//# sourceMappingURL=timed.d.ts.map
|
package/locale/de.json
ADDED
package/locale/en.json
ADDED
package/locale/es.json
ADDED
package/locale/fr.json
ADDED
package/locale/ja.json
ADDED
package/locale/pt.json
ADDED
package/locale/ru.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fishwithcat/dsh-tool-ask-user",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"description": "Model-facing ask_user_question tool over the ctx.userQuestions seam",
|
|
4
|
+
"version": "0.2.2-alpha.3",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/FishwithCat/deepseek-harness-tui",
|
|
11
|
+
"directory": "packages/interaction/tool-ask-user"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"types": "lib/types/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/types/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./src/*": "./src/*",
|
|
22
|
+
"./package.json": "./package.json",
|
|
23
|
+
"./locale/*.json": "./locale/*.json"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"locale/*.json",
|
|
27
|
+
"lib/index.js",
|
|
28
|
+
"lib/types/**/*.d.ts"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@deepseek-ai/dsh-agent": "0.2.2-alpha.3",
|
|
33
|
+
"@deepseek-ai/dsh-tools": "0.2.2-alpha.3",
|
|
34
|
+
"@deepseek-ai/dsh-user-questions": "0.2.2-alpha.3",
|
|
35
|
+
"@deepseek-ai/cordis": "~4.0.6-alpha.2"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@deepseek-ai/dsh-agent": "0.2.2-alpha.3",
|
|
39
|
+
"@deepseek-ai/dsh-llm": "0.2.2-alpha.3",
|
|
40
|
+
"@deepseek-ai/dsh-system-prompt": "0.2.2-alpha.3",
|
|
41
|
+
"@deepseek-ai/dsh-tool-executor-native": "0.2.2-alpha.3",
|
|
42
|
+
"@deepseek-ai/dsh-tool-testkit": "0.2.2-alpha.3",
|
|
43
|
+
"@deepseek-ai/dsh-tools": "0.2.2-alpha.3",
|
|
44
|
+
"@deepseek-ai/dsh-user-questions": "0.2.2-alpha.3",
|
|
45
|
+
"@deepseek-ai/cordis": "~4.0.6-alpha.2"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@deepseek-ai/schemastery": "~3.18.6-alpha.2"
|
|
49
|
+
}
|
|
50
|
+
}
|