@gajae-code/coding-agent 0.15.3 → 0.15.4
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 +6 -0
- package/dist/types/config/settings-schema.d.ts +52 -0
- package/dist/types/extensibility/extensions/types.d.ts +2 -0
- package/dist/types/gjc-runtime/workflow-placeholder.d.ts +6 -0
- package/dist/types/modes/components/hook-selector.d.ts +1 -0
- package/dist/types/tools/ask.d.ts +1 -0
- package/package.json +7 -7
- package/src/config/settings-schema.ts +53 -0
- package/src/config/settings.ts +1 -1
- package/src/deep-interview/render-middleware.ts +6 -2
- package/src/extensibility/extensions/types.ts +2 -0
- package/src/gjc-runtime/ralplan-runtime.ts +6 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +5 -0
- package/src/gjc-runtime/workflow-placeholder.ts +33 -0
- package/src/internal-urls/docs-index.generated.ts +4 -3
- package/src/modes/components/hook-selector.ts +2 -1
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/session/agent-session.ts +70 -10
- package/src/tools/ask-contract.ts +54 -3
- package/src/tools/ask.ts +46 -6
|
@@ -72,6 +72,7 @@ export interface HookSelectorOptions {
|
|
|
72
72
|
customInput?: {
|
|
73
73
|
optionLabel: string;
|
|
74
74
|
onSubmit: (text: string) => void;
|
|
75
|
+
allowEmpty?: boolean;
|
|
75
76
|
};
|
|
76
77
|
clarificationInput?: {
|
|
77
78
|
optionLabel: string;
|
|
@@ -390,7 +391,7 @@ export class HookSelectorComponent extends Container {
|
|
|
390
391
|
#outline: boolean;
|
|
391
392
|
#scrollTitleRows: number | undefined;
|
|
392
393
|
#inlineEditorMaxHeight: number | undefined;
|
|
393
|
-
#customInput: { optionLabel: string; onSubmit: (text: string) => void } | undefined;
|
|
394
|
+
#customInput: { optionLabel: string; onSubmit: (text: string) => void; allowEmpty?: boolean } | undefined;
|
|
394
395
|
#clarificationInput: { optionLabel: string; onSubmit: (text: string) => void; allowEmpty?: boolean } | undefined;
|
|
395
396
|
#activeInput: { onSubmit: (text: string) => void; allowEmpty?: boolean } | undefined;
|
|
396
397
|
#inputArea: Container;
|
|
@@ -1154,6 +1154,7 @@ export class ExtensionUiController {
|
|
|
1154
1154
|
customInput: dialogOptions?.customInput
|
|
1155
1155
|
? {
|
|
1156
1156
|
optionLabel: dialogOptions.customInput.optionLabel,
|
|
1157
|
+
allowEmpty: (dialogOptions.customInput as { allowEmpty?: boolean }).allowEmpty,
|
|
1157
1158
|
onSubmit: text => {
|
|
1158
1159
|
const optionLabel = dialogOptions.customInput?.optionLabel;
|
|
1159
1160
|
this.hideHookSelector();
|
|
@@ -20,6 +20,7 @@ import * as path from "node:path";
|
|
|
20
20
|
import { scheduler } from "node:timers/promises";
|
|
21
21
|
import * as util from "node:util";
|
|
22
22
|
import {
|
|
23
|
+
AdaptiveCompactionTracker,
|
|
23
24
|
type AfterToolCallContext,
|
|
24
25
|
type AfterToolCallResult,
|
|
25
26
|
type Agent,
|
|
@@ -2430,6 +2431,8 @@ export class AgentSession {
|
|
|
2430
2431
|
// required to re-trigger. A content signature (not object identity) is used so
|
|
2431
2432
|
// it survives the message-array rebuild that compaction/prune perform.
|
|
2432
2433
|
#lastMidRunMaintenanceAnchorSignature: string | undefined = undefined;
|
|
2434
|
+
#adaptiveCompactionTracker = new AdaptiveCompactionTracker();
|
|
2435
|
+
#adaptiveCompactionRecordedMessageKey: string | undefined;
|
|
2433
2436
|
#resourceSampler: () => EmergencyCompactionSample = () => this.#defaultResourceSample();
|
|
2434
2437
|
#retainedMemorySampler: (() => RetainedMemorySample) | undefined;
|
|
2435
2438
|
|
|
@@ -7702,6 +7705,7 @@ export class AgentSession {
|
|
|
7702
7705
|
* such event.
|
|
7703
7706
|
*/
|
|
7704
7707
|
#syncAgentSessionId(sessionId?: string): void {
|
|
7708
|
+
this.#resetAdaptiveCompactionState();
|
|
7705
7709
|
this.#reasoningControlContextGeneration++;
|
|
7706
7710
|
const sid = this.#providerSessionId ?? sessionId ?? this.sessionManager.getSessionId();
|
|
7707
7711
|
this.agent.sessionId = sid;
|
|
@@ -15857,6 +15861,7 @@ export class AgentSession {
|
|
|
15857
15861
|
fromExtension,
|
|
15858
15862
|
preserveData,
|
|
15859
15863
|
);
|
|
15864
|
+
this.#recordAdaptiveCompactionReset(tokensBefore);
|
|
15860
15865
|
await this.#applyCompactionPostAppend(compactionEntryId, firstKeptEntryId, fromExtension);
|
|
15861
15866
|
|
|
15862
15867
|
const compactionResult: CompactionResult = {
|
|
@@ -16380,7 +16385,12 @@ export class AgentSession {
|
|
|
16380
16385
|
// which breaks the provider prompt-cache prefix mid-epoch. Only prune at a
|
|
16381
16386
|
// sanctioned maintenance boundary, i.e. when the un-pruned context already
|
|
16382
16387
|
// crosses the compaction threshold. Pruning may then avert full compaction.
|
|
16383
|
-
|
|
16388
|
+
const adaptiveCompactionSettings = this.#recordAdaptiveCompactionCall(
|
|
16389
|
+
contextTokens,
|
|
16390
|
+
compactionSettings,
|
|
16391
|
+
assistantMessage,
|
|
16392
|
+
);
|
|
16393
|
+
if (!shouldCompact(contextTokens, contextWindow, adaptiveCompactionSettings, autoCompactionOutputReserveTokens))
|
|
16384
16394
|
return true;
|
|
16385
16395
|
const pruneEstimate = estimateToolOutputPruneSavings(this.sessionManager.getBranch(), DEFAULT_PRUNE_CONFIG, {
|
|
16386
16396
|
relaxedMinimum: 0,
|
|
@@ -16391,7 +16401,7 @@ export class AgentSession {
|
|
|
16391
16401
|
!shouldCompact(
|
|
16392
16402
|
Math.max(0, contextTokens - pruneEstimate.tokensSaved),
|
|
16393
16403
|
contextWindow,
|
|
16394
|
-
|
|
16404
|
+
adaptiveCompactionSettings,
|
|
16395
16405
|
autoCompactionOutputReserveTokens,
|
|
16396
16406
|
)
|
|
16397
16407
|
) {
|
|
@@ -16399,7 +16409,8 @@ export class AgentSession {
|
|
|
16399
16409
|
if (ownershipSignal?.aborted) return false;
|
|
16400
16410
|
if (pruneResult) contextTokens = Math.max(0, contextTokens - pruneResult.tokensSaved);
|
|
16401
16411
|
}
|
|
16402
|
-
|
|
16412
|
+
const prunedCompactionSettings = this.#compactionSettingsWithAdaptiveState(compactionSettings);
|
|
16413
|
+
if (shouldCompact(contextTokens, contextWindow, prunedCompactionSettings, autoCompactionOutputReserveTokens)) {
|
|
16403
16414
|
// Try promotion first — if a larger model is available, switch instead of compacting
|
|
16404
16415
|
const promoted = await this.#tryContextPromotion(assistantMessage, ownershipSignal);
|
|
16405
16416
|
if (ownershipSignal?.aborted) return false;
|
|
@@ -16453,7 +16464,7 @@ export class AgentSession {
|
|
|
16453
16464
|
}
|
|
16454
16465
|
if (isAborted()) return result("aborted");
|
|
16455
16466
|
|
|
16456
|
-
//
|
|
16467
|
+
// context-full maintenance. "off" defers entirely; "handoff"
|
|
16457
16468
|
// keeps its existing agent_end / pre-prompt boundaries (a mid-tool-loop
|
|
16458
16469
|
// session swap would be far more disruptive than the overflow it avoids).
|
|
16459
16470
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
@@ -16469,7 +16480,12 @@ export class AgentSession {
|
|
|
16469
16480
|
const autoCompactionOutputReserveTokens = 0;
|
|
16470
16481
|
const anchor = this.#findMidRunUsageAnchor(context.messages);
|
|
16471
16482
|
let contextTokens = this.#estimateMidRunContextTokens(context.messages);
|
|
16472
|
-
|
|
16483
|
+
const adaptiveCompactionSettings = anchor
|
|
16484
|
+
? this.#recordAdaptiveCompactionCall(contextTokens, compactionSettings, anchor.message)
|
|
16485
|
+
: this.#compactionSettingsWithAdaptiveState(compactionSettings);
|
|
16486
|
+
if (
|
|
16487
|
+
!shouldCompact(contextTokens, contextWindow, adaptiveCompactionSettings, autoCompactionOutputReserveTokens)
|
|
16488
|
+
) {
|
|
16473
16489
|
return result("not-needed");
|
|
16474
16490
|
}
|
|
16475
16491
|
// Anti-loop (#1662): a given provider response anchors at most one
|
|
@@ -16503,7 +16519,7 @@ export class AgentSession {
|
|
|
16503
16519
|
!shouldCompact(
|
|
16504
16520
|
Math.max(0, contextTokens - pruneEstimate.tokensSaved),
|
|
16505
16521
|
contextWindow,
|
|
16506
|
-
|
|
16522
|
+
adaptiveCompactionSettings,
|
|
16507
16523
|
autoCompactionOutputReserveTokens,
|
|
16508
16524
|
)
|
|
16509
16525
|
) {
|
|
@@ -16512,7 +16528,9 @@ export class AgentSession {
|
|
|
16512
16528
|
if (pruneResult?.failure === "artifact_persistence") return result("failed");
|
|
16513
16529
|
if (pruneResult) contextTokens = Math.max(0, contextTokens - pruneResult.tokensSaved);
|
|
16514
16530
|
}
|
|
16515
|
-
if (
|
|
16531
|
+
if (
|
|
16532
|
+
!shouldCompact(contextTokens, contextWindow, adaptiveCompactionSettings, autoCompactionOutputReserveTokens)
|
|
16533
|
+
) {
|
|
16516
16534
|
return pruneResult?.committed ? result("pruned", true) : result("not-needed");
|
|
16517
16535
|
}
|
|
16518
16536
|
|
|
@@ -16675,7 +16693,8 @@ export class AgentSession {
|
|
|
16675
16693
|
// Model maxTokens is a capability ceiling, not a per-turn reservation.
|
|
16676
16694
|
// Auto maintenance should track actual context fullness.
|
|
16677
16695
|
const autoCompactionOutputReserveTokens = 0;
|
|
16678
|
-
|
|
16696
|
+
const adaptiveCompactionSettings = this.#compactionSettingsWithAdaptiveState(compactionSettings);
|
|
16697
|
+
if (!shouldCompact(contextTokens, contextWindow, adaptiveCompactionSettings, autoCompactionOutputReserveTokens)) {
|
|
16679
16698
|
// Below the compaction threshold: optionally run evidence-gated maintenance
|
|
16680
16699
|
// pruning (opt-in, high savings + cache-epoch payback required).
|
|
16681
16700
|
await this.#maybeRunBelowThresholdMaintenancePrune();
|
|
@@ -16691,14 +16710,15 @@ export class AgentSession {
|
|
|
16691
16710
|
!shouldCompact(
|
|
16692
16711
|
Math.max(0, contextTokens - pruneEstimate.tokensSaved),
|
|
16693
16712
|
contextWindow,
|
|
16694
|
-
|
|
16713
|
+
adaptiveCompactionSettings,
|
|
16695
16714
|
autoCompactionOutputReserveTokens,
|
|
16696
16715
|
)
|
|
16697
16716
|
) {
|
|
16698
16717
|
const pruneResult = await this.#pruneToolOutputs(undefined, true);
|
|
16699
16718
|
if (pruneResult) contextTokens = Math.max(0, contextTokens - pruneResult.tokensSaved);
|
|
16700
16719
|
}
|
|
16701
|
-
|
|
16720
|
+
const prunedCompactionSettings = this.#compactionSettingsWithAdaptiveState(compactionSettings);
|
|
16721
|
+
if (shouldCompact(contextTokens, contextWindow, prunedCompactionSettings, autoCompactionOutputReserveTokens)) {
|
|
16702
16722
|
await this.#runAutoCompaction("threshold", false, false, {
|
|
16703
16723
|
continueAfterMaintenance: false,
|
|
16704
16724
|
deferHandoffMaintenance: false,
|
|
@@ -16706,6 +16726,45 @@ export class AgentSession {
|
|
|
16706
16726
|
}
|
|
16707
16727
|
}
|
|
16708
16728
|
|
|
16729
|
+
#recordAdaptiveCompactionCall<T extends { adaptive?: { enabled: boolean; turnWindow: number } }>(
|
|
16730
|
+
contextTokens: number,
|
|
16731
|
+
settings: T,
|
|
16732
|
+
assistantMessage: AssistantMessage,
|
|
16733
|
+
): T {
|
|
16734
|
+
if (!settings.adaptive?.enabled) return settings;
|
|
16735
|
+
this.#adaptiveCompactionTracker.setWindowMs(settings.adaptive.turnWindow * 60_000);
|
|
16736
|
+
const messageKey = this.#adaptiveCompactionMessageKey(assistantMessage);
|
|
16737
|
+
if (this.#adaptiveCompactionRecordedMessageKey === messageKey) {
|
|
16738
|
+
return this.#compactionSettingsWithAdaptiveState(settings);
|
|
16739
|
+
}
|
|
16740
|
+
this.#adaptiveCompactionRecordedMessageKey = messageKey;
|
|
16741
|
+
this.#adaptiveCompactionTracker.recordCall(contextTokens);
|
|
16742
|
+
return this.#compactionSettingsWithAdaptiveState(settings);
|
|
16743
|
+
}
|
|
16744
|
+
|
|
16745
|
+
#adaptiveCompactionMessageKey(message: AssistantMessage): string {
|
|
16746
|
+
return `${message.provider}\u0000${message.model}\u0000${message.timestamp}\u0000${message.usage?.totalTokens ?? ""}`;
|
|
16747
|
+
}
|
|
16748
|
+
|
|
16749
|
+
#compactionSettingsWithAdaptiveState<T extends { adaptive?: { enabled: boolean } }>(settings: T): T {
|
|
16750
|
+
if (!settings.adaptive?.enabled) return settings;
|
|
16751
|
+
return {
|
|
16752
|
+
...settings,
|
|
16753
|
+
adaptiveState: this.#adaptiveCompactionTracker.decisionState(),
|
|
16754
|
+
};
|
|
16755
|
+
}
|
|
16756
|
+
|
|
16757
|
+
#resetAdaptiveCompactionState(): void {
|
|
16758
|
+
this.#adaptiveCompactionTracker.reset();
|
|
16759
|
+
this.#adaptiveCompactionRecordedMessageKey = undefined;
|
|
16760
|
+
}
|
|
16761
|
+
|
|
16762
|
+
#recordAdaptiveCompactionReset(contextTokens: number): void {
|
|
16763
|
+
this.#adaptiveCompactionTracker.reset();
|
|
16764
|
+
this.#adaptiveCompactionTracker.recordCompact(contextTokens);
|
|
16765
|
+
this.#adaptiveCompactionRecordedMessageKey = undefined;
|
|
16766
|
+
}
|
|
16767
|
+
|
|
16709
16768
|
#assistantEndedWithSuccessfulYield(assistantMessage: AssistantMessage): boolean {
|
|
16710
16769
|
const toolCallId = this.#lastSuccessfulYieldToolCallId;
|
|
16711
16770
|
if (!toolCallId) return false;
|
|
@@ -18156,6 +18215,7 @@ export class AgentSession {
|
|
|
18156
18215
|
fromExtension,
|
|
18157
18216
|
preserveData,
|
|
18158
18217
|
);
|
|
18218
|
+
this.#recordAdaptiveCompactionReset(tokensBefore);
|
|
18159
18219
|
await this.#applyCompactionPostAppend(compactionEntryId, firstKeptEntryId, fromExtension);
|
|
18160
18220
|
if (autoCompactionSignal.aborted) return await emitAborted();
|
|
18161
18221
|
|
|
@@ -7,6 +7,11 @@ import { INTENT_FIELD } from "@gajae-code/agent-core";
|
|
|
7
7
|
import type { RawArgumentValidationResult } from "@gajae-code/ai/types";
|
|
8
8
|
import * as z from "zod/v4";
|
|
9
9
|
import { deepInterviewCharacterCount } from "../gjc-runtime/deep-interview-state";
|
|
10
|
+
import {
|
|
11
|
+
isLegacyDeepInterviewPlaceholder,
|
|
12
|
+
isWorkflowPlaceholderText,
|
|
13
|
+
WORKFLOW_PLACEHOLDER_CORRECTION,
|
|
14
|
+
} from "../gjc-runtime/workflow-placeholder";
|
|
10
15
|
|
|
11
16
|
function deepInterviewBoundedString(maximum: number) {
|
|
12
17
|
return z.string().superRefine((value, context) => {
|
|
@@ -146,6 +151,13 @@ function createQuestionItemSchema(deepInterviewSchema: z.ZodType<DeepInterviewMe
|
|
|
146
151
|
workflowGate: WorkflowGateMeta.describe("optional workflow gate stage/kind override").optional(),
|
|
147
152
|
})
|
|
148
153
|
.superRefine((value, context) => {
|
|
154
|
+
if (value.deepInterview !== undefined && isWorkflowPlaceholderText(value.question)) {
|
|
155
|
+
context.addIssue({
|
|
156
|
+
code: "custom",
|
|
157
|
+
message: `deep-interview question body must ${WORKFLOW_PLACEHOLDER_CORRECTION}`,
|
|
158
|
+
path: ["question"],
|
|
159
|
+
});
|
|
160
|
+
}
|
|
149
161
|
const labels = new Set(value.options.map(option => option.label));
|
|
150
162
|
const contract = intentContract(value.deepInterview);
|
|
151
163
|
const review = intentReview(value.deepInterview);
|
|
@@ -459,19 +471,58 @@ function knownIntentRejection(arguments_: Record<string, unknown>): RawArgumentV
|
|
|
459
471
|
return { outcome: "reject", code: "ask-intent-contract-requires-non-empty-authority" };
|
|
460
472
|
return undefined;
|
|
461
473
|
}
|
|
474
|
+
|
|
475
|
+
function deepInterviewPlaceholderRejection(
|
|
476
|
+
arguments_: Record<string, unknown>,
|
|
477
|
+
): RawArgumentValidationResult | undefined {
|
|
478
|
+
const root = parseEncodedContainer(arguments_);
|
|
479
|
+
if (!isPlainRecord(root)) return undefined;
|
|
480
|
+
const questions = parseEncodedContainer(root.questions);
|
|
481
|
+
if (!Array.isArray(questions)) return undefined;
|
|
482
|
+
for (const [index, rawValue] of questions.entries()) {
|
|
483
|
+
const rawQuestion = parseEncodedContainer(rawValue);
|
|
484
|
+
if (!isPlainRecord(rawQuestion) || !Object.hasOwn(rawQuestion, "deepInterview")) continue;
|
|
485
|
+
const metadata = parseEncodedContainer(rawQuestion.deepInterview);
|
|
486
|
+
if (metadata !== null && metadata !== undefined && isWorkflowPlaceholderText(rawQuestion.question)) {
|
|
487
|
+
return {
|
|
488
|
+
outcome: "reject",
|
|
489
|
+
code: "ask-deep-interview-question-body-required",
|
|
490
|
+
detail: {
|
|
491
|
+
rejectedKeys: [`questions[${index}].question`],
|
|
492
|
+
hint: WORKFLOW_PLACEHOLDER_CORRECTION,
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
for (const [index, rawValue] of questions.entries()) {
|
|
498
|
+
const question = parseEncodedContainer(rawValue);
|
|
499
|
+
if (isPlainRecord(question) && isLegacyDeepInterviewPlaceholder(question.question)) {
|
|
500
|
+
return {
|
|
501
|
+
outcome: "reject",
|
|
502
|
+
code: "ask-deep-interview-question-body-required",
|
|
503
|
+
detail: { rejectedKeys: [`questions[${index}].question`], hint: WORKFLOW_PLACEHOLDER_CORRECTION },
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return undefined;
|
|
508
|
+
}
|
|
509
|
+
|
|
462
510
|
export function recoverRoundZeroIntentContract(
|
|
463
511
|
arguments_: Record<string, unknown>,
|
|
464
512
|
stage?: "topology" | "post-topology",
|
|
465
513
|
): RawArgumentValidationResult {
|
|
514
|
+
const normalizedInput = normalizeRoundZeroOptionalNulls(arguments_);
|
|
515
|
+
const placeholderRejection = deepInterviewPlaceholderRejection(normalizedInput);
|
|
516
|
+
if (placeholderRejection) return placeholderRejection;
|
|
466
517
|
// #4649: an incomplete Round-0 topology object (deepInterview present,
|
|
467
518
|
// required fields omitted) is NOT a retired-pair recovery candidate, so it
|
|
468
519
|
// would passthrough into generic Zod validation whose error names no contract
|
|
469
520
|
// and no correction — the repeat-invalid-bisect loop. Non-candidates get the
|
|
470
521
|
// targeted correction here; candidates get it only after the stricter known
|
|
471
522
|
// intent rejections below, so every previously-covered verdict is unchanged.
|
|
472
|
-
if (!isRoundZeroRecoveryCandidate(
|
|
473
|
-
return roundZeroMetadataRejection(
|
|
474
|
-
const normalizedArguments =
|
|
523
|
+
if (!isRoundZeroRecoveryCandidate(normalizedInput))
|
|
524
|
+
return roundZeroMetadataRejection(normalizedInput, stage) ?? { outcome: "passthrough" };
|
|
525
|
+
const normalizedArguments = normalizedInput;
|
|
475
526
|
const knownRejection = knownIntentRejection(normalizedArguments);
|
|
476
527
|
if (knownRejection) return knownRejection;
|
|
477
528
|
const missingTopologyFields = roundZeroMetadataRejection(normalizedArguments, stage);
|
package/src/tools/ask.ts
CHANGED
|
@@ -114,11 +114,17 @@ const HEADLESS_CHECKBOX_CHECKED = "[x]";
|
|
|
114
114
|
const HEADLESS_CHECKBOX_UNCHECKED = "[ ]";
|
|
115
115
|
const DEEP_INTERVIEW_SELECTOR_SCROLL_TITLE_ROWS = Number.MAX_SAFE_INTEGER;
|
|
116
116
|
const DEEP_INTERVIEW_RECORDER_AWAIT_TIMEOUT_MS = 250;
|
|
117
|
+
export const MAX_EMPTY_CUSTOM_ATTEMPTS = 3;
|
|
118
|
+
const EMPTY_CUSTOM_RETRY_MESSAGE = "Custom input cannot be empty. Enter text or cancel the ask.";
|
|
117
119
|
|
|
118
120
|
function errorMessage(error: unknown): string {
|
|
119
121
|
return error instanceof Error ? error.message : String(error);
|
|
120
122
|
}
|
|
121
123
|
|
|
124
|
+
function nonEmptyCustomInput(value: string | undefined): string | undefined {
|
|
125
|
+
return value !== undefined && value.trim().length > 0 ? value : undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
122
128
|
function isAskTimeoutError(error: unknown): boolean {
|
|
123
129
|
return typeof error === "object" && error !== null && (error as { code?: unknown }).code === GJC_ASK_TIMEOUT_CODE;
|
|
124
130
|
}
|
|
@@ -324,7 +330,7 @@ interface UIContext {
|
|
|
324
330
|
onLeft?: () => void;
|
|
325
331
|
onRight?: () => void;
|
|
326
332
|
helpText?: string;
|
|
327
|
-
customInput?: { optionLabel: string; onSubmit: (text: string) => void };
|
|
333
|
+
customInput?: { optionLabel: string; onSubmit: (text: string) => void; allowEmpty?: boolean };
|
|
328
334
|
clarificationInput?: { optionLabel: string; onSubmit: (text: string) => void; allowEmpty?: boolean };
|
|
329
335
|
},
|
|
330
336
|
): Promise<string | undefined>;
|
|
@@ -400,6 +406,7 @@ async function askSingleQuestion(
|
|
|
400
406
|
helpText,
|
|
401
407
|
customInput: {
|
|
402
408
|
optionLabel: otherOptionLabel,
|
|
409
|
+
allowEmpty: false,
|
|
403
410
|
onSubmit: (text: string) => {
|
|
404
411
|
inlineInput = text;
|
|
405
412
|
},
|
|
@@ -541,7 +548,9 @@ async function askSingleQuestion(
|
|
|
541
548
|
timedOut = true;
|
|
542
549
|
break;
|
|
543
550
|
}
|
|
544
|
-
const input =
|
|
551
|
+
const input = nonEmptyCustomInput(
|
|
552
|
+
inlineInput !== undefined ? inlineInput : (await promptForCustomInput()).input,
|
|
553
|
+
);
|
|
545
554
|
if (input === undefined) {
|
|
546
555
|
break;
|
|
547
556
|
}
|
|
@@ -642,7 +651,9 @@ async function askSingleQuestion(
|
|
|
642
651
|
}
|
|
643
652
|
} else if (choice === otherOptionLabel) {
|
|
644
653
|
if (!selectTimedOut) {
|
|
645
|
-
const input =
|
|
654
|
+
const input = nonEmptyCustomInput(
|
|
655
|
+
inlineInput !== undefined ? inlineInput : (await promptForCustomInput()).input,
|
|
656
|
+
);
|
|
646
657
|
if (input !== undefined) {
|
|
647
658
|
customInput = input;
|
|
648
659
|
selectedOptions = [];
|
|
@@ -1100,10 +1111,15 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
|
|
|
1100
1111
|
|
|
1101
1112
|
const askQuestion = async (
|
|
1102
1113
|
q: AskParams["questions"][number],
|
|
1103
|
-
options?: {
|
|
1114
|
+
options?: {
|
|
1115
|
+
previous?: QuestionResult;
|
|
1116
|
+
navigation?: NavigationControls;
|
|
1117
|
+
emptyCustomAttempt?: number;
|
|
1118
|
+
},
|
|
1104
1119
|
) => {
|
|
1105
1120
|
const rawOptionLabels = q.options.map(o => o.label);
|
|
1106
1121
|
const questionIndex = params.questions.indexOf(q);
|
|
1122
|
+
const emptyCustomAttempt = options?.emptyCustomAttempt ?? 0;
|
|
1107
1123
|
// Route headless asks through the SDK workflow-gate emitter; a connected
|
|
1108
1124
|
// SDK responder supplies the durable answer instead of an interactive UI.
|
|
1109
1125
|
if (gateEmitter && canUseWorkflowGate) {
|
|
@@ -1133,7 +1149,11 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
|
|
|
1133
1149
|
try {
|
|
1134
1150
|
const deepInterviewPrompt = formatDeepInterviewSelectorPrompt(q.question);
|
|
1135
1151
|
const isDeepInterviewQuestion = deepInterviewPrompt !== null || q.deepInterview !== undefined;
|
|
1136
|
-
const
|
|
1152
|
+
const baseDisplayQuestion = deepInterviewPrompt ?? q.question;
|
|
1153
|
+
const displayQuestion =
|
|
1154
|
+
emptyCustomAttempt > 0
|
|
1155
|
+
? `${baseDisplayQuestion}\n\n${EMPTY_CUSTOM_RETRY_MESSAGE} (attempt ${emptyCustomAttempt + 1} of ${MAX_EMPTY_CUSTOM_ATTEMPTS})`
|
|
1156
|
+
: baseDisplayQuestion;
|
|
1137
1157
|
const shouldNumberOptions = isDeepInterviewQuestion || isDeepInterviewAskQuestion(q.question);
|
|
1138
1158
|
const optionLabels = shouldNumberOptions ? numberOptionLabels(rawOptionLabels) : rawOptionLabels;
|
|
1139
1159
|
const clarificationOptionLabel = shouldNumberOptions
|
|
@@ -1256,7 +1276,27 @@ export class AskTool implements AgentTool<AskParametersSchema, AskToolDetails> {
|
|
|
1256
1276
|
: { kind: "resolve_without_commit", reason: "cancelled" };
|
|
1257
1277
|
await settleActiveRemote(settlement);
|
|
1258
1278
|
activeRemoteRequest = undefined;
|
|
1259
|
-
if (settlement.kind === "invalid")
|
|
1279
|
+
if (settlement.kind === "invalid") {
|
|
1280
|
+
if (emptyCustomAttempt + 1 >= MAX_EMPTY_CUSTOM_ATTEMPTS) {
|
|
1281
|
+
return {
|
|
1282
|
+
optionLabels: rawOptionLabels,
|
|
1283
|
+
selectedOptions: [] as string[],
|
|
1284
|
+
customInput: undefined,
|
|
1285
|
+
clarificationQuestion: undefined,
|
|
1286
|
+
navigation: undefined,
|
|
1287
|
+
cancelled: true,
|
|
1288
|
+
timedOut: false,
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
// A remote source may resolve synchronously. Yield to a timer queue
|
|
1292
|
+
// between retries so repeated invalid answers cannot starve the
|
|
1293
|
+
// event loop indefinitely.
|
|
1294
|
+
await Bun.sleep(1);
|
|
1295
|
+
return askQuestion(q, {
|
|
1296
|
+
...options,
|
|
1297
|
+
emptyCustomAttempt: emptyCustomAttempt + 1,
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1260
1300
|
}
|
|
1261
1301
|
activeRemoteRequest = undefined;
|
|
1262
1302
|
return {
|