agentscope-ai / agentscope-ai/agentscope-java

Feature Request: Support Custom Agent Hooks via forwardedProps

Aberta
#901 1 comentário 0 reações 0 responsáveis Ver no GitHub
area/core/agent enhancement
Linguagem predominante
Java
Estrelas
5.6k
Forks
1.3k
Merge médio
4d 12h
PRs com merge (30d)
77

Descrição

# 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](agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/processor/AguiRequestProcessor.java)). 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

```java
/**
* 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

```java
/**
* Mutable context object passed through hook chain
*/
public class AguiExecutionContext {
private final String threadId;
private final String runId;
private final Map forwardedProps;

// Mutable state
private Map attributes = new ConcurrentHashMap<>();
private List modifiedMessages;
private ToolMergeMode overrideToolMergeMode;

// Getters and setters...
}
```

#### 3. Hook Registry

```java
/**
* Registry for managing AG-UI hooks
*/
public class AguiHookRegistry {
private final List requestHooks = new CopyOnWriteArrayList<>();
private final List responseHooks = new CopyOnWriteArrayList<>();

public void registerRequestHook(AguiRequestHook hook) { ... }
public void registerResponseHook(AguiResponseHook hook) { ... }
}
```

#### 4. Integration in AguiRequestProcessor

```java
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 events = adapter.run(effectiveInput);

// Wrap with response hooks
Flux 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

```java
@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

```java
@Bean
public AguiRequestHook customToolInjectionHook() {
return (input, context) -> {
// Frontend can request specific tools to be enabled
List requestedTools = (List) input.getForwardedProp("enableTools");
if (requestedTools != null) {
context.setAttribute("requestedTools", requestedTools);
}
};
}

// In AguiAgentAdapter or a wrapper
private Agent prepareAgent(Agent original, AguiExecutionContext context) {
List 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

```java
@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
```java
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()`:
```java
// 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 ID
- `context` - for additional context information
- `state` - for state management

This proposal extends the design philosophy by allowing `forwardedProps` to carry arbitrary metadata that hooks can consume.

### Implementation Priority
1. **Core**: Define hook interfaces and execution context
2. **Integration**: Modify `AguiRequestProcessor` to invoke hooks
3. **Spring Boot**: Provide auto-configuration and convenient registration
4. **Documentation**: Add examples and best practices

## Benefits

1. **Extensibility**: Allows custom logic without modifying core code
2. **Separation of Concerns**: Cross-cutting concerns (logging, auth) separated from business logic
3. **Flexibility**: Different deployments can have different hook configurations
4. **Backward Compatible**: Existing code continues to work without hooks
5. **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.

Guia de contribuição

Abrir o guia de contribuição

Avaliação

Esta issue ainda não foi avaliada.

Receba novas issues na sua caixa de entrada

Um resumo curto de issues do GitHub para quem está começando.