mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-30 19:09:28 +00:00
Merge pull request #74450 from NousResearch/bb/memory-save-chip
fix(desktop): gold→purple memory saves, stop warning chrome
This commit is contained in:
commit
4d9541b9c3
10 changed files with 133 additions and 11 deletions
|
|
@ -415,3 +415,38 @@ describe('countDiffLineStats', () => {
|
|||
expect(countDiffLineStats(`--- a/x\n+++ b/x\n@@\n-old\n+new\n context\n+another`)).toEqual({ added: 2, removed: 1 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildToolView memory status', () => {
|
||||
const memory = (overrides: Partial<Parameters<typeof part>[0]> = {}) =>
|
||||
buildToolView(part({ toolName: 'memory', ...overrides }), '')
|
||||
|
||||
it('treats an explicit success payload as success even with isError', () => {
|
||||
const view = memory({
|
||||
isError: true,
|
||||
result: {
|
||||
success: true,
|
||||
entry_count: 13,
|
||||
message: 'Applied 1 operation(s).',
|
||||
duration_s: 0.003
|
||||
}
|
||||
})
|
||||
|
||||
expect(view.status).toBe('success')
|
||||
expect(view.title).toBe('Saved to memory')
|
||||
expect(view.countLabel).toBe('13 entries')
|
||||
expect(view.subtitle).toBe('Applied 1 operation(s).')
|
||||
})
|
||||
|
||||
it('uses soft warning copy for over-budget refusals, not "Saved"', () => {
|
||||
const view = memory({
|
||||
result: {
|
||||
success: false,
|
||||
error: 'Memory is full (2,200/2,200). Consolidate before adding more.'
|
||||
}
|
||||
})
|
||||
|
||||
expect(view.status).toBe('warning')
|
||||
expect(view.title).toBe('Memory write noted')
|
||||
expect(view.subtitle).toContain('Memory is full')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -516,6 +516,16 @@ function toolResultCount(
|
|||
}
|
||||
}
|
||||
|
||||
// Memory success payloads put the live total on `entry_count` — keep the noun
|
||||
// as entry/entries instead of falling through the generic `*_count` path.
|
||||
if (part.toolName === 'memory') {
|
||||
const entryTotal = countFromUnknown(resultRecord.entry_count)
|
||||
|
||||
if (entryTotal !== null) {
|
||||
return countMetric(entryTotal, 'entry')
|
||||
}
|
||||
}
|
||||
|
||||
const directCount = countFromRecord(resultRecord, fallbackNounByTool)
|
||||
|
||||
if (directCount !== null) {
|
||||
|
|
@ -699,14 +709,20 @@ function toolStatus(part: ToolPart, resultRecord: Record<string, unknown>): Tool
|
|||
return 'running'
|
||||
}
|
||||
|
||||
// Explicit success wins over isError / nested-error heuristics. Memory writes
|
||||
// return `{ success: true }` when the batch landed; a stale outer `isError`
|
||||
// envelope must not paint a real save amber.
|
||||
if (resultRecord.success === true || resultRecord.ok === true) {
|
||||
return 'success'
|
||||
}
|
||||
|
||||
if (!toolErrorText(part, resultRecord)) {
|
||||
return 'success'
|
||||
}
|
||||
|
||||
// A rejected memory write is a budget negotiation, not a failure: the store
|
||||
// refuses an over-limit batch and the agent immediately retries a smaller
|
||||
// one. Painting the row destructive-red puts an alarm next to routine
|
||||
// bookkeeping the user never has to act on. Amber says "noted" instead.
|
||||
// refuses an over-limit batch and the agent retries smaller. Soft warning —
|
||||
// never destructive-red beside routine bookkeeping.
|
||||
return part.toolName === 'memory' ? 'warning' : 'error'
|
||||
}
|
||||
|
||||
|
|
@ -1395,8 +1411,17 @@ export function buildToolView(part: ToolPart, inlineDiff: string): ToolView {
|
|||
const resultRecord = parseMaybeObject(part.result)
|
||||
const meta = toolMeta(part.toolName)
|
||||
const status = toolStatus(part, resultRecord)
|
||||
const error = toolErrorText(part, resultRecord)
|
||||
const baseTitle = part.result === undefined ? meta.pending : meta.done
|
||||
// Skip residual error-heuristic text once status is success (stale isError
|
||||
// envelope over a landed memory write would otherwise foul the subtitle).
|
||||
const error = status === 'success' ? '' : toolErrorText(part, resultRecord)
|
||||
// Over-budget memory refusals stay amber — don't claim "Saved".
|
||||
const memoryMissed = part.toolName === 'memory' && part.result !== undefined && status !== 'success'
|
||||
const baseTitle =
|
||||
part.result === undefined
|
||||
? meta.pending
|
||||
: memoryMissed
|
||||
? translateNow('assistant.tool.memoryWriteNoted')
|
||||
: meta.done
|
||||
|
||||
const titleParts = dynamicTitle(
|
||||
part,
|
||||
|
|
|
|||
|
|
@ -214,11 +214,14 @@ function ToolGlyph({
|
|||
copy,
|
||||
filePath,
|
||||
icon,
|
||||
legendary,
|
||||
status
|
||||
}: {
|
||||
copy: ToolStatusCopy
|
||||
filePath?: string
|
||||
icon?: string
|
||||
/** Landed memory write — keep the brain glyph, tint it gold→purple. */
|
||||
legendary?: boolean
|
||||
status?: ToolStatus
|
||||
}) {
|
||||
const node = status ? (
|
||||
|
|
@ -226,10 +229,16 @@ function ToolGlyph({
|
|||
) : filePath ? (
|
||||
<FileTypeIcon className="text-(--ui-text-tertiary)" path={filePath} size="0.875rem" />
|
||||
) : icon ? (
|
||||
<ToolIcon className="text-(--ui-text-tertiary)" name={icon} size="0.875rem" />
|
||||
<ToolIcon
|
||||
className={legendary ? 'text-(--tool-memory-legendary-icon)' : 'text-(--ui-text-tertiary)'}
|
||||
name={icon}
|
||||
size="0.875rem"
|
||||
/>
|
||||
) : null
|
||||
|
||||
return node ? <span className={TOOL_HEADER_GLYPH_WRAP_CLASS}>{node}</span> : null
|
||||
return node ? (
|
||||
<span className={cn(TOOL_HEADER_GLYPH_WRAP_CLASS, legendary && 'tool-memory-legendary-glyph')}>{node}</span>
|
||||
) : null
|
||||
}
|
||||
|
||||
// Which status (if any) should pre-empt the tool's icon in the leading
|
||||
|
|
@ -275,11 +284,13 @@ function LinkifiedText({ className, text }: { className?: string; text: string }
|
|||
|
||||
function ToolTitle({
|
||||
isPending,
|
||||
legendary,
|
||||
status,
|
||||
title,
|
||||
titleAction
|
||||
}: {
|
||||
isPending: boolean
|
||||
legendary?: boolean
|
||||
status: ToolStatus
|
||||
title: string
|
||||
titleAction?: ToolTitleAction
|
||||
|
|
@ -290,7 +301,8 @@ function ToolTitle({
|
|||
SCAFFOLD_LABEL_CLASS,
|
||||
isPending && 'text-(--conversation-scaffold-meta)',
|
||||
status === 'error' && 'text-destructive',
|
||||
status === 'warning' && 'text-amber-700 dark:text-amber-300'
|
||||
status === 'warning' && 'text-amber-700 dark:text-amber-300',
|
||||
legendary && !isPending && 'tool-memory-legendary-title text-transparent'
|
||||
)}
|
||||
>
|
||||
{isPending && titleAction ? (
|
||||
|
|
@ -466,6 +478,10 @@ function ToolEntry({ part }: ToolEntryProps) {
|
|||
|
||||
const showDiffStats = !isPending && Boolean(diffStats && (diffStats.added > 0 || diffStats.removed > 0))
|
||||
|
||||
// Landed memory write gets gold→purple chrome instead of the plain scaffold grey.
|
||||
const memoryLegendary = !isPending && part.toolName === 'memory' && view.status === 'success'
|
||||
const memoryMetaClass = memoryLegendary ? 'tool-memory-legendary-meta' : undefined
|
||||
|
||||
// The header trailing slot only carries the live duration timer while the
|
||||
// tool is running. The copy control used to live here too, but an
|
||||
// `opacity-0` (yet still clickable) button straddling the caret/duration made
|
||||
|
|
@ -542,10 +558,19 @@ function ToolEntry({ part }: ToolEntryProps) {
|
|||
copy={copy}
|
||||
filePath={isFileEdit ? view.subtitle : undefined}
|
||||
icon={view.icon}
|
||||
legendary={memoryLegendary}
|
||||
status={leadingStatus(isPending, view.status)}
|
||||
/>
|
||||
<ToolTitle isPending={isPending} status={view.status} title={view.title} titleAction={view.titleAction} />
|
||||
{!isPending && view.countLabel && <span className={SCAFFOLD_META_CLASS}>{view.countLabel}</span>}
|
||||
<ToolTitle
|
||||
isPending={isPending}
|
||||
legendary={memoryLegendary}
|
||||
status={view.status}
|
||||
title={view.title}
|
||||
titleAction={view.titleAction}
|
||||
/>
|
||||
{!isPending && view.countLabel && (
|
||||
<span className={cn(SCAFFOLD_META_CLASS, memoryMetaClass)}>{view.countLabel}</span>
|
||||
)}
|
||||
{showDiffStats && diffStats && (
|
||||
<span className="flex shrink-0 items-center gap-1 font-mono text-[0.625rem] tabular-nums">
|
||||
{diffStats.added > 0 && (
|
||||
|
|
@ -557,7 +582,7 @@ function ToolEntry({ part }: ToolEntryProps) {
|
|||
</span>
|
||||
)}
|
||||
{!isFileEdit && !isPending && view.durationLabel && (
|
||||
<span className={SCAFFOLD_META_CLASS}>{view.durationLabel}</span>
|
||||
<span className={cn(SCAFFOLD_META_CLASS, memoryMetaClass)}>{view.durationLabel}</span>
|
||||
)}
|
||||
</span>
|
||||
</DisclosureRow>
|
||||
|
|
|
|||
|
|
@ -2349,6 +2349,7 @@ export const ar = defineLocale({
|
|||
statusError: 'خطأ',
|
||||
statusRecovered: 'تم الاسترداد',
|
||||
statusDone: 'تم',
|
||||
memoryWriteNoted: 'تم تسجيل كتابة الذاكرة',
|
||||
actions: {
|
||||
read: 'قراءة',
|
||||
reading: 'جار القراءة',
|
||||
|
|
|
|||
|
|
@ -2758,6 +2758,7 @@ export const en: Translations = {
|
|||
statusError: 'Error',
|
||||
statusRecovered: 'Recovered',
|
||||
statusDone: 'Done',
|
||||
memoryWriteNoted: 'Memory write noted',
|
||||
actions: {
|
||||
read: 'Read',
|
||||
reading: 'Reading',
|
||||
|
|
|
|||
|
|
@ -2602,6 +2602,7 @@ export const ja = defineLocale({
|
|||
statusError: 'エラー',
|
||||
statusRecovered: '回復しました',
|
||||
statusDone: '完了',
|
||||
memoryWriteNoted: 'メモリへの書き込みを記録',
|
||||
actions: {
|
||||
read: '読み取り完了',
|
||||
reading: '読み取り中',
|
||||
|
|
|
|||
|
|
@ -2354,6 +2354,8 @@ export interface Translations {
|
|||
statusError: string
|
||||
statusRecovered: string
|
||||
statusDone: string
|
||||
/** Over-budget / rejected memory write title — not "Saved to memory". */
|
||||
memoryWriteNoted: string
|
||||
actions: {
|
||||
read: string
|
||||
reading: string
|
||||
|
|
|
|||
|
|
@ -2521,6 +2521,7 @@ export const zhHant = defineLocale({
|
|||
statusError: '錯誤',
|
||||
statusRecovered: '已復原',
|
||||
statusDone: '完成',
|
||||
memoryWriteNoted: '已記下記憶寫入',
|
||||
actions: {
|
||||
read: '已讀取',
|
||||
reading: '正在讀取',
|
||||
|
|
|
|||
|
|
@ -2933,6 +2933,7 @@ export const zh: Translations = {
|
|||
statusError: '错误',
|
||||
statusRecovered: '已恢复',
|
||||
statusDone: '完成',
|
||||
memoryWriteNoted: '已记下记忆写入',
|
||||
actions: {
|
||||
read: '已读取',
|
||||
reading: '正在读取',
|
||||
|
|
|
|||
|
|
@ -188,6 +188,13 @@
|
|||
--context-usage-subagents: color-mix(in srgb, var(--ui-blue) 70%, var(--ui-cyan));
|
||||
--context-usage-memory: color-mix(in srgb, var(--ui-orange) 80%, var(--ui-yellow));
|
||||
--context-usage-conversation: var(--ui-cyan);
|
||||
/* Landed memory-write tool row — gold → purple, never amber warning. */
|
||||
--tool-memory-legendary-from: color-mix(in srgb, var(--ui-yellow) 72%, #f5d08a);
|
||||
--tool-memory-legendary-mid: color-mix(in srgb, var(--ui-orange) 55%, var(--ui-purple));
|
||||
--tool-memory-legendary-to: color-mix(in srgb, var(--ui-purple) 82%, #c4b5fd);
|
||||
--tool-memory-legendary-icon: color-mix(in srgb, var(--ui-yellow) 55%, var(--ui-purple));
|
||||
--tool-memory-legendary-meta: color-mix(in srgb, var(--ui-purple) 58%, var(--ui-text-tertiary));
|
||||
--tool-memory-legendary-glow: color-mix(in srgb, var(--ui-purple) 38%, transparent);
|
||||
/* Diff add/remove, derived from the semantic palette so every diff surface
|
||||
(tool cards, preview, review pane) tracks the theme's green/red. Only the
|
||||
foregrounds need a dark override — they mix toward the page instead of
|
||||
|
|
@ -738,6 +745,29 @@
|
|||
}
|
||||
}
|
||||
|
||||
/* Landed `memory` tool row — gold → purple on the brain glyph + title.
|
||||
Pair with `text-transparent` so the scaffold label color can't win. */
|
||||
.tool-memory-legendary-title {
|
||||
background-image: linear-gradient(
|
||||
105deg,
|
||||
var(--tool-memory-legendary-from) 0%,
|
||||
var(--tool-memory-legendary-mid) 48%,
|
||||
var(--tool-memory-legendary-to) 100%
|
||||
);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tool-memory-legendary-glyph {
|
||||
filter: drop-shadow(0 0 0.28rem var(--tool-memory-legendary-glow));
|
||||
}
|
||||
|
||||
.tool-memory-legendary-meta {
|
||||
color: var(--tool-memory-legendary-meta);
|
||||
}
|
||||
|
||||
.quest-glow {
|
||||
animation: quest-glow 1.8s ease-in-out infinite;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue