dsh-better-ask-user 0.1.2 → 0.1.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/package.json +1 -1
- package/src/ask-tool.js +11 -135
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-better-ask-user",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Independent ask_user_question plugin: configurable wait, input-extended countdown, markdown/mobile card, native or default timeout behavior, and PTC deadline exemption.",
|
|
5
5
|
"license": "GPL-3.0",
|
|
6
6
|
"repository": {
|
package/src/ask-tool.js
CHANGED
|
@@ -1,140 +1,16 @@
|
|
|
1
|
-
/** ask_user_question scope shadow: host-controlled wait and timeout behavior. */
|
|
2
|
-
import { ToolArgsError, validateJsonSchemaValue } from '@deepseek-ai/dsh-tools'
|
|
3
|
-
import { TIMED_WAIT_PARAMETER } from '@deepseek-ai/dsh-user-questions'
|
|
4
|
-
import { buildDefaultAnswer } from './default-answer.js'
|
|
5
|
-
|
|
6
|
-
/** Model-visible notice for the native pending result. */
|
|
7
|
-
export const PENDING_QUESTION_NOTICE = '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.'
|
|
8
|
-
|
|
9
|
-
/** Compatibility wait field description; the host owns the real wait. */
|
|
10
|
-
const COMPATIBILITY_TIMEOUT = {
|
|
11
|
-
type: 'integer',
|
|
12
|
-
description: 'Compatibility wait field. The host controls the wait duration and timeout behavior; any value supplied here is ignored.',
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function questionRequest(questions, exec) {
|
|
16
|
-
return {
|
|
17
|
-
questions: questions.map(question => ({
|
|
18
|
-
id: question.id,
|
|
19
|
-
question: question.question,
|
|
20
|
-
...(question.header !== undefined ? { header: question.header } : {}),
|
|
21
|
-
...(question.options !== undefined ? {
|
|
22
|
-
options: question.options.map(option => ({ ...option })),
|
|
23
|
-
} : {}),
|
|
24
|
-
...(question.multi_select !== undefined ? { multiSelect: question.multi_select } : {}),
|
|
25
|
-
...(question.detail !== undefined ? { detail: question.detail } : {}),
|
|
26
|
-
})),
|
|
27
|
-
...(exec.agent !== undefined ? { agent: exec.agent } : {}),
|
|
28
|
-
signal: exec.signal,
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function validateQuestionIds(questions) {
|
|
33
|
-
const ids = new Set()
|
|
34
|
-
for (const question of questions) {
|
|
35
|
-
if (ids.has(question.id)) throw new Error('question id ' + JSON.stringify(question.id) + ' must be unique within this call')
|
|
36
|
-
ids.add(question.id)
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function answerResult(result) {
|
|
41
|
-
return {
|
|
42
|
-
answers: result.answers.map(answer => ({
|
|
43
|
-
id: answer.id,
|
|
44
|
-
selected: [...answer.selected],
|
|
45
|
-
...(answer.custom !== undefined ? { custom: answer.custom } : {}),
|
|
46
|
-
})),
|
|
47
|
-
...(result.autoSelected === true ? { autoSelected: true } : {}),
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function answersOutputSchema(schema) {
|
|
52
|
-
const next = structuredClone(schema)
|
|
53
|
-
const branches = Array.isArray(next.oneOf) ? next.oneOf : [next]
|
|
54
|
-
for (const branch of branches) {
|
|
55
|
-
if (branch?.properties?.answers !== undefined) {
|
|
56
|
-
branch.properties.autoSelected = {
|
|
57
|
-
type: 'boolean',
|
|
58
|
-
description: 'True when the host selected default options after the wait expired; this is not a user answer.',
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return next
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function timeoutParameters(parameters) {
|
|
66
|
-
const next = structuredClone(parameters)
|
|
67
|
-
if (next?.properties === undefined) next.properties = {}
|
|
68
|
-
if (next.properties[TIMED_WAIT_PARAMETER] === undefined) {
|
|
69
|
-
next.properties[TIMED_WAIT_PARAMETER] = COMPATIBILITY_TIMEOUT
|
|
70
|
-
} else {
|
|
71
|
-
next.properties[TIMED_WAIT_PARAMETER] = { ...next.properties[TIMED_WAIT_PARAMETER], ...COMPATIBILITY_TIMEOUT }
|
|
72
|
-
}
|
|
73
|
-
return next
|
|
74
|
-
}
|
|
75
|
-
|
|
76
1
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* @returns a ToolDefinition replacing the base behavior.
|
|
2
|
+
* ask_user_question scope shadow: the official tool definition, schema, output
|
|
3
|
+
* and execute path verbatim. The ask-user row only supplies the default wait
|
|
4
|
+
* when the model omits the timeout argument; explicit model timeouts and every
|
|
5
|
+
* validation or error stay exactly those of @deepseek-ai/dsh-tool-ask-user.
|
|
82
6
|
*/
|
|
83
|
-
export function createBetterAskTool(
|
|
84
|
-
const parameters = timeoutParameters(base.parameters)
|
|
85
|
-
const output = {
|
|
86
|
-
...base.output,
|
|
87
|
-
schema: answersOutputSchema(base.output.schema),
|
|
88
|
-
render: (_args, value) => [{
|
|
89
|
-
type: 'text',
|
|
90
|
-
text: value?.autoSelected === true
|
|
91
|
-
? 'auto-selected default answer after the question wait expired; this is not a user answer. Verify before relying on it.\n' + JSON.stringify(value)
|
|
92
|
-
: JSON.stringify(value),
|
|
93
|
-
}],
|
|
94
|
-
}
|
|
95
|
-
|
|
7
|
+
export function createBetterAskTool(_ctx, base, readConfig) {
|
|
96
8
|
const execute = async (args, exec) => {
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (userQuestions === undefined) throw new Error('ask_user_question requires the userQuestions service')
|
|
103
|
-
|
|
104
|
-
const config = readConfig()
|
|
105
|
-
const goal = exec.agent === undefined ? undefined : ctx.get('goals')?.get?.(exec.agent)
|
|
106
|
-
const goalActive = goal?.phase === 'active' && goal.activation === 'armed'
|
|
107
|
-
const mode = goalActive ? config.goalTimeoutBehavior : config.timeoutBehavior
|
|
108
|
-
const seconds = config.questionTimeoutSeconds
|
|
109
|
-
const questions = questionRequest(args.questions, exec)
|
|
110
|
-
const defaultAnswer = mode === 'default' && seconds !== -1
|
|
111
|
-
? buildDefaultAnswer(args.questions)
|
|
112
|
-
: undefined
|
|
113
|
-
|
|
114
|
-
if (seconds === -1) {
|
|
115
|
-
const result = await userQuestions.ask({ ...questions, wait: { callId: exec.callId } })
|
|
116
|
-
return answerResult(result)
|
|
117
|
-
}
|
|
118
|
-
if (exec.agent === undefined) throw new Error('timed questions require a live agent')
|
|
119
|
-
|
|
120
|
-
const waitMs = seconds * 1000
|
|
121
|
-
const result = await userQuestions.askTimed({
|
|
122
|
-
...questions,
|
|
123
|
-
agent: exec.agent,
|
|
124
|
-
better: { waitMs, timeoutMode: mode, defaultAnswer },
|
|
125
|
-
}, exec.callId, waitMs)
|
|
126
|
-
if ('pending' in result) {
|
|
127
|
-
if (mode === 'default' && defaultAnswer !== undefined) return defaultAnswer
|
|
128
|
-
return { ...result, message: PENDING_QUESTION_NOTICE }
|
|
129
|
-
}
|
|
130
|
-
return answerResult(result)
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return {
|
|
134
|
-
...base,
|
|
135
|
-
description: base.description + ' The host controls the wait duration and timeout behavior; the timeout argument is a compatibility field and is ignored.',
|
|
136
|
-
parameters,
|
|
137
|
-
output,
|
|
138
|
-
execute,
|
|
9
|
+
const { questionTimeoutSeconds } = readConfig()
|
|
10
|
+
const withDefault = args.timeout === undefined
|
|
11
|
+
? { ...args, timeout: questionTimeoutSeconds }
|
|
12
|
+
: args
|
|
13
|
+
return base.execute(withDefault, exec)
|
|
139
14
|
}
|
|
15
|
+
return { ...base, execute }
|
|
140
16
|
}
|