Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions .changeset/friendly-feedback-state.md

This file was deleted.

27 changes: 27 additions & 0 deletions .changeset/friendly-feedback-world.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
"@voltagent/core": patch
"@voltagent/server-core": patch
---

feat: add persisted feedback-provided markers for message feedback metadata

- `AgentFeedbackMetadata` now supports `provided`, `providedAt`, and `feedbackId`.
- Added `Agent.isFeedbackProvided(...)` and `Agent.isMessageFeedbackProvided(...)` helpers.
- Added `agent.markFeedbackProvided(...)` to persist a feedback-submitted marker on a stored message so feedback UI can stay hidden after memory reloads.
- Added `result.feedback.markFeedbackProvided(...)` and `result.feedback.isProvided()` helper methods for SDK usage.
- Updated server response schema to include the new feedback metadata fields.

```ts
const result = await agent.generateText("How was this answer?", {
userId: "user-1",
conversationId: "conv-1",
feedback: true,
});

if (result.feedback && !result.feedback.isProvided()) {
// call after your feedback ingestion succeeds
await result.feedback.markFeedbackProvided({
feedbackId: "fb_123", // optional
});
}
```
79 changes: 70 additions & 9 deletions packages/core/src/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,13 @@ import {
TOOL_ROUTING_SEARCHED_TOOLS_CONTEXT_KEY,
} from "./context-keys";
import { ConversationBuffer } from "./conversation-buffer";
import {
createFeedbackHandle as createFeedbackHandleHelper,
findFeedbackMessageId as findFeedbackMessageIdHelper,
isFeedbackProvided as isFeedbackProvidedHelper,
isMessageFeedbackProvided as isMessageFeedbackProvidedHelper,
markFeedbackProvided as markFeedbackProvidedHelper,
} from "./feedback";
import {
type NormalizedInputGuardrail,
type NormalizedOutputGuardrail,
Expand Down Expand Up @@ -161,10 +168,12 @@ import type { VoltAgentTextStreamPart } from "./subagent/types";
import type {
AgentEvalConfig,
AgentEvalOperationType,
AgentFeedbackHandle,
AgentFeedbackMetadata,
AgentFeedbackOptions,
AgentFullState,
AgentGuardrailState,
AgentMarkFeedbackProvidedInput,
AgentModelConfig,
AgentModelValue,
AgentOptions,
Expand Down Expand Up @@ -429,7 +438,7 @@ export type StreamTextResultWithContext<
// Additional context field
context: Map<string | symbol, unknown>;
// Feedback metadata for the trace, if enabled
feedback?: AgentFeedbackMetadata | null;
feedback?: AgentFeedbackHandle | null;
} & Record<never, OUTPUT>;

/**
Expand Down Expand Up @@ -471,7 +480,7 @@ export interface GenerateTextResultWithContext<
experimental_output: OutputValue<OUTPUT>;
output: OutputValue<OUTPUT>;
// Feedback metadata for the trace, if enabled
feedback?: AgentFeedbackMetadata | null;
feedback?: AgentFeedbackHandle | null;
}

type LLMOperation =
Expand Down Expand Up @@ -1196,12 +1205,27 @@ export class Agent {
await persistQueue.flush(buffer, oc);
}

const feedbackValue = (() => {
if (!feedbackMetadata) {
return null;
}
const metadata = feedbackMetadata;
return createFeedbackHandleHelper({
metadata,
defaultUserId: oc.userId,
defaultConversationId: oc.conversationId,
resolveMessageId: () =>
findFeedbackMessageIdHelper(buffer.getAllMessages(), metadata),
markFeedbackProvided: (input) => this.markFeedbackProvided(input),
});
})();

return cloneGenerateTextResultWithContext(result, {
text: finalText,
context: oc.context,
toolCalls: aggregatedToolCalls,
toolResults: aggregatedToolResults,
feedback: feedbackMetadata,
feedback: feedbackValue,
});
} catch (error) {
if (this.shouldRetryMiddleware(error, middlewareRetryCount, maxMiddlewareRetries)) {
Expand Down Expand Up @@ -1335,7 +1359,8 @@ export class Agent {
const feedbackDeferred = feedbackOptions
? createDeferred<AgentFeedbackMetadata | null>()
: null;
let feedbackValue: AgentFeedbackMetadata | null = null;
let feedbackMetadataValue: AgentFeedbackMetadata | null = null;
let feedbackValue: AgentFeedbackHandle | null = null;
let feedbackResolved = false;
let feedbackFinalizeRequested = false;
let feedbackApplied = false;
Expand Down Expand Up @@ -1385,7 +1410,17 @@ export class Agent {
if (feedbackPromise) {
feedbackPromise
.then((metadata) => {
feedbackValue = metadata;
feedbackMetadataValue = metadata;
feedbackValue = metadata
? createFeedbackHandleHelper({
metadata,
defaultUserId: oc.userId,
defaultConversationId: oc.conversationId,
resolveMessageId: () =>
findFeedbackMessageIdHelper(buffer.getAllMessages(), metadata),
markFeedbackProvided: (input) => this.markFeedbackProvided(input),
})
: null;
resolveFeedbackDeferred(metadata);
if (feedbackFinalizeRequested) {
scheduleFeedbackPersist(metadata);
Expand Down Expand Up @@ -1862,8 +1897,8 @@ export class Agent {
await feedbackDeferred.promise;
}

if (feedbackResolved && feedbackValue) {
scheduleFeedbackPersist(feedbackValue);
if (feedbackResolved && feedbackMetadataValue) {
scheduleFeedbackPersist(feedbackMetadataValue);
} else if (shouldDeferPersist) {
void persistQueue.flush(buffer, oc).catch((error) => {
oc.logger?.debug?.("Failed to persist deferred messages", { error });
Expand Down Expand Up @@ -2173,11 +2208,11 @@ export class Agent {
if (feedbackDeferred) {
await feedbackDeferred.promise;
}
if (feedbackResolved && feedbackValue) {
if (feedbackResolved && feedbackMetadataValue) {
controller.enqueue({
type: "message-metadata",
messageMetadata: {
feedback: feedbackValue,
feedback: feedbackMetadataValue,
},
} as UIStreamChunk);
}
Expand Down Expand Up @@ -7011,6 +7046,32 @@ export class Agent {
return voltOpsClient !== undefined;
}

/**
* Check whether feedback has already been provided for a feedback metadata object.
*/
public static isFeedbackProvided(feedback?: AgentFeedbackMetadata | null): boolean {
return isFeedbackProvidedHelper(feedback);
}

/**
* Check whether a message already has feedback marked as provided.
*/
public static isMessageFeedbackProvided(message?: UIMessage | null): boolean {
return isMessageFeedbackProvidedHelper(message);
}

/**
* Persist a "feedback provided" marker into assistant message metadata.
*/
public async markFeedbackProvided(
input: AgentMarkFeedbackProvidedInput,
): Promise<AgentFeedbackMetadata | null> {
return await markFeedbackProvidedHelper({
memory: this.memoryManager.getMemory(),
input,
});
}

/**
* Get memory manager
*/
Expand Down
Loading