feat(core): experimental in-progress steering hints (2 of 2) (#19307)

This commit is contained in:
joshualitt
2026-02-18 14:05:50 -08:00
committed by GitHub
parent 81c8893e05
commit 87f5dd15d6
37 changed files with 1280 additions and 48 deletions

View File

@@ -60,6 +60,7 @@ import { getToolCallContext } from '../utils/toolCallContext.js';
import { scheduleAgentTools } from './agent-scheduler.js';
import { DeadlineTimer } from '../utils/deadlineTimer.js';
import { LlmRole } from '../telemetry/types.js';
import { formatUserHintsForModel } from '../utils/fastAckHelper.js';
/** A callback function to report on agent activity. */
export type ActivityCallback = (activity: SubagentActivityEvent) => void;
@@ -463,45 +464,82 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
const query = this.definition.promptConfig.query
? templateString(this.definition.promptConfig.query, augmentedInputs)
: DEFAULT_QUERY_STRING;
let currentMessage: Content = { role: 'user', parts: [{ text: query }] };
while (true) {
// Check for termination conditions like max turns.
const reason = this.checkTermination(turnCounter, maxTurns);
if (reason) {
terminateReason = reason;
break;
}
const pendingHintsQueue: string[] = [];
const hintListener = (hint: string) => {
pendingHintsQueue.push(hint);
};
// Capture the index of the last hint before starting to avoid re-injecting old hints.
// NOTE: Hints added AFTER this point will be broadcast to all currently running
// local agents via the listener below.
const startIndex =
this.runtimeContext.userHintService.getLatestHintIndex();
this.runtimeContext.userHintService.onUserHint(hintListener);
// Check for timeout or external abort.
if (combinedSignal.aborted) {
// Determine which signal caused the abort.
terminateReason = deadlineTimer.signal.aborted
? AgentTerminateMode.TIMEOUT
: AgentTerminateMode.ABORTED;
break;
}
try {
const initialHints =
this.runtimeContext.userHintService.getUserHintsAfter(startIndex);
const formattedInitialHints = formatUserHintsForModel(initialHints);
const turnResult = await this.executeTurn(
chat,
currentMessage,
turnCounter++,
combinedSignal,
deadlineTimer.signal,
onWaitingForConfirmation,
);
let currentMessage: Content = formattedInitialHints
? {
role: 'user',
parts: [{ text: formattedInitialHints }, { text: query }],
}
: { role: 'user', parts: [{ text: query }] };
if (turnResult.status === 'stop') {
terminateReason = turnResult.terminateReason;
// Only set finalResult if the turn provided one (e.g., error or goal).
if (turnResult.finalResult) {
finalResult = turnResult.finalResult;
while (true) {
// Check for termination conditions like max turns.
const reason = this.checkTermination(turnCounter, maxTurns);
if (reason) {
terminateReason = reason;
break;
}
break; // Exit the loop for *any* stop reason.
}
// If status is 'continue', update message for the next loop
currentMessage = turnResult.nextMessage;
// Check for timeout or external abort.
if (combinedSignal.aborted) {
// Determine which signal caused the abort.
terminateReason = deadlineTimer.signal.aborted
? AgentTerminateMode.TIMEOUT
: AgentTerminateMode.ABORTED;
break;
}
const turnResult = await this.executeTurn(
chat,
currentMessage,
turnCounter++,
combinedSignal,
deadlineTimer.signal,
onWaitingForConfirmation,
);
if (turnResult.status === 'stop') {
terminateReason = turnResult.terminateReason;
// Only set finalResult if the turn provided one (e.g., error or goal).
if (turnResult.finalResult) {
finalResult = turnResult.finalResult;
}
break; // Exit the loop for *any* stop reason.
}
// If status is 'continue', update message for the next loop
currentMessage = turnResult.nextMessage;
// Check for new user steering hints collected via subscription
if (pendingHintsQueue.length > 0) {
const hintsToProcess = [...pendingHintsQueue];
pendingHintsQueue.length = 0;
const formattedHints = formatUserHintsForModel(hintsToProcess);
if (formattedHints) {
// Append hints to the current message (next turn)
currentMessage.parts ??= [];
currentMessage.parts.unshift({ text: formattedHints });
}
}
}
} finally {
this.runtimeContext.userHintService.offUserHint(hintListener);
}
// === UNIFIED RECOVERY BLOCK ===