-
Notifications
You must be signed in to change notification settings - Fork 362
Description
Feature Request: Support Custom Agent Hooks via forwardedProps
Is your feature request related to a problem? Please describe.
Currently, the forwardedProps field in RunAgentInput is only used for extracting the agentId parameter (AguiRequestProcessor.java:142-148). The remaining properties in forwardedProps are simply passed through but never actually utilized.
This represents a missed opportunity for extensibility. Many use cases require custom preprocessing or postprocessing of agent execution based on runtime parameters, such as:
- Adding custom context/metadata before agent execution
- Modifying agent behavior dynamically (e.g., temperature, max tokens)
- Injecting custom tools or modifying tool configurations
- Logging/auditing with custom metadata
- Multi-tenant scenarios requiring tenant-specific configurations
- A/B testing different agent configurations
Describe the solution you'd like
I propose implementing a Hook Mechanism that allows developers to register custom hooks that can access and utilize forwardedProps to modify agent execution behavior.
Proposed Architecture
1. Define Hook Interfaces
/**
* Hook interface for preprocessing AG-UI requests
*/
public interface AguiRequestHook {
/**
* Called before agent resolution
* @param input The original request input
* @param context Execution context (mutable)
*/
void preResolve(RunAgentInput input, AguiExecutionContext context);
/**
* Called after agent resolution but before execution
* @param agent The resolved agent
* @param input The request input
* @param context Execution context (mutable)
*/
void preExecute(Agent agent, RunAgentInput input, AguiExecutionContext context);
}
/**
* Hook interface for postprocessing AG-UI responses
*/
public interface AguiResponseHook {
/**
* Called after agent execution completes
* @param event The final event
* @param context Execution context
*/
void onComplete(AguiEvent event, AguiExecutionContext context);
/**
* Called when an error occurs
* @param error The error
* @param context Execution context
*/
void onError(Throwable error, AguiExecutionContext context);
}2. Execution Context
/**
* Mutable context object passed through hook chain
*/
public class AguiExecutionContext {
private final String threadId;
private final String runId;
private final Map<String, Object> forwardedProps;
// Mutable state
private Map<String, Object> attributes = new ConcurrentHashMap<>();
private List<Msg> modifiedMessages;
private ToolMergeMode overrideToolMergeMode;
// Getters and setters...
}3. Hook Registry
/**
* Registry for managing AG-UI hooks
*/
public class AguiHookRegistry {
private final List<AguiRequestHook> requestHooks = new CopyOnWriteArrayList<>();
private final List<AguiResponseHook> responseHooks = new CopyOnWriteArrayList<>();
public void registerRequestHook(AguiRequestHook hook) { ... }
public void registerResponseHook(AguiResponseHook hook) { ... }
}4. Integration in AguiRequestProcessor
public class AguiRequestProcessor {
private final AgentResolver agentResolver;
private final AguiAdapterConfig config;
private final AguiHookRegistry hookRegistry; // New
public ProcessResult process(RunAgentInput input, String headerAgentId, String pathAgentId) {
String threadId = input.getThreadId();
String agentId = resolveAgentId(input, headerAgentId, pathAgentId);
// Create execution context
AguiExecutionContext context = new AguiExecutionContext(threadId, input);
// Invoke preResolve hooks
if (hookRegistry != null) {
hookRegistry.getRequestHooks().forEach(hook ->
hook.preResolve(input, context));
}
// Resolve agent (may be modified by hooks)
Agent agent = agentResolver.resolveAgent(agentId, threadId);
// Invoke preExecute hooks
if (hookRegistry != null) {
hookRegistry.getResponseHooks().forEach(hook ->
hook.preExecute(agent, input, context));
}
// Apply modifications from context
RunAgentInput effectiveInput = applyModifications(input, context);
// Execute agent
AguiAgentAdapter adapter = new AguiAgentAdapter(agent, config);
Flux<AguiEvent> events = adapter.run(effectiveInput);
// Wrap with response hooks
Flux<AguiEvent> hookedEvents = events.doOnComplete(() -> {
if (hookRegistry != null) {
hookRegistry.getResponseHooks().forEach(hook ->
hook.onComplete(null, context));
}
}).doOnError(error -> {
if (hookRegistry != null) {
hookRegistry.getResponseHooks().forEach(hook ->
hook.onError(error, context));
}
});
return new ProcessResult(agent, hookedEvents);
}
}Usage Examples
Example 1: Spring Boot Auto-Configuration
@Configuration
public class AguiHookConfiguration {
@Bean
public AguiHookRegistry aguiHookRegistry() {
return new AguiHookRegistry();
}
@Bean
public AguiRequestHook tenantContextHook() {
return (input, context) -> {
// Extract tenant ID from forwardedProps
String tenantId = (String) input.getForwardedProp("tenantId");
if (tenantId != null) {
context.setAttribute("tenantId", tenantId);
MDC.put("tenantId", tenantId); // For logging
}
};
}
@Bean
public AguiRequestHook dynamicConfigHook() {
return (input, context) -> {
// Allow frontend to override model parameters
Double temperature = (Double) input.getForwardedProp("temperature");
if (temperature != null) {
context.setAttribute("model.temperature", temperature);
}
Integer maxTokens = (Integer) input.getForwardedProp("maxTokens");
if (maxTokens != null) {
context.setAttribute("model.maxTokens", maxTokens);
}
};
}
@Bean
public AguiResponseHook auditLogHook() {
return (event, context) -> {
if (event instanceof AguiEvent.RunFinished) {
String tenantId = context.getAttribute("tenantId");
logAuditEvent(tenantId, context.getThreadId(), context.getRunId());
}
};
}
}Example 2: Custom Tool Injection
@Bean
public AguiRequestHook customToolInjectionHook() {
return (input, context) -> {
// Frontend can request specific tools to be enabled
List<String> requestedTools = (List<String>) input.getForwardedProp("enableTools");
if (requestedTools != null) {
context.setAttribute("requestedTools", requestedTools);
}
};
}
// In AguiAgentAdapter or a wrapper
private Agent prepareAgent(Agent original, AguiExecutionContext context) {
List<String> requestedTools = context.getAttribute("requestedTools");
if (requestedTools != null) {
// Filter or modify agent's toolkit based on request
return filterAgentTools(original, requestedTools);
}
return original;
}Example 3: Request Validation
@Bean
public AguiRequestHook validationHook() {
return (input, context) -> {
// Validate required props
if (input.getForwardedProp("requireAuth") != null) {
String authToken = (String) input.getForwardedProp("authToken");
if (authToken == null || !isValid(authToken)) {
throw new AguiException.AuthenticationException("Invalid or missing auth token");
}
}
// Rate limiting check
String userId = (String) input.getForwardedProp("userId");
if (userId != null && isRateLimitExceeded(userId)) {
throw new AguiException.RateLimitExceededException("Rate limit exceeded");
}
};
}Describe alternatives you've considered
Alternative 1: Extend RunAgentInput with More Fields
Pros: Type-safe, clear API
Cons: Requires protocol changes, not flexible for custom extensions
Alternative 2: Add Callback Interface to AguiAdapterConfig
AguiAdapterConfig.builder()
.preExecutionHook((input) -> { ... })
.postExecutionHook((event) -> { ... })
.build();Pros: Simple for basic use cases
Cons: Limited flexibility, doesn't support multiple hooks
Alternative 3: Use ThreadLocal Context
Pros: Easy to implement
Cons: Not reactive-friendly, harder to test, potential memory leaks
Additional context
Current forwardedProps Usage
The only current usage of forwardedProps is in AguiRequestProcessor.resolveAgentId():
// 3. Check forwardedProps for agentId
Object agentIdProp = input.getForwardedProp("agentId");
if (agentIdProp != null) {
String propsAgentId = agentIdProp.toString();
logger.debug("Using agent ID from forwardedProps: {}", propsAgentId);
return propsAgentId;
}All other properties in forwardedProps are stored but never accessed.
Related AG-UI Protocol Features
The AG-UI protocol already supports:
forwardedProps.agentId- for specifying agent IDcontext- for additional context informationstate- for state management
This proposal extends the design philosophy by allowing forwardedProps to carry arbitrary metadata that hooks can consume.
Implementation Priority
- Core: Define hook interfaces and execution context
- Integration: Modify
AguiRequestProcessorto invoke hooks - Spring Boot: Provide auto-configuration and convenient registration
- Documentation: Add examples and best practices
Benefits
- Extensibility: Allows custom logic without modifying core code
- Separation of Concerns: Cross-cutting concerns (logging, auth) separated from business logic
- Flexibility: Different deployments can have different hook configurations
- Backward Compatible: Existing code continues to work without hooks
- Reactive Friendly: Designed to work with Reactor
Flux
Note: I'm willing to contribute a PR for this feature if the maintainers find it valuable and provide guidance on the preferred implementation approach.
Metadata
Metadata
Assignees
Labels
Type
Projects
Status