mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-20 17:02:11 +00:00
Add CustomMessageEntry for hook-injected messages in LLM context
- CustomMessageEntry<T> type with customType, content, display, details - appendCustomMessageEntry() in SessionManager - buildSessionContext() includes custom_message entries as user messages - Exported CustomEntry and CustomMessageEntry from main index CustomEntry is for hook state (not in context). CustomMessageEntry is for hook-injected content (in context).
This commit is contained in:
parent
9bba388ec5
commit
9da36e5ac6
4 changed files with 100 additions and 11 deletions
|
|
@ -13,7 +13,8 @@
|
||||||
- `saveCompaction(entry)` replaced with `appendCompaction(summary, firstKeptEntryId, tokensBefore)`
|
- `saveCompaction(entry)` replaced with `appendCompaction(summary, firstKeptEntryId, tokensBefore)`
|
||||||
- `getEntries()` now excludes the session header (use `getHeader()` separately)
|
- `getEntries()` now excludes the session header (use `getHeader()` separately)
|
||||||
- New methods: `getTree()`, `getPath()`, `getLeafUuid()`, `getLeafEntry()`, `getEntry()`, `branchWithSummary()`
|
- New methods: `getTree()`, `getPath()`, `getLeafUuid()`, `getLeafEntry()`, `getEntry()`, `branchWithSummary()`
|
||||||
- New `appendCustomEntry(customType, data)` for hooks to store custom data
|
- New `appendCustomEntry(customType, data)` for hooks to store custom data (not in LLM context)
|
||||||
|
- New `appendCustomMessageEntry(customType, content, display, details?)` for hooks to inject messages into LLM context
|
||||||
- **Compaction API**:
|
- **Compaction API**:
|
||||||
- `CompactionEntry<T>` and `CompactionResult<T>` are now generic with optional `details?: T` for hook-specific data
|
- `CompactionEntry<T>` and `CompactionResult<T>` are now generic with optional `details?: T` for hook-specific data
|
||||||
- `compact()` now returns `CompactionResult` (`{ summary, firstKeptEntryId, tokensBefore, details? }`) instead of `CompactionEntry`
|
- `compact()` now returns `CompactionResult` (`{ summary, firstKeptEntryId, tokensBefore, details? }`) instead of `CompactionEntry`
|
||||||
|
|
|
||||||
|
|
@ -118,14 +118,14 @@ export interface CustomMessageEntry<T = unknown> extends SessionEntryBase {
|
||||||
```
|
```
|
||||||
|
|
||||||
Behavior:
|
Behavior:
|
||||||
- [ ] Participates in context and compaction as user messages (after messageTransformer)
|
- [x] Type definition matching plan
|
||||||
- [ ] Not displayed as user messages in TUI
|
- [x] `appendCustomMessageEntry(customType, content, display, details?)` in SessionManager
|
||||||
- [ ] Display options:
|
- [x] `buildSessionContext()` includes custom_message entries as user messages
|
||||||
|
- [x] Exported from main index
|
||||||
|
- [ ] TUI rendering:
|
||||||
- `display: false` - hidden entirely
|
- `display: false` - hidden entirely
|
||||||
- `display: true` - baseline renderer (content with different bg/fg color)
|
- `display: true` - baseline renderer (content with different bg/fg color)
|
||||||
- Custom renderer defined by the hook that contributes it
|
- Custom renderer defined by the hook that contributes it (future)
|
||||||
- [ ] Define injection mechanism for hooks to add CustomMessageEntry
|
|
||||||
- [ ] Hook registration for custom renderers
|
|
||||||
|
|
||||||
See also: `CustomEntry<T>` for storing hook state that does NOT participate in context.
|
See also: `CustomEntry<T>` for storing hook state that does NOT participate in context.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { AppMessage } from "@mariozechner/pi-agent-core";
|
import type { AppMessage, Attachment } from "@mariozechner/pi-agent-core";
|
||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from "crypto";
|
||||||
import {
|
import {
|
||||||
appendFileSync,
|
appendFileSync,
|
||||||
|
|
@ -86,6 +86,26 @@ export interface LabelEntry extends SessionEntryBase {
|
||||||
label: string | undefined;
|
label: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom message entry for hooks to inject messages into LLM context.
|
||||||
|
* Use customType to identify your hook's entries.
|
||||||
|
*
|
||||||
|
* Unlike CustomEntry, this DOES participate in LLM context.
|
||||||
|
* The content is converted to a user message in buildSessionContext().
|
||||||
|
* Use details for hook-specific metadata (not sent to LLM).
|
||||||
|
*
|
||||||
|
* display controls TUI rendering:
|
||||||
|
* - false: hidden entirely
|
||||||
|
* - true: rendered with distinct styling (different from user messages)
|
||||||
|
*/
|
||||||
|
export interface CustomMessageEntry<T = unknown> extends SessionEntryBase {
|
||||||
|
type: "custom_message";
|
||||||
|
customType: string;
|
||||||
|
content: (string | Attachment)[];
|
||||||
|
details?: T;
|
||||||
|
display: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/** Session entry - has id/parentId for tree structure (returned by "read" methods in SessionManager) */
|
/** Session entry - has id/parentId for tree structure (returned by "read" methods in SessionManager) */
|
||||||
export type SessionEntry =
|
export type SessionEntry =
|
||||||
| SessionMessageEntry
|
| SessionMessageEntry
|
||||||
|
|
@ -94,6 +114,7 @@ export type SessionEntry =
|
||||||
| CompactionEntry
|
| CompactionEntry
|
||||||
| BranchSummaryEntry
|
| BranchSummaryEntry
|
||||||
| CustomEntry
|
| CustomEntry
|
||||||
|
| CustomMessageEntry
|
||||||
| LabelEntry;
|
| LabelEntry;
|
||||||
|
|
||||||
/** Raw file entry (includes header) */
|
/** Raw file entry (includes header) */
|
||||||
|
|
@ -140,6 +161,35 @@ export function createSummaryMessage(summary: string, timestamp: string): AppMes
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Convert CustomMessageEntry content to AppMessage format */
|
||||||
|
function createCustomMessage(entry: CustomMessageEntry): AppMessage {
|
||||||
|
// Convert content array to AppMessage content format
|
||||||
|
const content = entry.content.map((item) => {
|
||||||
|
if (typeof item === "string") {
|
||||||
|
return { type: "text" as const, text: item };
|
||||||
|
}
|
||||||
|
// Attachment - convert to appropriate content type
|
||||||
|
if (item.type === "image") {
|
||||||
|
return {
|
||||||
|
type: "image" as const,
|
||||||
|
data: item.content,
|
||||||
|
mimeType: item.mimeType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Document attachment - use extracted text or indicate document
|
||||||
|
return {
|
||||||
|
type: "text" as const,
|
||||||
|
text: item.extractedText ?? `[Document: ${item.fileName}]`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
role: "user",
|
||||||
|
content,
|
||||||
|
timestamp: new Date(entry.timestamp).getTime(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** Generate a unique short ID (8 hex chars, collision-checked) */
|
/** Generate a unique short ID (8 hex chars, collision-checked) */
|
||||||
function generateId(byId: { has(id: string): boolean }): string {
|
function generateId(byId: { has(id: string): boolean }): string {
|
||||||
for (let i = 0; i < 100; i++) {
|
for (let i = 0; i < 100; i++) {
|
||||||
|
|
@ -308,8 +358,12 @@ export function buildSessionContext(
|
||||||
if (entry.id === compaction.firstKeptEntryId) {
|
if (entry.id === compaction.firstKeptEntryId) {
|
||||||
foundFirstKept = true;
|
foundFirstKept = true;
|
||||||
}
|
}
|
||||||
if (foundFirstKept && entry.type === "message") {
|
if (foundFirstKept) {
|
||||||
|
if (entry.type === "message") {
|
||||||
messages.push(entry.message);
|
messages.push(entry.message);
|
||||||
|
} else if (entry.type === "custom_message") {
|
||||||
|
messages.push(createCustomMessage(entry));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -318,15 +372,19 @@ export function buildSessionContext(
|
||||||
const entry = path[i];
|
const entry = path[i];
|
||||||
if (entry.type === "message") {
|
if (entry.type === "message") {
|
||||||
messages.push(entry.message);
|
messages.push(entry.message);
|
||||||
|
} else if (entry.type === "custom_message") {
|
||||||
|
messages.push(createCustomMessage(entry));
|
||||||
} else if (entry.type === "branch_summary") {
|
} else if (entry.type === "branch_summary") {
|
||||||
messages.push(createSummaryMessage(entry.summary, entry.timestamp));
|
messages.push(createSummaryMessage(entry.summary, entry.timestamp));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No compaction - emit all messages, handle branch summaries
|
// No compaction - emit all messages, handle branch summaries and custom messages
|
||||||
for (const entry of path) {
|
for (const entry of path) {
|
||||||
if (entry.type === "message") {
|
if (entry.type === "message") {
|
||||||
messages.push(entry.message);
|
messages.push(entry.message);
|
||||||
|
} else if (entry.type === "custom_message") {
|
||||||
|
messages.push(createCustomMessage(entry));
|
||||||
} else if (entry.type === "branch_summary") {
|
} else if (entry.type === "branch_summary") {
|
||||||
messages.push(createSummaryMessage(entry.summary, entry.timestamp));
|
messages.push(createSummaryMessage(entry.summary, entry.timestamp));
|
||||||
}
|
}
|
||||||
|
|
@ -623,6 +681,34 @@ export class SessionManager {
|
||||||
return entry.id;
|
return entry.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append a custom message entry (for hooks) that participates in LLM context.
|
||||||
|
* @param customType Hook identifier for filtering on reload
|
||||||
|
* @param content Message content (strings and attachments)
|
||||||
|
* @param display Whether to show in TUI (true = styled display, false = hidden)
|
||||||
|
* @param details Optional hook-specific metadata (not sent to LLM)
|
||||||
|
* @returns Entry id
|
||||||
|
*/
|
||||||
|
appendCustomMessageEntry<T = unknown>(
|
||||||
|
customType: string,
|
||||||
|
content: (string | Attachment)[],
|
||||||
|
display: boolean,
|
||||||
|
details?: T,
|
||||||
|
): string {
|
||||||
|
const entry: CustomMessageEntry<T> = {
|
||||||
|
type: "custom_message",
|
||||||
|
customType,
|
||||||
|
content,
|
||||||
|
display,
|
||||||
|
details,
|
||||||
|
id: generateId(this.byId),
|
||||||
|
parentId: this.leafId || null,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
this._appendEntry(entry);
|
||||||
|
return entry.id;
|
||||||
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Tree Traversal
|
// Tree Traversal
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,8 @@ export {
|
||||||
buildSessionContext,
|
buildSessionContext,
|
||||||
type CompactionEntry,
|
type CompactionEntry,
|
||||||
CURRENT_SESSION_VERSION,
|
CURRENT_SESSION_VERSION,
|
||||||
|
type CustomEntry,
|
||||||
|
type CustomMessageEntry,
|
||||||
createSummaryMessage,
|
createSummaryMessage,
|
||||||
type FileEntry,
|
type FileEntry,
|
||||||
getLatestCompactionEntry,
|
getLatestCompactionEntry,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue