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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. 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.2",
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
- * Build the ask_user_question shadow from the currently registered base tool.
78
- * @param ctx - root host context with userQuestions and optional goals.
79
- * @param base - existing official ask_user_question definition.
80
- * @param readConfig - live config reader.
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(ctx, base, readConfig) {
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 violations = validateJsonSchemaValue(parameters, args, '')
98
- if (violations.length > 0) throw new ToolArgsError(violations)
99
- validateQuestionIds(args.questions)
100
-
101
- const userQuestions = ctx.get('userQuestions')
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
  }