File size: 3,229 Bytes
7a1ad33 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | /**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Source of an injection into the model conversation.
* - `user_steering`: Interactive guidance from the user (gated on model steering).
* - `background_completion`: Output from a backgrounded execution that has finished.
*/
import { debugLogger } from '../utils/debugLogger.js';
export type InjectionSource = 'user_steering' | 'background_completion';
/**
* Typed listener that receives both the injection text and its source.
*/
export type InjectionListener = (text: string, source: InjectionSource) => void;
/**
* Service for managing injections into the model conversation.
*
* Multiple sources (user steering, background execution completions, etc.)
* can feed into this service. Consumers register listeners via
* {@link onInjection} to receive injections with source information.
*/
export class InjectionService {
private readonly injections: Array<{
text: string;
source: InjectionSource;
timestamp: number;
}> = [];
private readonly injectionListeners: Set<InjectionListener> = new Set();
constructor(private readonly isEnabled: () => boolean) {}
/**
* Adds an injection from any source.
*
* `user_steering` injections are gated on model steering being enabled.
* Other sources (e.g. `background_completion`) are always accepted.
*/
addInjection(text: string, source: InjectionSource): void {
if (source === 'user_steering' && !this.isEnabled()) {
return;
}
const trimmed = text.trim();
if (trimmed.length === 0) {
return;
}
this.injections.push({ text: trimmed, source, timestamp: Date.now() });
for (const listener of this.injectionListeners) {
try {
listener(trimmed, source);
} catch (error) {
debugLogger.warn(
`Injection listener failed for source "${source}": ${error}`,
);
}
}
}
/**
* Registers a listener for injections from any source.
*/
onInjection(listener: InjectionListener): void {
this.injectionListeners.add(listener);
}
/**
* Unregisters an injection listener.
*/
offInjection(listener: InjectionListener): void {
this.injectionListeners.delete(listener);
}
/**
* Returns collected injection texts, optionally filtered by source.
*/
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, optionally filtered by source.
*/
getInjectionsAfter(index: number, source?: InjectionSource): string[] {
if (index < 0) {
return this.getInjections(source);
}
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.
*/
getLatestInjectionIndex(): number {
return this.injections.length - 1;
}
/**
* Clears all collected injections.
*/
clear(): void {
this.injections.length = 0;
}
}
|