agentscope-ai / agentscope-ai/agentscope-java

Feature Request: Support Custom Agent Hooks via forwardedProps

Đang mở
#901 1 bình luận 0 reaction 0 người được giao Xem trên GitHub
area/core/agent enhancement
Ngôn ngữ chính
Java
Star
5.6k
Fork
1.3k
Merge trung bình
4 ngày 12 giờ
Pull request đã merge (30 ngày)
77

Mô tả

# 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.

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Đánh giá

Issue này chưa được đánh giá.

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.