fix(core): source-aware injection getters, fix unshift ordering, remove dead code

Rename getUserHints/getUserHintsAfter/getLatestHintIndex to
getInjections/getInjectionsAfter/getLatestInjectionIndex with optional
source filter so bg completions don't get formatted as user hints.

Swap unshift ordering so bg completions appear before user hints in the
message — the model sees context before the user's reaction to it.

Remove unused getLastUserHintAt().
This commit is contained in:
Adam Weidman
2026-03-15 22:06:29 -04:00
parent 8fcb18996a
commit 2727a871c3
5 changed files with 69 additions and 58 deletions
+17 -14
View File
@@ -538,12 +538,14 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
// 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.config.injectionService.getLatestHintIndex();
const startIndex = this.config.injectionService.getLatestInjectionIndex();
this.config.injectionService.onInjection(injectionListener);
try {
const initialHints =
this.config.injectionService.getUserHintsAfter(startIndex);
const initialHints = this.config.injectionService.getInjectionsAfter(
startIndex,
'user_steering',
);
const formattedInitialHints = formatUserHintsForModel(initialHints);
let currentMessage: Content = formattedInitialHints
@@ -591,17 +593,9 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
// If status is 'continue', update message for the next loop
currentMessage = turnResult.nextMessage;
// Inject background completion output into the next turn.
if (pendingBgCompletionsQueue.length > 0) {
const bgText = pendingBgCompletionsQueue.join('\n');
pendingBgCompletionsQueue.length = 0;
currentMessage.parts ??= [];
currentMessage.parts.unshift({
text: `Background execution update:\n${bgText}\n\nThe above background execution has completed. Review the output and continue your work accordingly.`,
});
}
// Check for new user steering hints collected via subscription.
// Prepend inter-turn injections. User hints are unshifted first so
// that bg completions (unshifted second) appear before them in the
// final message — the model sees context before the user's reaction.
if (pendingHintsQueue.length > 0) {
const hintsToProcess = [...pendingHintsQueue];
pendingHintsQueue.length = 0;
@@ -611,6 +605,15 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
currentMessage.parts.unshift({ text: formattedHints });
}
}
if (pendingBgCompletionsQueue.length > 0) {
const bgText = pendingBgCompletionsQueue.join('\n');
pendingBgCompletionsQueue.length = 0;
currentMessage.parts ??= [];
currentMessage.parts.unshift({
text: `Background execution update:\n${bgText}\n\nThe above background execution has completed. Review the output and continue your work accordingly.`,
});
}
}
} finally {
this.config.injectionService.offInjection(injectionListener);
+3 -2
View File
@@ -137,7 +137,7 @@ class SubAgentInvocation extends BaseToolInvocation<AgentInputs, ToolResult> {
_toolName ?? definition.name,
_toolDisplayName ?? definition.displayName ?? definition.name,
);
this.startIndex = context.config.injectionService.getLatestHintIndex();
this.startIndex = context.config.injectionService.getLatestInjectionIndex();
}
private get config(): Config {
@@ -200,8 +200,9 @@ class SubAgentInvocation extends BaseToolInvocation<AgentInputs, ToolResult> {
return agentArgs;
}
const userHints = this.config.injectionService.getUserHintsAfter(
const userHints = this.config.injectionService.getInjectionsAfter(
this.startIndex,
'user_steering',
);
const formattedHints = formatUserHintsForModel(userHints);
if (!formattedHints) {
@@ -11,8 +11,8 @@ describe('InjectionService', () => {
it('is disabled by default and ignores user_steering injections', () => {
const service = new InjectionService(() => false);
service.addInjection('this hint should be ignored', 'user_steering');
expect(service.getUserHints()).toEqual([]);
expect(service.getLatestHintIndex()).toBe(-1);
expect(service.getInjections()).toEqual([]);
expect(service.getLatestInjectionIndex()).toBe(-1);
});
it('stores trimmed injections and exposes them via indexing when enabled', () => {
@@ -22,25 +22,14 @@ describe('InjectionService', () => {
service.addInjection('second hint', 'user_steering');
service.addInjection(' ', 'user_steering');
expect(service.getUserHints()).toEqual(['first hint', 'second hint']);
expect(service.getLatestHintIndex()).toBe(1);
expect(service.getUserHintsAfter(-1)).toEqual([
expect(service.getInjections()).toEqual(['first hint', 'second hint']);
expect(service.getLatestInjectionIndex()).toBe(1);
expect(service.getInjectionsAfter(-1)).toEqual([
'first hint',
'second hint',
]);
expect(service.getUserHintsAfter(0)).toEqual(['second hint']);
expect(service.getUserHintsAfter(1)).toEqual([]);
});
it('tracks the last injection timestamp', () => {
const service = new InjectionService(() => true);
expect(service.getLastUserHintAt()).toBeNull();
service.addInjection('hint', 'user_steering');
const timestamp = service.getLastUserHintAt();
expect(timestamp).not.toBeNull();
expect(typeof timestamp).toBe('number');
expect(service.getInjectionsAfter(0)).toEqual(['second hint']);
expect(service.getInjectionsAfter(1)).toEqual([]);
});
it('notifies listeners when an injection is added', () => {
@@ -68,11 +57,11 @@ describe('InjectionService', () => {
const service = new InjectionService(() => true);
service.addInjection('hint 1', 'user_steering');
service.addInjection('hint 2', 'user_steering');
expect(service.getUserHints()).toHaveLength(2);
expect(service.getInjections()).toHaveLength(2);
service.clear();
expect(service.getUserHints()).toHaveLength(0);
expect(service.getLatestHintIndex()).toBe(-1);
expect(service.getInjections()).toHaveLength(0);
expect(service.getLatestInjectionIndex()).toBe(-1);
});
describe('source-specific behavior', () => {
@@ -110,7 +99,30 @@ describe('InjectionService', () => {
'bg output',
'background_completion',
);
expect(service.getUserHints()).toEqual(['bg output']);
expect(service.getInjections()).toEqual(['bg output']);
});
it('filters injections by source when requested', () => {
const service = new InjectionService(() => true);
service.addInjection('hint', 'user_steering');
service.addInjection('bg output', 'background_completion');
service.addInjection('hint 2', 'user_steering');
expect(service.getInjections('user_steering')).toEqual([
'hint',
'hint 2',
]);
expect(service.getInjections('background_completion')).toEqual([
'bg output',
]);
expect(service.getInjections()).toEqual(['hint', 'bg output', 'hint 2']);
expect(service.getInjectionsAfter(0, 'user_steering')).toEqual([
'hint 2',
]);
expect(service.getInjectionsAfter(0, 'background_completion')).toEqual([
'bg output',
]);
});
it('rejects user_steering when model steering is disabled', () => {
@@ -121,7 +133,7 @@ describe('InjectionService', () => {
service.addInjection('steering hint', 'user_steering');
expect(listener).not.toHaveBeenCalled();
expect(service.getUserHints()).toEqual([]);
expect(service.getInjections()).toEqual([]);
});
});
});
+13 -18
View File
@@ -69,39 +69,34 @@ export class InjectionService {
}
/**
* Returns all collected injection texts (all sources).
* Returns collected injection texts, optionally filtered by source.
*/
getUserHints(): string[] {
return this.injections.map((h) => h.text);
getInjections(source?: InjectionSource): string[] {
const items = source
? this.injections.filter((h) => h.source === source)
: this.injections;
return items.map((h) => h.text);
}
/**
* Returns injection texts added after a specific index.
* Returns injection texts added after a specific index, optionally filtered by source.
*/
getUserHintsAfter(index: number): string[] {
getInjectionsAfter(index: number, source?: InjectionSource): string[] {
if (index < 0) {
return this.getUserHints();
return this.getInjections(source);
}
return this.injections.slice(index + 1).map((h) => h.text);
const items = this.injections.slice(index + 1);
const filtered = source ? items.filter((h) => h.source === source) : items;
return filtered.map((h) => h.text);
}
/**
* Returns the index of the latest injection.
*/
getLatestHintIndex(): number {
getLatestInjectionIndex(): number {
return this.injections.length - 1;
}
/**
* Returns the timestamp of the last injection.
*/
getLastUserHintAt(): number | null {
if (this.injections.length === 0) {
return null;
}
return this.injections[this.injections.length - 1].timestamp;
}
/**
* Clears all collected injections.
*/
@@ -339,7 +339,7 @@ describe('ExecutionLifecycleService', () => {
'',
undefined,
'none',
(output, error) => error ? `Error: ${error.message}` : output,
(output, error) => (error ? `Error: ${error.message}` : output),
);
const executionId = handle.pid!;