@zhushanwen/pi-ask-user 0.0.3 → 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/ARCHITECTURE.md +171 -0
- package/README.md +135 -9
- package/package.json +3 -2
- package/src/__tests__/component-keymap.test.ts +553 -0
- package/src/__tests__/component.test.ts +81 -10
- package/src/__tests__/fixtures.ts +56 -2
- package/src/__tests__/index.test.ts +24 -1
- package/src/__tests__/question-view.test.ts +113 -17
- package/src/__tests__/sdk-contract.test.ts +111 -0
- package/src/__tests__/validate.test.ts +14 -1
- package/src/__tests__/w2-draft-hint.test.ts +246 -0
- package/src/__tests__/w3-regression.test.ts +310 -0
- package/src/component.ts +160 -94
- package/src/index.ts +6 -4
- package/src/question-view.ts +80 -41
- package/src/submit-view.ts +17 -7
- package/src/types.ts +24 -2
- package/src/validate.ts +9 -1
package/src/component.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
// src/component.ts
|
|
2
|
-
import { type Component, matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
|
|
2
|
+
import { type Component, matchesKey, parseKey, truncateToWidth } from "@mariozechner/pi-tui";
|
|
3
3
|
|
|
4
4
|
import { allOptions, renderQuestionView } from "./question-view";
|
|
5
|
-
import { buildResult, renderSubmitView } from "./submit-view";
|
|
5
|
+
import { buildResult, renderButtonBar, renderSubmitView } from "./submit-view";
|
|
6
6
|
import {
|
|
7
7
|
createQuestionState,
|
|
8
8
|
HEADER_MAX_CHARS,
|
|
9
|
+
isHighSurrogate,
|
|
9
10
|
type Question,
|
|
10
11
|
type QuestionState,
|
|
11
12
|
type Result,
|
|
13
|
+
SURROGATE_PAIR_LEN,
|
|
12
14
|
type ThemeLike,
|
|
13
15
|
} from "./types";
|
|
14
16
|
|
|
@@ -32,7 +34,6 @@ export class AskUserComponent implements Component {
|
|
|
32
34
|
|
|
33
35
|
private states: QuestionState[];
|
|
34
36
|
private activeTab: number = 0;
|
|
35
|
-
private editorText: string = "";
|
|
36
37
|
|
|
37
38
|
/** Submit tab 上的左右焦点:默认 Submit;← / → 切换;Enter 触发当前项。
|
|
38
39
|
* 问题 tab 上无意义(仅视觉占位),不参与输入路由。 */
|
|
@@ -107,7 +108,7 @@ export class AskUserComponent implements Component {
|
|
|
107
108
|
} else {
|
|
108
109
|
const q = this.questions[this.activeTab]!;
|
|
109
110
|
const state = this.states[this.activeTab]!;
|
|
110
|
-
for (const line of renderQuestionView(q, state, t, innerWidth, this.isSingle,
|
|
111
|
+
for (const line of renderQuestionView(q, state, t, innerWidth, this.isSingle, state.draftText)) {
|
|
111
112
|
add(line);
|
|
112
113
|
}
|
|
113
114
|
}
|
|
@@ -115,7 +116,7 @@ export class AskUserComponent implements Component {
|
|
|
115
116
|
// 多问题:底部按钮栏(Submit / Cancel)。Submit tab 上不重复渲染(renderSubmitView 内嵌 focus 高亮)
|
|
116
117
|
if (!this.isSingle && this.activeTab < this.questions.length) {
|
|
117
118
|
inner.push("");
|
|
118
|
-
this.
|
|
119
|
+
add(renderButtonBar(this.theme, this.allConfirmed(), null));
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
// 用 box 边框包裹:每行 pad 到 innerWidth 后加 │ 左右边框
|
|
@@ -162,28 +163,6 @@ export class AskUserComponent implements Component {
|
|
|
162
163
|
add(truncateToWidth(parts.join(""), innerWidth));
|
|
163
164
|
}
|
|
164
165
|
|
|
165
|
-
/**
|
|
166
|
-
* 底部按钮栏:[ Submit ] [ Cancel ]。
|
|
167
|
-
* focus: null=纯展示(问题 tab),"submit"/"cancel"=高亮对应按钮(Submit tab)。
|
|
168
|
-
*/
|
|
169
|
-
private renderButtonBar(
|
|
170
|
-
innerWidth: number,
|
|
171
|
-
add: (s: string) => void,
|
|
172
|
-
focus: "submit" | "cancel" | null,
|
|
173
|
-
): void {
|
|
174
|
-
const t = this.theme;
|
|
175
|
-
const allDone = this.allConfirmed();
|
|
176
|
-
const isSubmit = focus === "submit";
|
|
177
|
-
const isCancel = focus === "cancel";
|
|
178
|
-
const submit = isSubmit
|
|
179
|
-
? (allDone ? t.fg("success", t.bold(" Submit ")) : t.fg("accent", t.bold(" Submit ")))
|
|
180
|
-
: (allDone ? t.fg("success", " Submit ") : t.fg("dim", " Submit "));
|
|
181
|
-
const cancel = isCancel
|
|
182
|
-
? t.fg("accent", t.bold(" Cancel "))
|
|
183
|
-
: t.fg("muted", " Cancel ");
|
|
184
|
-
add(`${t.fg("dim", "[")}${submit}${t.fg("dim", "]")} ${t.fg("dim", "[")}${cancel}${t.fg("dim", "]")}`);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
166
|
// ── 输入路由 ──
|
|
188
167
|
handleInput(data: string): void {
|
|
189
168
|
if (this._resolved) return;
|
|
@@ -215,15 +194,22 @@ export class AskUserComponent implements Component {
|
|
|
215
194
|
return;
|
|
216
195
|
}
|
|
217
196
|
|
|
197
|
+
// options mode → delegate to handleOptionsInput
|
|
198
|
+
this.handleOptionsInput(data, state, q);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* options 模式的输入处理(从 handleInput 拆出)。
|
|
203
|
+
* 含:Esc 回退/确认取消、←/→ 切 tab、↑/↓ 移光标、Enter 确认、Space toggle。
|
|
204
|
+
*/
|
|
205
|
+
private handleOptionsInput(data: string, state: QuestionState, q: Question): void {
|
|
218
206
|
// Esc → 回退到上一个问题;在首个问题时进入确认取消覆盖层
|
|
219
207
|
if (matchesKey(data, "escape")) {
|
|
220
208
|
this.escBackOrConfirm();
|
|
221
209
|
return;
|
|
222
210
|
}
|
|
223
211
|
|
|
224
|
-
// ← / → 切换问题 tab(多问题,options
|
|
225
|
-
// 首个 ← 停在首个问题(不环绕)。Tab/Shift+Tab 不在问题 tab 切 tab——
|
|
226
|
-
// 留给 Submit tab 上的 Submit/Cancel 焦点切换。
|
|
212
|
+
// ← / → 切换问题 tab(多问题,options 模式)
|
|
227
213
|
if (!this.isSingle && matchesKey(data, "right")) {
|
|
228
214
|
this.gotoTab(Math.min(this.activeTab + 1, this.questions.length));
|
|
229
215
|
return;
|
|
@@ -250,12 +236,12 @@ export class AskUserComponent implements Component {
|
|
|
250
236
|
const opts = allOptions(q);
|
|
251
237
|
const onOther = state.cursorIndex === opts.length - 1;
|
|
252
238
|
|
|
253
|
-
// Other row → Enter opens freeform editor
|
|
254
|
-
// 单选/多选普通选项的 Enter 各自有不同语义(确认/加入选中),与 Other 互斥:
|
|
255
|
-
// onOther 时本分支先消费 Enter,下面多选/单选分支不再处理。
|
|
239
|
+
// Other row → Enter opens freeform editor
|
|
256
240
|
if (onOther && matchesKey(data, "enter")) {
|
|
241
|
+
state.savedOptionsCursorIndex = state.cursorIndex;
|
|
257
242
|
state.mode = "freeform";
|
|
258
|
-
|
|
243
|
+
state.draftText = state.freeTextValue ?? state.freeDraft ?? "";
|
|
244
|
+
state.cursorIndex = state.draftText.length;
|
|
259
245
|
this.invalidate();
|
|
260
246
|
this.tui.requestRender();
|
|
261
247
|
return;
|
|
@@ -267,10 +253,6 @@ export class AskUserComponent implements Component {
|
|
|
267
253
|
return;
|
|
268
254
|
}
|
|
269
255
|
if (matchesKey(data, "enter")) {
|
|
270
|
-
// Enter 先把光标所在的普通选项加入选中再确认,与单选分支(Enter 时
|
|
271
|
-
// selectedIndex = cursorIndex)保持一致。即使 freeTextValue 已存在
|
|
272
|
-
// (Other 录入),也尊重「在此选项上按 Enter 想选中它」的意图,避免
|
|
273
|
-
// 静默用旧 Other 文本确认。add 幂等,不会误取消已选项。
|
|
274
256
|
state.selectedIndices.add(state.cursorIndex);
|
|
275
257
|
this.afterConfirm(state, q);
|
|
276
258
|
return;
|
|
@@ -330,76 +312,157 @@ export class AskUserComponent implements Component {
|
|
|
330
312
|
}
|
|
331
313
|
|
|
332
314
|
private handleEditorInput(data: string, state: QuestionState, q: Question): void {
|
|
315
|
+
// parseKey 白名单拦截 — 替代旧的 matchesKey 散调 + 兜底 printable 遍历
|
|
316
|
+
const keyId = parseKey(data);
|
|
317
|
+
if (keyId !== undefined) {
|
|
318
|
+
this.handleEditorKey(data, keyId, state, q);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
this.handleEditorPaste(data, state);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** parseKey 命中的键:escape/enter/backspace/光标移动/space/printable 各有语义,
|
|
325
|
+
* 其他 special key(功能键/modifier 组合)no-op。 */
|
|
326
|
+
private handleEditorKey(data: string, keyId: string, state: QuestionState, q: Question): void {
|
|
333
327
|
if (matchesKey(data, "escape")) {
|
|
334
|
-
|
|
335
|
-
// AC-17: Esc in comment = skip comment, advance (keep existing commentValue)
|
|
336
|
-
state.mode = "options";
|
|
337
|
-
this.editorText = "";
|
|
338
|
-
this.advance();
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
// Esc in freeform editor = back to options (discard input)
|
|
342
|
-
state.mode = "options";
|
|
343
|
-
this.editorText = "";
|
|
344
|
-
this.invalidate();
|
|
345
|
-
this.tui.requestRender();
|
|
328
|
+
this.handleEditorEsc(state);
|
|
346
329
|
return;
|
|
347
330
|
}
|
|
348
331
|
if (matchesKey(data, "enter")) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
332
|
+
this.handleEditorEnter(state, q);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (matchesKey(data, "backspace")) {
|
|
336
|
+
this.deleteCharBeforeCursor(state);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (this.moveCursor(data, state)) return;
|
|
340
|
+
// 空格特判:parseKey(" ") 返回 "space"(非单字符),需显式插入
|
|
341
|
+
if (matchesKey(data, "space")) {
|
|
342
|
+
this.insertAtCursor(state, " ");
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
// 单字符 printable:parseKey("a") 返回 "a"(code 32-126),在光标处插入
|
|
346
|
+
if (keyId.length === 1 && keyId >= " " && keyId <= "~") {
|
|
347
|
+
this.insertAtCursor(state, keyId);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
// 其他 special key(功能键/modifier 组合)→ no-op(不泄漏)
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Esc:comment 跳过评论并 advance;freeform 存 freeDraft 草稿后回 options。 */
|
|
354
|
+
private handleEditorEsc(state: QuestionState): void {
|
|
355
|
+
if (state.mode === "comment") {
|
|
356
|
+
// comment Esc: skip comment, advance (keep existing commentValue)
|
|
374
357
|
state.mode = "options";
|
|
375
|
-
|
|
358
|
+
state.draftText = "";
|
|
359
|
+
state.cursorIndex = state.savedOptionsCursorIndex;
|
|
376
360
|
this.advance();
|
|
377
361
|
return;
|
|
378
362
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
363
|
+
// freeform Esc: save draft to freeDraft (separate from submitted freeTextValue)
|
|
364
|
+
// so discarded drafts don't pollute the answer or trigger auto-confirm.
|
|
365
|
+
state.freeDraft = state.draftText || null;
|
|
366
|
+
state.mode = "options";
|
|
367
|
+
state.draftText = "";
|
|
368
|
+
state.cursorIndex = state.savedOptionsCursorIndex;
|
|
369
|
+
this.invalidate();
|
|
370
|
+
this.tui.requestRender();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Enter:freeform 有文本→提交,空文本→回退;comment→保存评论并 advance。 */
|
|
374
|
+
private handleEditorEnter(state: QuestionState, q: Question): void {
|
|
375
|
+
const text = state.draftText.trim();
|
|
376
|
+
if (state.mode === "freeform") {
|
|
377
|
+
state.cursorIndex = state.savedOptionsCursorIndex;
|
|
378
|
+
if (text) {
|
|
379
|
+
state.freeTextValue = text;
|
|
380
|
+
state.selectedIndex = null;
|
|
381
|
+
state.mode = "options";
|
|
382
|
+
state.draftText = "";
|
|
383
|
+
this.afterConfirm(state, q);
|
|
384
|
+
} else {
|
|
385
|
+
state.freeTextValue = null;
|
|
386
|
+
state.mode = "options";
|
|
387
|
+
state.draftText = "";
|
|
388
|
+
// freeTextValue 刚清空;confirmed 仅在无其他选择时置 false(允许重新作答)
|
|
389
|
+
if (q.multiSelect ? state.selectedIndices.size === 0 : state.selectedIndex === null) {
|
|
390
|
+
state.confirmed = false;
|
|
391
|
+
}
|
|
392
|
+
this.invalidate();
|
|
393
|
+
this.tui.requestRender();
|
|
394
|
+
}
|
|
383
395
|
return;
|
|
384
396
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
397
|
+
state.commentValue = text || null;
|
|
398
|
+
state.mode = "options";
|
|
399
|
+
state.draftText = "";
|
|
400
|
+
state.cursorIndex = state.savedOptionsCursorIndex;
|
|
401
|
+
this.advance();
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** 在光标处插入文本,光标前移 text.length。 */
|
|
405
|
+
private insertAtCursor(state: QuestionState, text: string): void {
|
|
406
|
+
state.draftText = state.draftText.slice(0, state.cursorIndex) + text + state.draftText.slice(state.cursorIndex);
|
|
407
|
+
state.cursorIndex += text.length;
|
|
408
|
+
this.invalidate();
|
|
409
|
+
this.tui.requestRender();
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** 删除光标前一个 code point(surrogate pair 时删整个 code point)。 */
|
|
413
|
+
private deleteCharBeforeCursor(state: QuestionState): void {
|
|
414
|
+
if (state.cursorIndex <= 0) return;
|
|
415
|
+
const deleteCount = state.cursorIndex >= SURROGATE_PAIR_LEN && isHighSurrogate(state.draftText, state.cursorIndex - SURROGATE_PAIR_LEN) ? SURROGATE_PAIR_LEN : 1;
|
|
416
|
+
state.draftText = state.draftText.slice(0, state.cursorIndex - deleteCount) + state.draftText.slice(state.cursorIndex);
|
|
417
|
+
state.cursorIndex -= deleteCount;
|
|
418
|
+
this.invalidate();
|
|
419
|
+
this.tui.requestRender();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** 光标移动(left/right/home/end),surrogate pair 安全跳过代理中间位。
|
|
423
|
+
* 返回 true 表示命中了移动键;false 表示非移动键,交由上层处理。 */
|
|
424
|
+
private moveCursor(data: string, state: QuestionState): boolean {
|
|
425
|
+
if (matchesKey(data, "left")) {
|
|
426
|
+
const newLeft = state.cursorIndex - 1;
|
|
427
|
+
state.cursorIndex = newLeft > 0 && isHighSurrogate(state.draftText, newLeft - 1)
|
|
428
|
+
? newLeft - 1
|
|
429
|
+
: Math.max(0, newLeft);
|
|
430
|
+
} else if (matchesKey(data, "right")) {
|
|
431
|
+
state.cursorIndex = isHighSurrogate(state.draftText, state.cursorIndex)
|
|
432
|
+
? Math.min(state.draftText.length, state.cursorIndex + SURROGATE_PAIR_LEN)
|
|
433
|
+
: Math.min(state.draftText.length, state.cursorIndex + 1);
|
|
434
|
+
} else if (matchesKey(data, "home")) {
|
|
435
|
+
state.cursorIndex = 0;
|
|
436
|
+
} else if (matchesKey(data, "end")) {
|
|
437
|
+
state.cursorIndex = state.draftText.length;
|
|
438
|
+
} else {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
this.invalidate();
|
|
442
|
+
this.tui.requestRender();
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** 多字符粘贴 chunk → printable 提取(BC-1/BC-2/BC-3 保持)。
|
|
447
|
+
* 未识别控制序列(OSC/DA/DCS/APC/unknown CSI)整体丢弃,不提取可见残渣。
|
|
448
|
+
* 排除 bracketed paste 标记防 C-PASTE 退化。依赖 StdinBuffer 序列拆分保证 data 整体性。 */
|
|
449
|
+
private handleEditorPaste(data: string, state: QuestionState): void {
|
|
450
|
+
if (data.startsWith("\x1b") && !data.includes("\x1b[200~") && !data.includes("\x1b[201~")) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
const cleaned = data.replace(/\x1b\[200~|\x1b\[201~/g, "");
|
|
392
454
|
let changed = false;
|
|
393
|
-
|
|
455
|
+
// 用 Array.from 正确拆分 emoji/surrogate pairs
|
|
456
|
+
for (const c of Array.from(cleaned)) {
|
|
394
457
|
if (c >= " ") {
|
|
395
|
-
|
|
458
|
+
state.draftText = state.draftText.slice(0, state.cursorIndex) + c + state.draftText.slice(state.cursorIndex);
|
|
459
|
+
state.cursorIndex += c.length;
|
|
396
460
|
changed = true;
|
|
397
461
|
}
|
|
398
462
|
}
|
|
399
463
|
if (changed) {
|
|
400
464
|
this.invalidate();
|
|
401
465
|
this.tui.requestRender();
|
|
402
|
-
return;
|
|
403
466
|
}
|
|
404
467
|
}
|
|
405
468
|
|
|
@@ -448,10 +511,10 @@ export class AskUserComponent implements Component {
|
|
|
448
511
|
private afterConfirm(state: QuestionState, q: Question): void {
|
|
449
512
|
state.confirmed = true;
|
|
450
513
|
if (q.allowComment && state.mode !== "comment") {
|
|
451
|
-
|
|
452
|
-
// 允许回改时重新编辑/清除已输入评论,避免评论被错误附着到后改的答案。
|
|
514
|
+
state.savedOptionsCursorIndex = state.cursorIndex;
|
|
453
515
|
state.mode = "comment";
|
|
454
|
-
|
|
516
|
+
state.draftText = state.commentValue ?? "";
|
|
517
|
+
state.cursorIndex = state.draftText.length;
|
|
455
518
|
this.invalidate();
|
|
456
519
|
this.tui.requestRender();
|
|
457
520
|
return;
|
|
@@ -478,8 +541,11 @@ export class AskUserComponent implements Component {
|
|
|
478
541
|
this.done(buildResult(this.questions, this.states));
|
|
479
542
|
}
|
|
480
543
|
|
|
481
|
-
/** 取消。public 供 signal abort 监听器复用 _resolved 守卫(FR-12
|
|
544
|
+
/** 取消。public 供 signal abort 监听器复用 _resolved 守卫(FR-12 竞态)。
|
|
545
|
+
* 守卫在方法内:signal abort 可能在用户已 submit/cancel 后才触发,
|
|
546
|
+
* 此时必须 no-op,避免二次调 done(FR-12)。 */
|
|
482
547
|
cancel(): void {
|
|
548
|
+
if (this._resolved) return;
|
|
483
549
|
this._resolved = true;
|
|
484
550
|
this.done(null);
|
|
485
551
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import type { AgentToolResult, ExtensionAPI, ExtensionContext, ToolRenderResultOptions } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { AgentToolResult, AgentToolUpdateCallback, ExtensionAPI, ExtensionContext, ToolRenderResultOptions } from "@mariozechner/pi-coding-agent";
|
|
3
3
|
import { Box, Text, TruncatedText, truncateToWidth } from "@mariozechner/pi-tui";
|
|
4
4
|
import { type Static } from "@sinclair/typebox";
|
|
5
5
|
|
|
@@ -76,7 +76,7 @@ If you recommend an option, prefix its label with "(Recommended)" and list it fi
|
|
|
76
76
|
_toolCallId: string,
|
|
77
77
|
params: Static<typeof InputSchema>,
|
|
78
78
|
signal: AbortSignal | undefined,
|
|
79
|
-
_onUpdate:
|
|
79
|
+
_onUpdate: AgentToolUpdateCallback<AskUserDetails> | undefined,
|
|
80
80
|
ctx: ExtensionContext,
|
|
81
81
|
): Promise<ExecuteResult> {
|
|
82
82
|
const questions = params.questions;
|
|
@@ -111,13 +111,15 @@ If you recommend an option, prefix its label with "(Recommended)" and list it fi
|
|
|
111
111
|
};
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
// 3. Signal abort 入口检查(spec FR-10
|
|
114
|
+
// 3. Signal abort 入口检查(spec FR-10)。
|
|
115
|
+
// abort 是 agent 被外部终止(goal 取消 / context compact / session 切换),
|
|
116
|
+
// 不是用户意图——文案必须区别于 step 5 的用户取消,避免 LLM 俊等用户。
|
|
115
117
|
if (signal?.aborted) {
|
|
116
118
|
return {
|
|
117
119
|
content: [
|
|
118
120
|
{
|
|
119
121
|
type: "text" as const,
|
|
120
|
-
text: "
|
|
122
|
+
text: "Agent aborted (goal cancelled, context compacted, or session switched). Do not assume an answer; do not retry ask_user — propagate the abort, or wait for new instructions if the decision is still required.",
|
|
121
123
|
},
|
|
122
124
|
],
|
|
123
125
|
details: { questions, answers: {}, cancelled: true } satisfies Result,
|