-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Description
In the course of #14971, we identified the potential need of AI variables that depend on other variables. The concrete example use case was a chat context summary variable that lists all variables of the context; to be able to summarize a context variable, it needs to be resolved first. Other use cases may involve reusing core variables, like workspace root, environment, etc.
Currently, resolving a variable in the course of another variable resolution is not explicitly supported; the variable provider can only capture a reference to the variable service when it is asked to register the variable itself, and then use this variable. It cannot get the variable service injected directly, as this would cause cyclic dependency injection. Also we may run into cyclic variable resolutions.
As a potential solution we discussed the approach below.
Extend the variable resolver
export interface AIVariableResolverWithVariableDependencies extends AIVariableResolver {
resolve(
request: AIVariableResolutionRequest,
context: AIVariableContext,
resolveDependency: (req: AIVariableResolutionRequest) => Promise<ResolvedAIVariable | undefined>
): Promise<ResolvedAIVariable | undefined>;
}
function isResolverWithDependencies(resolver: AIVariableResolver | undefined): resolver is AIVariableResolverWithVariableDependencies {
return resolver !== undefined && resolver.resolve.length >= 3;
}
Enhance the resolveVariable method of the variable service
interface CacheEntry {
promise: Promise<ResolvedAIVariable | undefined>;
inProgress: boolean;
}
export class DefaultAIVariableService implements AIVariableService {
// ... existing properties and methods
async resolveVariable(
request: AIVariableArg,
context: AIVariableContext,
cache: Map<string, CacheEntry> = new Map()
): Promise<ResolvedAIVariable | undefined> {
const variableName = typeof request === 'string'
? request
: typeof request.variable === 'string'
? request.variable
: request.variable.name;
if (cache.has(variableName)) {
const entry = cache.get(variableName)!;
if (entry.inProgress) {
this.logger.warn(`Cycle detected for variable: ${variableName}. Skipping resolution.`);
return undefined;
}
return entry.promise;
}
const entry: CacheEntry = { promise: Promise.resolve(undefined), inProgress: true };
cache.set(variableName, entry);
const promise = (async () => {
const variable = this.getVariable(variableName);
if (!variable) {
return undefined;
}
const arg = typeof request === 'string' ? undefined : request.arg;
const resolver = await this.getResolver(variableName, arg, context);
let resolved: ResolvedAIVariable | undefined;
if (isResolverWithDependencies(resolver)) {
resolved = await resolver.resolve(
{ variable, arg },
context,
async (depRequest: AIVariableResolutionRequest) =>
this.resolveVariable(depRequest, context, cache)
);
} else {
resolved = await resolver?.resolve({ variable, arg }, context);
}
return resolved ? { ...resolved, arg } : undefined;
})();
entry.promise = promise;
promise.finally(() => {
entry.inProgress = false;
});
return promise;
}
}
Summary
- AIVariableResolverWithVariableDependencies lets a resolver access dependencies through a callback during its resolution.
- The updated
resolveVariable
method uses a promise-based cache with aninProgress
flag for cycle detection. This way, if a variable is already being resolved, it’s skipped—avoiding cycles. - With this approach, resolved variables are accessible during the resolution of dependent ones.