1
/**2
* Session feedback: the `feedback/record` event, its command-independent3
* producer, the `sessionFeedback` Host Remote a product surface records4
* through, and the human-facing `/feedback` command. Recording appends one5
* authoritative log-only event and does not start model work. The append is6
* eager but unflushed, so acknowledgement reports that the entry is logged,7
* not that it reached disk.8
* @module @deepseek-ai/dsh-command-feedback9
*/11
import type { Context } from '@deepseek-ai/cordis'12
import { CommandDefinitionId } from '@deepseek-ai/dsh-commands/brand'13
import type { CommandInvocation, CommandResult } from '@deepseek-ai/dsh-commands'14
import type { Session } from '@deepseek-ai/dsh-session'15
import { getOrCreateAnonymousUserId } from '@deepseek-ai/dsh-anonymous-user-id'16
import { TypertRemoteService, Remote } from '@deepseek-ai/dsh-typert-protocol'17
import type {18
FeedbackCategory,19
FeedbackRecord,20
SessionFeedbackRecordRequest,21
SessionFeedbackRecordResult,22
} from './types.ts'24
export type * from './types.ts'26
/**27
* Every feedback category in the order product surfaces present them; each28
* surface owns its localized labels.29
*/30
export const FEEDBACK_CATEGORIES = [31
'task-result',32
'instruction-following',33
'product-interaction',34
'service-stability',35
'resource-cost',36
'security-privacy-permission',37
'other',38
] as const satisfies readonly FeedbackCategory[]40
export const name = 'command-feedback'41
export const inject = ['commands']43
const USAGE = 'Usage: /feedback <text>'45
declare module '@deepseek-ai/cordis' {46
interface Context {47
sessionFeedback: SessionFeedbackService48
}49
}51
/**52
* Record feedback independently of any UI trigger. Surrounding whitespace is53
* discarded and a blank text is recorded as absent; an entry with neither54
* text nor category is still recorded.55
* @param session - session the feedback describes.56
* @param entry - human-authored remark and its category.57
*/58
export function recordFeedback(session: Session, entry: FeedbackRecord): void {59
const text = entry.text?.trim() ?? ''60
session.append('feedback/record', {61
...(text.length === 0 ? {} : { text }),62
...(entry.category === undefined ? {} : { category: entry.category }),63
})64
}66
/**67
* Validate, record, and acknowledge one feedback entry. Returning an error68
* leaves no `feedback/record` event.69
* @param invocation - receiving agent, raw command input, and UI cancellation.70
* @returns an acknowledgement containing the receiving session and anonymous71
* user ids, or a usage error when no feedback text was supplied.72
*/73
function executeFeedbackCommand(invocation: CommandInvocation): CommandResult {74
if (invocation.rawInput.trim().length === 0) {75
return { kind: 'error', text: `Feedback text is required. ${USAGE}` }76
}77
recordFeedback(invocation.agent.session, { text: invocation.rawInput })78
return {79
kind: 'success',80
text: `Feedback recorded for session ${invocation.agent.session.id}\nAnonymous user: ${getOrCreateAnonymousUserId()}.`,81
}82
}84
/** Host Remote through which a product surface records a Session-level remark. */85
export class SessionFeedbackService extends TypertRemoteService {86
static inject = ['sessions']88
/**89
* @param ctx - Host context carrying the live Session store.90
*/91
constructor(ctx: Context) {92
super(ctx, 'sessionFeedback')93
}95
/**96
* Record one remark on a live Session.97
* @param request - target Session plus the optional text and category.98
* @returns the recorded postcondition, or `session-not-found` when no live99
* Session carries the id.100
*/101
@Remote('record')102
record(request: SessionFeedbackRecordRequest): Promise<SessionFeedbackRecordResult> {103
const session = this.ctx.sessions.get(request.sessionId)104
if (session === undefined) {105
return Promise.resolve({ ok: false, error: { code: 'session-not-found', sessionId: request.sessionId } })106
}107
recordFeedback(session, request)108
return Promise.resolve({ ok: true, value: { recorded: true } })109
}110
}112
/**113
* Register the global `/feedback` command for every composed command adapter114
* and mount the `sessionFeedback` Remote.115
* @param ctx - Host context.116
*/117
export function apply(ctx: Context): void {118
ctx.plugin(SessionFeedbackService)119
ctx.commands.register({120
definitionId: CommandDefinitionId('@deepseek-ai/dsh-command-feedback'),121
name: 'feedback',122
description: 'Record feedback about this session',123
input: { hint: '<text>' },124
recordInput: false,125
handler: executeFeedbackCommand,126
})127
}