diff --git a/packages/core/src/agents/local-executor.ts b/packages/core/src/agents/local-executor.ts index 29c0593184..07872040eb 100644 --- a/packages/core/src/agents/local-executor.ts +++ b/packages/core/src/agents/local-executor.ts @@ -538,12 +538,14 @@ export class LocalAgentExecutor { // 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 { // 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 { 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); diff --git a/packages/core/src/agents/subagent-tool.ts b/packages/core/src/agents/subagent-tool.ts index dd2115dc3c..0c4f19ee8b 100644 --- a/packages/core/src/agents/subagent-tool.ts +++ b/packages/core/src/agents/subagent-tool.ts @@ -137,7 +137,7 @@ class SubAgentInvocation extends BaseToolInvocation { _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 { return agentArgs; } - const userHints = this.config.injectionService.getUserHintsAfter( + const userHints = this.config.injectionService.getInjectionsAfter( this.startIndex, + 'user_steering', ); const formattedHints = formatUserHintsForModel(userHints); if (!formattedHints) { diff --git a/packages/core/src/config/injectionService.test.ts b/packages/core/src/config/injectionService.test.ts index 482eca6403..737f7cd843 100644 --- a/packages/core/src/config/injectionService.test.ts +++ b/packages/core/src/config/injectionService.test.ts @@ -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([]); }); }); }); diff --git a/packages/core/src/config/injectionService.ts b/packages/core/src/config/injectionService.ts index 7018f34e1c..48c1a7ca5a 100644 --- a/packages/core/src/config/injectionService.ts +++ b/packages/core/src/config/injectionService.ts @@ -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. */ diff --git a/packages/core/src/services/executionLifecycleService.test.ts b/packages/core/src/services/executionLifecycleService.test.ts index ce485fc823..0d800c6e55 100644 --- a/packages/core/src/services/executionLifecycleService.test.ts +++ b/packages/core/src/services/executionLifecycleService.test.ts @@ -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!;