Merge pull request #110 from multica-ai/Bohan-J/dual-track-user-display-content-20260209

fix(agent): separate display content from agent user turns
This commit is contained in:
Bohan Jiang 2026-02-09 16:41:31 +08:00 committed by GitHub
commit be312cd3e0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 249 additions and 238 deletions

View file

@ -81,7 +81,6 @@ interface SkillAddResult {
interface ProfileData {
profileId: string | undefined
name: string | undefined
style: string | undefined
userContent: string | undefined
}
@ -184,7 +183,6 @@ interface ElectronAPI {
profile: {
get: () => Promise<ProfileData>
updateName: (name: string) => Promise<unknown>
updateStyle: (style: string) => Promise<unknown>
updateUser: (content: string) => Promise<unknown>
}
provider: {

View file

@ -358,7 +358,7 @@ export function registerHubIpcHandlers(): void {
try {
await agent.ensureInitialized()
const allMessages = agent.loadSessionMessages()
const allMessages = agent.loadSessionMessagesForDisplay()
const total = allMessages.length
// Must match DEFAULT_MESSAGES_LIMIT from @multica/sdk/actions/rpc
const limit = options?.limit ?? 200

View file

@ -25,7 +25,6 @@ function getDefaultAgent() {
export interface ProfileData {
profileId: string | undefined
name: string | undefined
style: string | undefined
userContent: string | undefined
}
@ -42,7 +41,6 @@ export function registerProfileIpcHandlers(): void {
return {
profileId: undefined,
name: undefined,
style: undefined,
userContent: undefined,
}
}
@ -50,7 +48,6 @@ export function registerProfileIpcHandlers(): void {
return {
profileId: agent.getProfileId(),
name: agent.getAgentName(),
style: agent.getAgentStyle(),
userContent: agent.getUserContent(),
}
})
@ -92,19 +89,4 @@ export function registerProfileIpcHandlers(): void {
return { ok: true }
})
/**
* Update agent communication style.
*/
ipcMain.handle('profile:updateStyle', async (_event, style: string) => {
const agent = getDefaultAgent()
if (!agent) {
return { error: 'No agent available' }
}
agent.setAgentStyle(style)
// Reload system prompt to apply changes immediately
agent.reloadSystemPrompt()
return { ok: true, style }
})
}

View file

@ -40,7 +40,6 @@ export interface SkillInfo {
export interface ProfileData {
profileId: string | undefined
name: string | undefined
style: string | undefined
userContent: string | undefined
}
@ -93,10 +92,6 @@ export interface LocalChatApproval {
expiresAtMs: number
}
// Available style options
export const AGENT_STYLES = ['concise', 'warm', 'playful', 'professional'] as const
export type AgentStyle = (typeof AGENT_STYLES)[number]
// ============================================================================
// Expose typed API to Renderer process
// ============================================================================
@ -174,7 +169,6 @@ const electronAPI = {
profile: {
get: (): Promise<ProfileData> => ipcRenderer.invoke('profile:get'),
updateName: (name: string) => ipcRenderer.invoke('profile:updateName', name),
updateStyle: (style: string) => ipcRenderer.invoke('profile:updateStyle', style),
updateUser: (content: string) => ipcRenderer.invoke('profile:updateUser', content),
},

View file

@ -12,15 +12,7 @@ import { Input } from '@multica/ui/components/ui/input'
import { Textarea } from '@multica/ui/components/ui/textarea'
import { Label } from '@multica/ui/components/ui/label'
import { HugeiconsIcon } from '@hugeicons/react'
import { Loading03Icon, Tick02Icon } from '@hugeicons/core-free-icons'
// Style options with labels
const STYLE_OPTIONS = [
{ value: 'concise', label: 'Concise', description: 'Brief and to the point' },
{ value: 'warm', label: 'Warm', description: 'Friendly and approachable' },
{ value: 'playful', label: 'Playful', description: 'Fun and lighthearted' },
{ value: 'professional', label: 'Professional', description: 'Formal and business-like' },
] as const
import { Loading03Icon } from '@hugeicons/core-free-icons'
interface AgentSettingsDialogProps {
open: boolean
@ -31,7 +23,6 @@ export function AgentSettingsDialog({ open, onOpenChange }: AgentSettingsDialogP
const [loading, setLoading] = useState(false)
const [saving, setSaving] = useState(false)
const [name, setName] = useState('')
const [style, setStyle] = useState<string>('concise')
const [userContent, setUserContent] = useState('')
// Load profile data when dialog opens
@ -46,7 +37,6 @@ export function AgentSettingsDialog({ open, onOpenChange }: AgentSettingsDialogP
try {
const data = await window.electronAPI.profile.get()
setName(data.name ?? '')
setStyle(data.style ?? 'concise')
setUserContent(data.userContent ?? '')
} catch (err) {
console.error('Failed to load profile:', err)
@ -60,8 +50,6 @@ export function AgentSettingsDialog({ open, onOpenChange }: AgentSettingsDialogP
try {
// Update name if changed
await window.electronAPI.profile.updateName(name)
// Update style
await window.electronAPI.profile.updateStyle(style)
// Update user content
await window.electronAPI.profile.updateUser(userContent)
onOpenChange(false)
@ -78,7 +66,7 @@ export function AgentSettingsDialog({ open, onOpenChange }: AgentSettingsDialogP
<DialogHeader>
<DialogTitle>Edit Agent</DialogTitle>
<DialogDescription>
Customize your agent's name, style and personal settings.
Customize your agent's name and personal settings.
</DialogDescription>
</DialogHeader>
@ -99,35 +87,6 @@ export function AgentSettingsDialog({ open, onOpenChange }: AgentSettingsDialogP
/>
</div>
{/* Style */}
<div className="space-y-2">
<Label>Communication Style</Label>
<div className="grid grid-cols-2 gap-2">
{STYLE_OPTIONS.map((option) => (
<button
key={option.value}
type="button"
onClick={() => setStyle(option.value)}
className={`relative flex flex-col items-start rounded-lg border p-3 text-left transition-colors hover:bg-accent ${
style === option.value
? 'border-primary bg-primary/5'
: 'border-border'
}`}
>
<div className="flex w-full items-center justify-between">
<span className="font-medium text-sm">{option.label}</span>
{style === option.value && (
<HugeiconsIcon icon={Tick02Icon} className="size-4 text-primary" />
)}
</div>
<span className="text-xs text-muted-foreground mt-0.5">
{option.description}
</span>
</button>
))}
</div>
</div>
{/* User Content */}
<div className="space-y-2">
<Label htmlFor="user-content">About You</Label>

View file

@ -4,7 +4,11 @@ import { AsyncAgent } from "./async-agent.js";
const subscribeCallbacks: Array<(event: any) => void> = [];
const internalRunState = { value: false };
const runMock = vi.fn(async (_prompt: string) => ({ text: "", thinking: undefined, error: undefined as string | undefined }));
const runMock = vi.fn(async (_prompt: string, _options?: { displayPrompt?: string }) => ({
text: "",
thinking: undefined,
error: undefined as string | undefined,
}));
const runInternalMock = vi.fn(async (_prompt: string) => ({ text: "", thinking: undefined, error: undefined as string | undefined }));
const flushSessionMock = vi.fn(async () => {});
const persistAssistantSummaryMock = vi.fn();
@ -58,10 +62,6 @@ vi.mock("./runner.js", () => ({
return undefined;
}
setUserContent() {}
getAgentStyle() {
return undefined;
}
setAgentStyle() {}
reloadSystemPrompt() {}
getProviderInfo() {
return { provider: "test", model: "test-model" };
@ -107,8 +107,9 @@ describe("AsyncAgent internal flow", () => {
await agent.waitForIdle();
expect(runMock).toHaveBeenCalledTimes(1);
const [message] = runMock.mock.calls[0] ?? [];
const [message, runOptions] = runMock.mock.calls[0] ?? [];
expect(message).toMatch(/^\[Wed 2026-01-28 20:30 EST\] recent news$/);
expect(runOptions).toEqual({ displayPrompt: "recent news" });
agent.close();
});
@ -119,7 +120,9 @@ describe("AsyncAgent internal flow", () => {
agent.write("raw heartbeat prompt", { injectTimestamp: false });
await agent.waitForIdle();
expect(runMock).toHaveBeenCalledWith("raw heartbeat prompt");
expect(runMock).toHaveBeenCalledWith("raw heartbeat prompt", {
displayPrompt: "raw heartbeat prompt",
});
agent.close();
});

View file

@ -65,7 +65,7 @@ export class AsyncAgent {
this.queue = this.queue
.then(async () => {
if (this._closed) return;
const result = await this.agent.run(message);
const result = await this.agent.run(message, { displayPrompt: content });
// Flush pending session writes so waitForIdle() callers
// can safely read session data from disk.
await this.agent.flushSession();
@ -318,20 +318,6 @@ export class AsyncAgent {
this.agent.setUserContent(content);
}
/**
* Get agent communication style from profile config.
*/
getAgentStyle(): string | undefined {
return this.agent.getAgentStyle();
}
/**
* Update agent communication style in profile config.
*/
setAgentStyle(style: string): void {
this.agent.setAgentStyle(style);
}
/**
* Reload profile from disk and rebuild system prompt.
* Call this after updating profile files to apply changes immediately.
@ -360,6 +346,14 @@ export class AsyncAgent {
return this.agent.loadSessionMessages(options);
}
/**
* Load session messages for UI rendering.
* User messages prefer displayContent when present.
*/
loadSessionMessagesForDisplay(options?: { includeInternal?: boolean }): AgentMessage[] {
return this.agent.loadSessionMessagesForDisplay(options);
}
/**
* Get current provider and model information.
*/

View file

@ -297,59 +297,4 @@ export class ProfileManager {
}
}
/** 获取 Agent 风格 */
getStyle(): string | undefined {
const profile = this.getProfile();
return profile?.config?.style;
}
/** 更新 Agent 风格 */
updateStyle(style: string): void {
const profile = this.getOrCreateProfile(false);
const currentConfig = profile.config ?? {};
// Use Object.assign to avoid exactOptionalPropertyTypes issues with spread
const newConfig: ProfileConfig = Object.assign({}, currentConfig, {
style: style as ProfileConfig["style"],
});
profile.config = newConfig;
this.profile = profile;
writeProfileConfig(this.profileId, newConfig, { baseDir: this.baseDir });
// Also update soul.md to include the style
this.updateSoulWithStyle(style);
}
/** 更新 soul.md确保包含 Agent 风格 */
private updateSoulWithStyle(style: string): void {
const profile = this.getOrCreateProfile(true);
let soulContent = profile.soul ?? DEFAULT_TEMPLATES.soul;
// 替换 soul.md 中的 Style 字段
// 匹配 "- **Style:** xxx" 格式
const stylePattern = /- \*\*Style:\*\* .*/;
const newStyleLine = `- **Style:** ${style}`;
if (stylePattern.test(soulContent)) {
soulContent = soulContent.replace(stylePattern, newStyleLine);
} else {
// 如果没有找到 Style 字段,在 Identity 部分的 Role 后添加
const rolePattern = /(- \*\*Role:\*\* .*)/;
if (rolePattern.test(soulContent)) {
soulContent = soulContent.replace(rolePattern, `$1\n${newStyleLine}`);
} else {
// 如果没有 Role尝试在 Name 后添加
const namePattern = /(- \*\*Name:\*\* .*)/;
if (namePattern.test(soulContent)) {
soulContent = soulContent.replace(namePattern, `$1\n${newStyleLine}`);
}
}
}
// 保存更新后的 soul.md
writeProfileFile(this.profileId, PROFILE_FILES.soul, soulContent, { baseDir: this.baseDir });
// 更新缓存
if (this.profile) {
this.profile.soul = soulContent;
}
}
}

View file

@ -15,22 +15,10 @@ export const PROFILE_FILES = {
config: "config.json",
} as const;
/** Available style options for agent personality */
export const AGENT_STYLES = [
"concise", // 简洁直接
"warm", // 温暖友好
"playful", // 轻松活泼
"professional", // 专业正式
] as const;
export type AgentStyle = (typeof AGENT_STYLES)[number];
/** Profile config.json structure */
export interface ProfileConfig {
/** Agent display name */
name?: string;
/** Agent communication style */
style?: AgentStyle;
/** Tools policy configuration */
tools?: ToolsConfig;
/** Default LLM provider */

View file

@ -1,4 +1,5 @@
import { Agent as PiAgentCore, type AgentEvent, type AgentMessage } from "@mariozechner/pi-agent-core";
import type { UserMessage } from "@mariozechner/pi-ai";
import { v7 as uuidv7 } from "uuid";
import type { AgentOptions, AgentRunResult, ReasoningMode } from "./types.js";
import type { MulticaEvent, CompactionEndEvent } from "./events.js";
@ -87,6 +88,7 @@ export class Agent {
// Internal run state
private _internalRun = false;
private _runMutex: Promise<void> = Promise.resolve();
private currentUserDisplayPrompt: string | undefined;
// MulticaEvent subscribers (parallel to PiAgentCore's subscriber list)
// Typed as AgentEvent | MulticaEvent to match subscribeAll() callback signature
@ -364,9 +366,12 @@ export class Agent {
this.emitMulticaEvent({ type: "agent_error", message });
}
async run(prompt: string): Promise<AgentRunResult> {
async run(
prompt: string,
options?: { displayPrompt?: string },
): Promise<AgentRunResult> {
// Run-level mutex: prevents concurrent run/runInternal from mis-tagging messages
return this.withRunMutex(() => this._run(prompt));
return this.withRunMutex(() => this._run(prompt, options));
}
/**
@ -406,70 +411,78 @@ export class Agent {
}
}
private async _run(prompt: string): Promise<AgentRunResult> {
private async _run(
prompt: string,
options?: { displayPrompt?: string },
): Promise<AgentRunResult> {
await this.ensureInitialized();
this.refreshAuthState();
this.output.state.lastAssistantText = "";
this.currentUserDisplayPrompt = options?.displayPrompt;
// Early validation: check API key before calling PiAgentCore.prompt(),
// because getApiKey errors thrown inside PiAgentCore's internal async
// context result in UnhandledPromiseRejection instead of propagating.
if (!this.currentApiKey) {
const errorMsg = `No API key configured for provider: ${this.resolvedProvider}. Please configure a provider in Agent Settings.`;
return { text: "", error: errorMsg };
}
const canRotate = !this.pinnedProfile && this.profileCandidates.length > 1;
let lastError: unknown;
// Loop to exhaust all candidate profiles on rotatable errors
while (true) {
try {
await this.agent.prompt(prompt);
break; // success — exit loop
} catch (error) {
lastError = error;
const reason = classifyError(error);
if (this.currentProfileId && isRotatableError(reason)) {
markAuthProfileFailure(this.currentProfileId, reason);
}
if (!canRotate || !this.currentProfileId) throw error;
if (!isRotatableError(reason)) throw error;
if (this.debug) {
this.stderr.write(
`[auth-profile] Profile "${this.currentProfileId}" failed (${reason}), attempting rotation...\n`,
);
}
if (!this.advanceAuthProfile()) {
throw lastError; // All profiles exhausted
}
if (this.debug) {
this.stderr.write(
`[auth-profile] Rotated to profile "${this.currentProfileId}"\n`,
);
}
// Reset output for retry
this.output.state.lastAssistantText = "";
// continue loop with new profile
try {
// Early validation: check API key before calling PiAgentCore.prompt(),
// because getApiKey errors thrown inside PiAgentCore's internal async
// context result in UnhandledPromiseRejection instead of propagating.
if (!this.currentApiKey) {
const errorMsg = `No API key configured for provider: ${this.resolvedProvider}. Please configure a provider in Agent Settings.`;
return { text: "", error: errorMsg };
}
}
// Mark success
if (this.currentProfileId) {
markAuthProfileUsed(this.currentProfileId);
markAuthProfileGood(this.resolvedProvider, this.currentProfileId);
}
const canRotate = !this.pinnedProfile && this.profileCandidates.length > 1;
let lastError: unknown;
const thinking = this.reasoningMode !== "off"
? this.output.state.lastAssistantThinking || undefined
: undefined;
return { text: this.output.state.lastAssistantText, thinking, error: this.agent.state.error };
// Loop to exhaust all candidate profiles on rotatable errors
while (true) {
try {
await this.agent.prompt(prompt);
break; // success — exit loop
} catch (error) {
lastError = error;
const reason = classifyError(error);
if (this.currentProfileId && isRotatableError(reason)) {
markAuthProfileFailure(this.currentProfileId, reason);
}
if (!canRotate || !this.currentProfileId) throw error;
if (!isRotatableError(reason)) throw error;
if (this.debug) {
this.stderr.write(
`[auth-profile] Profile "${this.currentProfileId}" failed (${reason}), attempting rotation...\n`,
);
}
if (!this.advanceAuthProfile()) {
throw lastError; // All profiles exhausted
}
if (this.debug) {
this.stderr.write(
`[auth-profile] Rotated to profile "${this.currentProfileId}"\n`,
);
}
// Reset output for retry
this.output.state.lastAssistantText = "";
// continue loop with new profile
}
}
// Mark success
if (this.currentProfileId) {
markAuthProfileUsed(this.currentProfileId);
markAuthProfileGood(this.resolvedProvider, this.currentProfileId);
}
const thinking = this.reasoningMode !== "off"
? this.output.state.lastAssistantThinking || undefined
: undefined;
return { text: this.output.state.lastAssistantText, thinking, error: this.agent.state.error };
} finally {
this.currentUserDisplayPrompt = undefined;
}
}
/**
@ -560,7 +573,14 @@ export class Agent {
private handleSessionEvent(event: AgentEvent) {
if (event.type === "message_end") {
const message = event.message as AgentMessage;
this.session.saveMessage(message, this._internalRun ? { internal: true } : undefined);
const saveOptions: { internal?: boolean; displayContent?: UserMessage["content"] } = {};
if (this._internalRun) {
saveOptions.internal = true;
}
if (message.role === "user" && this.currentUserDisplayPrompt !== undefined) {
saveOptions.displayContent = this.currentUserDisplayPrompt;
}
this.session.saveMessage(message, Object.keys(saveOptions).length > 0 ? saveOptions : undefined);
// Skip compaction during internal runs — internal messages will be
// rolled back from memory afterwards, so compacting now would be incorrect.
if (message.role === "assistant" && !this._internalRun) {
@ -689,6 +709,14 @@ export class Agent {
return this.session.loadMessages(options);
}
/**
* Load messages from session storage for UI rendering.
* User messages prefer stored displayContent when present.
*/
loadSessionMessagesForDisplay(options?: { includeInternal?: boolean }): AgentMessage[] {
return this.session.loadMessagesForDisplay(options);
}
/**
* Get all skills with their eligibility status.
* Returns empty array if skills are disabled.
@ -807,20 +835,6 @@ export class Agent {
this.profile?.updateUserContent(content);
}
/**
* Get agent communication style from profile config.
*/
getAgentStyle(): string | undefined {
return this.profile?.getStyle();
}
/**
* Update agent communication style in profile config.
*/
setAgentStyle(style: string): void {
this.profile?.updateStyle(style);
}
/**
* Get current provider and model information.
*/

View file

@ -0,0 +1,96 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { existsSync, mkdirSync, rmSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { SessionManager } from "./session-manager.js";
import { readEntries, writeEntries } from "./storage.js";
import type { SessionEntry } from "./types.js";
describe("SessionManager display content view", () => {
const testBaseDir = join(tmpdir(), `multica-session-display-${Date.now()}`);
beforeEach(() => {
if (existsSync(testBaseDir)) {
rmSync(testBaseDir, { recursive: true });
}
mkdirSync(testBaseDir, { recursive: true });
});
afterEach(() => {
if (existsSync(testBaseDir)) {
rmSync(testBaseDir, { recursive: true });
}
});
it("uses displayContent for user messages in display view", async () => {
const sessionId = "display-view";
const session = new SessionManager({ sessionId, baseDir: testBaseDir });
const entries: SessionEntry[] = [
{
type: "message",
message: { role: "user", content: "[Mon 2026-02-09 14:37 GMT+8] hi" },
displayContent: "hi",
timestamp: 1,
},
{
type: "message",
message: { role: "assistant", content: "hello there" },
timestamp: 2,
},
];
await writeEntries(sessionId, entries, { baseDir: testBaseDir });
const raw = session.loadMessages();
const display = session.loadMessagesForDisplay();
expect(raw[0]?.content).toBe("[Mon 2026-02-09 14:37 GMT+8] hi");
expect(display[0]?.content).toBe("hi");
expect(display[1]?.content).toBe("hello there");
});
it("keeps internal filtering behavior in display view", async () => {
const sessionId = "display-internal";
const session = new SessionManager({ sessionId, baseDir: testBaseDir });
const entries: SessionEntry[] = [
{
type: "message",
message: { role: "user", content: "[Mon 2026-02-09 14:37 GMT+8] hidden" },
displayContent: "hidden",
internal: true,
timestamp: 1,
},
{
type: "message",
message: { role: "user", content: "[Mon 2026-02-09 14:38 GMT+8] visible" },
displayContent: "visible",
timestamp: 2,
},
];
await writeEntries(sessionId, entries, { baseDir: testBaseDir });
const defaultView = session.loadMessagesForDisplay();
const includeInternalView = session.loadMessagesForDisplay({ includeInternal: true });
expect(defaultView).toHaveLength(1);
expect(defaultView[0]?.content).toBe("visible");
expect(includeInternalView).toHaveLength(2);
expect(includeInternalView[0]?.content).toBe("hidden");
});
it("persists displayContent on saveMessage", async () => {
const sessionId = "display-save";
const session = new SessionManager({ sessionId, baseDir: testBaseDir });
session.saveMessage(
{ role: "user", content: "[Mon 2026-02-09 14:39 GMT+8] save me" },
{ displayContent: "save me" },
);
await session.flush();
const entries = readEntries(sessionId, { baseDir: testBaseDir }) as Array<
Extract<SessionEntry, { type: "message" }>
>;
expect(entries).toHaveLength(1);
expect(entries[0]?.displayContent).toBe("save me");
});
});

View file

@ -1,5 +1,5 @@
import type { AgentMessage } from "@mariozechner/pi-agent-core";
import { getModel, type Model } from "@mariozechner/pi-ai";
import { getModel, type Model, type UserMessage } from "@mariozechner/pi-ai";
import type { SessionEntry, SessionMeta } from "./types.js";
import { appendEntry, readEntries, resolveSessionPath, writeEntries } from "./storage.js";
import { compactMessages, compactMessagesAsync, type CompactionResult } from "./compaction.js";
@ -168,6 +168,17 @@ export class SessionManager {
}
loadMessages(options?: { includeInternal?: boolean }): AgentMessage[] {
return this.loadMessagesFromEntries(options, false);
}
loadMessagesForDisplay(options?: { includeInternal?: boolean }): AgentMessage[] {
return this.loadMessagesFromEntries(options, true);
}
private loadMessagesFromEntries(
options: { includeInternal?: boolean } | undefined,
preferDisplayContent: boolean,
): AgentMessage[] {
const entries = this.loadEntries();
let messages = entries
.filter((entry) => {
@ -175,7 +186,17 @@ export class SessionManager {
if (!options?.includeInternal && entry.internal) return false;
return true;
})
.map((entry) => (entry as { type: "message"; message: AgentMessage }).message);
.map((entry) => {
const messageEntry = entry as Extract<SessionEntry, { type: "message" }>;
if (
preferDisplayContent
&& messageEntry.message.role === "user"
&& messageEntry.displayContent !== undefined
) {
return { ...messageEntry.message, content: messageEntry.displayContent };
}
return messageEntry.message;
});
messages = sanitizeToolCallInputs(messages);
messages = sanitizeToolUseResultPairing(messages);
return messages;
@ -207,7 +228,10 @@ export class SessionManager {
);
}
saveMessage(message: AgentMessage, options?: { internal?: boolean }) {
saveMessage(
message: AgentMessage,
options?: { internal?: boolean; displayContent?: UserMessage["content"] },
) {
void this.enqueue(() =>
appendEntry(
this.sessionId,
@ -216,6 +240,9 @@ export class SessionManager {
message,
timestamp: Date.now(),
...(options?.internal ? { internal: true } : {}),
...(options?.displayContent !== undefined
? { displayContent: options.displayContent }
: {}),
},
{ baseDir: this.baseDir },
),

View file

@ -1,4 +1,5 @@
import type { AgentMessage } from "@mariozechner/pi-agent-core";
import type { UserMessage } from "@mariozechner/pi-ai";
export type SessionMeta = {
provider?: string;
@ -11,7 +12,17 @@ export type SessionMeta = {
};
export type SessionEntry =
| { type: "message"; message: AgentMessage; timestamp: number; internal?: boolean }
| {
type: "message";
message: AgentMessage;
timestamp: number;
internal?: boolean;
/**
* User-visible content preserved for UI/history rendering.
* When omitted, consumers should fall back to message.content.
*/
displayContent?: UserMessage["content"];
}
| { type: "meta"; meta: SessionMeta; timestamp: number }
| {
type: "compaction";

View file

@ -29,7 +29,7 @@ export function createGetAgentMessagesHandler(): RpcHandler {
}
const session = new SessionManager({ sessionId: agentId });
const allMessages = session.loadMessages();
const allMessages = session.loadMessagesForDisplay();
const total = allMessages.length;
// When offset is not provided, return the latest messages