@sayknow-cli/coding-agent 0.5.1 → 0.5.6
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 +185 -14
- package/dist/types/cli/update-cli.d.ts +8 -1
- package/dist/types/commands/session.d.ts +7 -0
- package/dist/types/config/atomic-yaml-patch.d.ts +20 -1
- package/dist/types/config/keybindings.d.ts +10 -0
- package/dist/types/config/model-registry.d.ts +1 -1
- package/dist/types/config/models-config-schema.d.ts +4 -0
- package/dist/types/config/settings-schema.d.ts +9 -0
- package/dist/types/config/telegram-autostart.d.ts +9 -1
- package/dist/types/dap/client.d.ts +1 -0
- package/dist/types/lsp/client.d.ts +5 -0
- package/dist/types/modes/components/pet-capability.d.ts +8 -7
- package/dist/types/modes/components/pet-selector.d.ts +1 -1
- package/dist/types/modes/components/sayknow-pet-widget.d.ts +1 -1
- package/dist/types/modes/components/welcome.d.ts +1 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
- package/dist/types/modes/interactive-mode.d.ts +13 -1
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +7 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -0
- package/dist/types/modes/types.d.ts +7 -1
- package/dist/types/runtime-mcp/transports/stdio.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +9 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +3 -1
- package/dist/types/session/internal/managed-session-storage.d.ts +2 -0
- package/dist/types/session/session-manager.d.ts +9 -0
- package/dist/types/session/session-storage.d.ts +17 -0
- package/dist/types/session-import/claude.d.ts +26 -0
- package/dist/types/session-import/codex.d.ts +22 -0
- package/dist/types/session-import/command.d.ts +20 -0
- package/dist/types/session-import/detect.d.ts +22 -0
- package/dist/types/session-import/index.d.ts +7 -0
- package/dist/types/session-import/redact.d.ts +23 -0
- package/dist/types/session-import/service.d.ts +28 -0
- package/dist/types/session-import/types.d.ts +125 -0
- package/dist/types/skc-runtime/boot-generation.d.ts +59 -0
- package/dist/types/skc-runtime/launch-tmux.d.ts +10 -2
- package/dist/types/skc-runtime/launch-worktree.d.ts +17 -1
- package/dist/types/skc-runtime/session-restore-runtime.d.ts +41 -0
- package/dist/types/skc-runtime/session-restore.d.ts +99 -0
- package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +160 -0
- package/dist/types/skc-runtime/tmux-sessions.d.ts +26 -1
- package/dist/types/slash-commands/builtin-registry.d.ts +4 -0
- package/dist/types/slash-commands/types.d.ts +5 -0
- package/dist/types/tools/ask.d.ts +169 -4
- package/package.json +16 -7
- package/scripts/generate-sdk-operation-inventory.ts +5 -6
- package/src/cli/config-cli.ts +23 -8
- package/src/cli/update-cli.ts +265 -21
- package/src/commands/session.ts +88 -2
- package/src/config/atomic-yaml-patch.ts +167 -30
- package/src/config/keybindings.ts +10 -0
- package/src/config/model-profiles.ts +69 -1
- package/src/config/model-registry.ts +72 -38
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +9 -0
- package/src/config/settings.ts +13 -2
- package/src/config/telegram-autostart.ts +11 -4
- package/src/dap/client.ts +78 -30
- package/src/defaults/skc/skills/deep-interview/SKILL.md +29 -3
- package/src/internal-urls/docs-index.generated.ts +5 -4
- package/src/lsp/client.ts +17 -29
- package/src/main.ts +1 -1
- package/src/modes/action-registry.ts +2 -0
- package/src/modes/components/pet-capability.ts +22 -13
- package/src/modes/components/pet-selector.ts +1 -1
- package/src/modes/components/sayknow-pet-widget.ts +41 -7
- package/src/modes/components/welcome.ts +5 -0
- package/src/modes/controllers/event-controller.ts +1 -1
- package/src/modes/controllers/goal-mode-controller.ts +7 -2
- package/src/modes/controllers/input-controller.ts +71 -6
- package/src/modes/controllers/plan-mode-controller.ts +8 -1
- package/src/modes/controllers/runtime-mcp-command-controller.ts +14 -0
- package/src/modes/controllers/selector-controller.ts +47 -19
- package/src/modes/interactive-mode.ts +54 -4
- package/src/modes/shared/agent-wire/unattended-session.ts +40 -9
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -0
- package/src/modes/types.ts +5 -1
- package/src/notifications/lifecycle-control-runtime.ts +258 -179
- package/src/prompts/system/eager-todo.md +2 -0
- package/src/prompts/system/plan-mode-approved.md +1 -1
- package/src/prompts/system/system-prompt.md +4 -2
- package/src/runtime-mcp/transports/stdio.ts +96 -33
- package/src/sdk/broker/lifecycle.ts +6 -5
- package/src/sdk/bus/lifecycle-control-runtime.ts +189 -110
- package/src/sdk/protocol/operation-inventory.generated.json +33 -0
- package/src/sdk/session.ts +25 -18
- package/src/session/agent-session.ts +120 -25
- package/src/session/internal/managed-session-scope.ts +41 -6
- package/src/session/internal/managed-session-storage.ts +14 -0
- package/src/session/session-manager.ts +113 -11
- package/src/session/session-storage.ts +70 -0
- package/src/session-import/claude.ts +382 -0
- package/src/session-import/codex.ts +458 -0
- package/src/session-import/command.ts +61 -0
- package/src/session-import/detect.ts +133 -0
- package/src/session-import/index.ts +27 -0
- package/src/session-import/redact.ts +125 -0
- package/src/session-import/service.ts +749 -0
- package/src/session-import/types.ts +163 -0
- package/src/skc-runtime/boot-generation.ts +172 -0
- package/src/skc-runtime/launch-tmux.ts +219 -41
- package/src/skc-runtime/launch-worktree.ts +277 -4
- package/src/skc-runtime/session-restore-runtime.ts +120 -0
- package/src/skc-runtime/session-restore.ts +296 -0
- package/src/skc-runtime/session-state-sidecar.ts +41 -0
- package/src/skc-runtime/tmux-owner-isolation.ts +665 -0
- package/src/skc-runtime/tmux-sessions.ts +284 -108
- package/src/slash-commands/acp-builtins.ts +5 -1
- package/src/slash-commands/builtin-registry.ts +109 -5
- package/src/slash-commands/types.ts +5 -0
- package/src/tools/ask.ts +188 -10
- package/src/tools/eval.ts +2 -2
- package/src/export/html/template.css +0 -1060
- package/src/export/html/template.html +0 -47
- package/src/export/html/template.js +0 -2348
- package/vendor/insane-search/engine/tests/test_hardening.py +0 -57
- package/vendor/insane-search/engine/tests/test_smoke.py +0 -152
- package/vendor/insane-search/engine/tests/test_u1.py +0 -200
- package/vendor/insane-search/engine/tests/test_u4.py +0 -131
- package/vendor/insane-search/engine/tests/test_u5.py +0 -163
- package/vendor/insane-search/engine/tests/test_u7.py +0 -124
- package/vendor/insane-search/engine/tests/test_u8.py +0 -216
package/src/tools/ask.ts
CHANGED
|
@@ -39,6 +39,7 @@ import { getMarkdownTheme, type Theme, theme } from "../modes/theme/theme";
|
|
|
39
39
|
import askDescription from "../prompts/tools/ask.md" with { type: "text" };
|
|
40
40
|
import { appendOrMergeDeepInterviewRound, syncDeepInterviewRecorderHud } from "../skc-runtime/deep-interview-recorder";
|
|
41
41
|
import { deepInterviewStatePath } from "../skc-runtime/deep-interview-runtime";
|
|
42
|
+
import { DEEP_INTERVIEW_INTENT_CATEGORIES, deepInterviewCharacterCount } from "../skc-runtime/deep-interview-state";
|
|
42
43
|
import { renderStatusLine } from "../tui";
|
|
43
44
|
import type { ToolSession } from ".";
|
|
44
45
|
import { formatErrorMessage, formatMeta, formatTitle } from "./render-utils";
|
|
@@ -53,13 +54,114 @@ const OptionItem = z.object({
|
|
|
53
54
|
label: z.string().describe("display label"),
|
|
54
55
|
});
|
|
55
56
|
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Bounded by code points, not UTF-16 units: an emoji-padded field must hit the same
|
|
59
|
+
* ceiling the gate adapter and interview state enforce, or oversized metadata survives
|
|
60
|
+
* the tool boundary and only fails deeper in the recorder.
|
|
61
|
+
*/
|
|
62
|
+
const boundedText = (max: number) => z.string().refine(value => deepInterviewCharacterCount(value) <= max);
|
|
63
|
+
const ADAPTER_TEXT_MAX = 256;
|
|
64
|
+
const CORE_TEXT_MAX = 128;
|
|
65
|
+
const LONG_TEXT_MAX = 2048;
|
|
66
|
+
|
|
67
|
+
/** Inert adapter context: never fetched, never allowed to alter the first question. */
|
|
68
|
+
const AskGateReferenceItem = z
|
|
69
|
+
.object({
|
|
70
|
+
reference_id: boundedText(ADAPTER_TEXT_MAX),
|
|
71
|
+
label: boundedText(ADAPTER_TEXT_MAX),
|
|
72
|
+
origin: boundedText(ADAPTER_TEXT_MAX),
|
|
73
|
+
url: boundedText(LONG_TEXT_MAX).optional(),
|
|
74
|
+
excerpt: boundedText(LONG_TEXT_MAX).optional(),
|
|
75
|
+
})
|
|
76
|
+
.strict();
|
|
77
|
+
|
|
78
|
+
/** One locked-intent line. `id` MUST carry its own category as a prefix. */
|
|
79
|
+
const IntentItem = z
|
|
80
|
+
.object({
|
|
81
|
+
id: z.string().min(1),
|
|
82
|
+
category: z.enum(DEEP_INTERVIEW_INTENT_CATEGORIES),
|
|
83
|
+
statement: boundedText(1_000).min(1),
|
|
84
|
+
})
|
|
85
|
+
.strict()
|
|
86
|
+
.refine(item => item.id.startsWith(`${item.category}:`));
|
|
87
|
+
|
|
88
|
+
const IntentSubstitution = z
|
|
89
|
+
.object({
|
|
90
|
+
removed_id: z.string().min(1),
|
|
91
|
+
replacement_ids: z.array(z.string().min(1)),
|
|
92
|
+
rationale: boundedText(500),
|
|
93
|
+
})
|
|
94
|
+
.strict();
|
|
95
|
+
|
|
96
|
+
const IntentContract = z
|
|
97
|
+
.object({ items: z.array(IntentItem).min(1).max(64), confirmation_options: z.array(z.string().min(1)).min(1) })
|
|
98
|
+
.strict()
|
|
99
|
+
.describe("Round 0 locked-intent manifest proposal");
|
|
100
|
+
|
|
101
|
+
const IntentReview = z
|
|
102
|
+
.object({
|
|
103
|
+
observed_items: z.array(IntentItem).min(1).max(64),
|
|
104
|
+
supporting_substitutions: z.array(IntentSubstitution).max(64),
|
|
105
|
+
approval_options: z.array(z.string().min(1)).min(1),
|
|
106
|
+
})
|
|
107
|
+
.strict()
|
|
108
|
+
.describe("post-Round-0 locked-intent reduction review");
|
|
109
|
+
|
|
110
|
+
const deepInterviewMetaBase = {
|
|
111
|
+
round_id: boundedText(CORE_TEXT_MAX).describe("stable optional round identity").optional(),
|
|
59
112
|
round: z.number().int().nonnegative().describe("round number"),
|
|
60
|
-
component:
|
|
61
|
-
dimension:
|
|
113
|
+
component: boundedText(CORE_TEXT_MAX).min(1).describe("targeted topology component"),
|
|
114
|
+
dimension: boundedText(CORE_TEXT_MAX).min(1).describe("targeted clarity dimension"),
|
|
62
115
|
ambiguity: z.number().min(0).max(1).describe("ambiguity at ask time (0..1)"),
|
|
116
|
+
confused_terms: z
|
|
117
|
+
.array(boundedText(ADAPTER_TEXT_MAX))
|
|
118
|
+
.max(32)
|
|
119
|
+
.describe("inert adapter vocabulary; never alters the question")
|
|
120
|
+
.optional(),
|
|
121
|
+
references: z
|
|
122
|
+
.array(AskGateReferenceItem)
|
|
123
|
+
.max(32)
|
|
124
|
+
.describe("inert adapter references; never auto-fetched")
|
|
125
|
+
.optional(),
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Optional structured deep-interview round metadata; when present the round is
|
|
130
|
+
* recorded automatically. The three branches are mutually exclusive **on the
|
|
131
|
+
* wire**, not just at parse time: the manifest is locked exactly once, by the
|
|
132
|
+
* Round 0 topology confirmation (accepting a contract on any later round would
|
|
133
|
+
* let a mid-interview answer replace the locked intent), and a reduction review
|
|
134
|
+
* only exists after Round 0 has locked a manifest. Providers that only see the
|
|
135
|
+
* JSON schema must be unable to combine the two.
|
|
136
|
+
*/
|
|
137
|
+
const DeepInterviewMeta = z.union([
|
|
138
|
+
// Branch order is load-bearing: the intent-carrying branches must win when
|
|
139
|
+
// their key is present, and every branch is strict so a contract or review
|
|
140
|
+
// attached to the wrong round fails closed instead of being silently
|
|
141
|
+
// stripped by a laxer branch.
|
|
142
|
+
z
|
|
143
|
+
.object({
|
|
144
|
+
...deepInterviewMetaBase,
|
|
145
|
+
round: z.literal(0).describe("the manifest is locked exactly once, at Round 0"),
|
|
146
|
+
component: z.literal("review-topology"),
|
|
147
|
+
intent_contract: IntentContract,
|
|
148
|
+
})
|
|
149
|
+
.strict()
|
|
150
|
+
.describe("Round 0 topology confirmation carrying the locked-intent manifest proposal"),
|
|
151
|
+
z
|
|
152
|
+
.object({
|
|
153
|
+
...deepInterviewMetaBase,
|
|
154
|
+
round: z.number().int().min(1).describe("reviews only exist after Round 0 locked a manifest"),
|
|
155
|
+
intent_review: IntentReview,
|
|
156
|
+
})
|
|
157
|
+
.strict()
|
|
158
|
+
.describe("post-Round-0 locked-intent reduction review"),
|
|
159
|
+
z.object(deepInterviewMetaBase).strict().describe("ordinary interview round metadata"),
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
const WorkflowGateMeta = z.object({
|
|
163
|
+
stage: z.enum(["deep-interview", "ralplan", "ultragoal"]).describe("workflow gate stage"),
|
|
164
|
+
kind: z.enum(["question", "approval", "execution"]).describe("workflow gate kind"),
|
|
63
165
|
});
|
|
64
166
|
|
|
65
167
|
const QuestionItem = z.object({
|
|
@@ -68,6 +170,7 @@ const QuestionItem = z.object({
|
|
|
68
170
|
options: z.array(OptionItem).describe("available options"),
|
|
69
171
|
multi: z.boolean().describe("allow multiple selections").optional(),
|
|
70
172
|
recommended: z.number().describe("recommended option index").optional(),
|
|
173
|
+
workflowGate: WorkflowGateMeta.describe("optional workflow gate stage/kind override").optional(),
|
|
71
174
|
deepInterview: DeepInterviewMeta.describe("optional deep-interview round metadata").optional(),
|
|
72
175
|
});
|
|
73
176
|
|
|
@@ -93,6 +196,8 @@ export interface AskToolDetails {
|
|
|
93
196
|
multi?: boolean;
|
|
94
197
|
selectedOptions?: string[];
|
|
95
198
|
customInput?: string;
|
|
199
|
+
/** Set when the user answered with a clarification question instead of a choice. */
|
|
200
|
+
clarificationQuestion?: string;
|
|
96
201
|
/** Multi-part question mode */
|
|
97
202
|
results?: QuestionResult[];
|
|
98
203
|
}
|
|
@@ -490,6 +595,11 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
490
595
|
readonly description: string;
|
|
491
596
|
readonly parameters = askSchema;
|
|
492
597
|
readonly strict = true;
|
|
598
|
+
/**
|
|
599
|
+
* These fields are rendered to the user and carry no executable or durable
|
|
600
|
+
* meaning. IDs and workflow/deep-interview metadata remain fail-closed.
|
|
601
|
+
*/
|
|
602
|
+
readonly displaySafeEscapedArgFields = ["questions.question", "questions.options.label"] as const;
|
|
493
603
|
readonly loadMode = "discoverable";
|
|
494
604
|
|
|
495
605
|
constructor(private readonly session: ToolSession) {
|
|
@@ -519,6 +629,9 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
519
629
|
): Promise<void> {
|
|
520
630
|
const meta = q.deepInterview;
|
|
521
631
|
if (!meta) return;
|
|
632
|
+
// A stage/kind override addresses a non-deep-interview gate; recording it as
|
|
633
|
+
// an interview round would corrupt the interview state.
|
|
634
|
+
if (q.workflowGate && (q.workflowGate.stage !== "deep-interview" || q.workflowGate.kind !== "question")) return;
|
|
522
635
|
const cwd = this.session.cwd;
|
|
523
636
|
const sessionId = this.session.getSessionId?.() ?? undefined;
|
|
524
637
|
const statePath = deepInterviewStatePath(cwd, sessionId);
|
|
@@ -536,6 +649,11 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
536
649
|
ambiguity: meta.ambiguity,
|
|
537
650
|
selectedOptions,
|
|
538
651
|
customInput,
|
|
652
|
+
// The Round 0 manifest is locked from the confirmation answer itself, and any
|
|
653
|
+
// later reduction is reviewed against that lock. Dropping them here leaves the
|
|
654
|
+
// recorder nothing to bind, so the "locked intent" is never actually locked.
|
|
655
|
+
intent_contract: "intent_contract" in meta ? meta.intent_contract : undefined,
|
|
656
|
+
intent_review: "intent_review" in meta ? meta.intent_review : undefined,
|
|
539
657
|
},
|
|
540
658
|
{ sessionId },
|
|
541
659
|
).then(async () => {
|
|
@@ -553,7 +671,11 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
553
671
|
): Promise<AgentToolResult<AskToolDetails>> {
|
|
554
672
|
await assertUltragoalAskAllowed(this.session.cwd);
|
|
555
673
|
const gateEmitter = this.session.getWorkflowGateEmitter?.();
|
|
556
|
-
|
|
674
|
+
// A negotiated unattended run answers through workflow gates even when a UI
|
|
675
|
+
// exists; otherwise gates are the headless answer path only.
|
|
676
|
+
const canUseWorkflowGate =
|
|
677
|
+
gateEmitter?.supportsRemoteGateAnswers() === true &&
|
|
678
|
+
(gateEmitter.isUnattended?.() === true || !context?.hasUI);
|
|
557
679
|
|
|
558
680
|
// Headless fallback: unattended workflow gates are the non-TUI answer path.
|
|
559
681
|
if (!canUseWorkflowGate && (!context?.hasUI || !context.ui)) {
|
|
@@ -652,7 +774,11 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
652
774
|
|
|
653
775
|
const askQuestion = async (
|
|
654
776
|
q: AskParams["questions"][number],
|
|
655
|
-
options?: {
|
|
777
|
+
options?: {
|
|
778
|
+
previous?: QuestionResult;
|
|
779
|
+
navigation?: NavigationControls;
|
|
780
|
+
navigationLabel?: "Next" | "Done";
|
|
781
|
+
},
|
|
656
782
|
) => {
|
|
657
783
|
const rawOptionLabels = q.options.map(o => o.label);
|
|
658
784
|
// Unattended (#316/#323/G011): route the question through the workflow-gate
|
|
@@ -663,7 +789,12 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
663
789
|
question: q.question,
|
|
664
790
|
options: q.options,
|
|
665
791
|
multi: q.multi,
|
|
792
|
+
// Interactive multi-select allows finishing with nothing selected
|
|
793
|
+
// ("Next"); the gate path must keep the same empty-answer semantics.
|
|
794
|
+
allowEmpty: q.multi === true,
|
|
795
|
+
navigationLabel: options?.navigationLabel,
|
|
666
796
|
recommended: q.recommended,
|
|
797
|
+
workflowGate: q.workflowGate,
|
|
667
798
|
deepInterview: q.deepInterview,
|
|
668
799
|
};
|
|
669
800
|
const answer = await gateEmitter.emitGate(questionToGate(gateQuestion));
|
|
@@ -672,6 +803,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
672
803
|
optionLabels: rawOptionLabels,
|
|
673
804
|
selectedOptions: decoded.selectedOptions,
|
|
674
805
|
customInput: decoded.customInput,
|
|
806
|
+
clarificationQuestion: decoded.clarificationQuestion,
|
|
675
807
|
navigation: undefined as NavigationControls | undefined,
|
|
676
808
|
cancelled: false,
|
|
677
809
|
timedOut: false,
|
|
@@ -716,7 +848,15 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
716
848
|
return displayIndex >= 0 ? (rawOptionLabels[displayIndex] ?? selected) : selected;
|
|
717
849
|
})
|
|
718
850
|
: displaySelectedOptions;
|
|
719
|
-
return {
|
|
851
|
+
return {
|
|
852
|
+
optionLabels: rawOptionLabels,
|
|
853
|
+
selectedOptions,
|
|
854
|
+
customInput,
|
|
855
|
+
clarificationQuestion: undefined as string | undefined,
|
|
856
|
+
navigation,
|
|
857
|
+
cancelled,
|
|
858
|
+
timedOut,
|
|
859
|
+
};
|
|
720
860
|
} catch (error) {
|
|
721
861
|
if (error instanceof Error && error.name === "AbortError") {
|
|
722
862
|
throw new ToolAbortError("Ask input was cancelled");
|
|
@@ -727,8 +867,12 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
727
867
|
|
|
728
868
|
if (params.questions.length === 1) {
|
|
729
869
|
const [q] = params.questions;
|
|
730
|
-
const { optionLabels, selectedOptions, customInput, cancelled, timedOut } =
|
|
870
|
+
const { optionLabels, selectedOptions, customInput, clarificationQuestion, cancelled, timedOut } =
|
|
871
|
+
await askQuestion(q);
|
|
731
872
|
|
|
873
|
+
if (clarificationQuestion !== undefined) {
|
|
874
|
+
return this.#clarificationResult(q, optionLabels, clarificationQuestion);
|
|
875
|
+
}
|
|
732
876
|
if (!timedOut && (cancelled || (selectedOptions.length === 0 && customInput === undefined))) {
|
|
733
877
|
context?.abort();
|
|
734
878
|
throw new ToolAbortError("Ask tool was cancelled by the user");
|
|
@@ -777,11 +921,21 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
777
921
|
optionLabels,
|
|
778
922
|
selectedOptions,
|
|
779
923
|
customInput,
|
|
924
|
+
clarificationQuestion,
|
|
780
925
|
navigation: navAction,
|
|
781
926
|
cancelled,
|
|
782
927
|
timedOut,
|
|
783
|
-
} = await askQuestion(q, {
|
|
928
|
+
} = await askQuestion(q, {
|
|
929
|
+
previous,
|
|
930
|
+
navigation,
|
|
931
|
+
navigationLabel: questionIndex + 1 < params.questions.length ? "Next" : "Done",
|
|
932
|
+
});
|
|
784
933
|
|
|
934
|
+
// A clarification is an answer about the ask itself: stop the sequence and
|
|
935
|
+
// surface the question instead of treating the empty selection as a cancel.
|
|
936
|
+
if (clarificationQuestion !== undefined) {
|
|
937
|
+
return this.#clarificationResult(q, optionLabels, clarificationQuestion);
|
|
938
|
+
}
|
|
785
939
|
if (cancelled && !timedOut) {
|
|
786
940
|
context?.abort();
|
|
787
941
|
throw new ToolAbortError("Ask tool was cancelled by the user");
|
|
@@ -824,6 +978,30 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
|
|
824
978
|
|
|
825
979
|
return { content: [{ type: "text" as const, text: responseText }], details };
|
|
826
980
|
}
|
|
981
|
+
|
|
982
|
+
/** A clarification answer surfaces the user's question instead of a selection. */
|
|
983
|
+
#clarificationResult(
|
|
984
|
+
q: AskParams["questions"][number],
|
|
985
|
+
optionLabels: string[],
|
|
986
|
+
clarificationQuestion: string,
|
|
987
|
+
): AgentToolResult<AskToolDetails> {
|
|
988
|
+
return {
|
|
989
|
+
content: [
|
|
990
|
+
{
|
|
991
|
+
type: "text" as const,
|
|
992
|
+
text: `User asked a clarification question about the choices: ${clarificationQuestion}`,
|
|
993
|
+
},
|
|
994
|
+
],
|
|
995
|
+
details: {
|
|
996
|
+
question: q.question,
|
|
997
|
+
options: optionLabels,
|
|
998
|
+
multi: q.multi ?? false,
|
|
999
|
+
selectedOptions: [],
|
|
1000
|
+
customInput: undefined,
|
|
1001
|
+
clarificationQuestion,
|
|
1002
|
+
},
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
827
1005
|
}
|
|
828
1006
|
|
|
829
1007
|
// =============================================================================
|
package/src/tools/eval.ts
CHANGED
|
@@ -183,8 +183,8 @@ function timeoutSecondsFromMs(timeoutMs: number): number {
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
async function resolveBackend(session: ToolSession, language: EvalLanguage): Promise<ResolvedBackend> {
|
|
186
|
-
|
|
187
|
-
const
|
|
186
|
+
// SKC_PY wins over the legacy PI_PY/PI_JS env flags, which win over settings.
|
|
187
|
+
const { python: allowPy, js: allowJs } = resolveEvalBackends(session);
|
|
188
188
|
|
|
189
189
|
if (language === "python") {
|
|
190
190
|
if (!allowPy) throw new ToolError("Python backend is disabled (eval.py = false).");
|