@linxiraos/pi-agent-core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/CHANGELOG.md +1263 -0
  2. package/README.md +473 -0
  3. package/dist/types/agent-loop.d.ts +137 -0
  4. package/dist/types/agent.d.ts +465 -0
  5. package/dist/types/append-only-context.d.ts +131 -0
  6. package/dist/types/compaction/branch-summarization.d.ts +101 -0
  7. package/dist/types/compaction/compaction-v2-streaming.d.ts +85 -0
  8. package/dist/types/compaction/compaction.d.ts +307 -0
  9. package/dist/types/compaction/entries.d.ts +111 -0
  10. package/dist/types/compaction/errors.d.ts +38 -0
  11. package/dist/types/compaction/index.d.ts +13 -0
  12. package/dist/types/compaction/message-cache.d.ts +23 -0
  13. package/dist/types/compaction/messages.d.ts +79 -0
  14. package/dist/types/compaction/openai.d.ts +117 -0
  15. package/dist/types/compaction/pruning.d.ts +105 -0
  16. package/dist/types/compaction/shake.d.ts +97 -0
  17. package/dist/types/compaction/tool-protection.d.ts +19 -0
  18. package/dist/types/compaction/utils.d.ts +61 -0
  19. package/dist/types/compaction.d.ts +1 -0
  20. package/dist/types/index.d.ts +13 -0
  21. package/dist/types/pause.d.ts +46 -0
  22. package/dist/types/proxy.d.ts +91 -0
  23. package/dist/types/replay-policy.d.ts +5 -0
  24. package/dist/types/run-collector.d.ts +196 -0
  25. package/dist/types/telemetry.d.ts +608 -0
  26. package/dist/types/thinking.d.ts +18 -0
  27. package/dist/types/tokenizer.d.ts +2 -0
  28. package/dist/types/types.d.ts +781 -0
  29. package/dist/types/utils/yield.d.ts +71 -0
  30. package/package.json +79 -0
  31. package/src/agent-loop.ts +2926 -0
  32. package/src/agent.ts +1748 -0
  33. package/src/append-only-context.ts +348 -0
  34. package/src/compaction/branch-summarization.ts +369 -0
  35. package/src/compaction/compaction-v2-streaming.ts +846 -0
  36. package/src/compaction/compaction.ts +1733 -0
  37. package/src/compaction/entries.ts +141 -0
  38. package/src/compaction/errors.ts +47 -0
  39. package/src/compaction/index.ts +14 -0
  40. package/src/compaction/message-cache.ts +92 -0
  41. package/src/compaction/messages.ts +241 -0
  42. package/src/compaction/openai.ts +992 -0
  43. package/src/compaction/prompts/auto-handoff-threshold-focus.md +1 -0
  44. package/src/compaction/prompts/branch-summary-context.md +5 -0
  45. package/src/compaction/prompts/branch-summary-preamble.md +2 -0
  46. package/src/compaction/prompts/branch-summary.md +30 -0
  47. package/src/compaction/prompts/compaction-short-summary.md +9 -0
  48. package/src/compaction/prompts/compaction-summary-context.md +5 -0
  49. package/src/compaction/prompts/compaction-summary.md +38 -0
  50. package/src/compaction/prompts/compaction-turn-prefix.md +17 -0
  51. package/src/compaction/prompts/compaction-update-summary.md +45 -0
  52. package/src/compaction/prompts/context-window-truncated-output.md +1 -0
  53. package/src/compaction/prompts/file-operations.md +5 -0
  54. package/src/compaction/prompts/handoff-document.md +49 -0
  55. package/src/compaction/prompts/snapcompact-archive-context.md +3 -0
  56. package/src/compaction/prompts/summarization-system.md +3 -0
  57. package/src/compaction/pruning.ts +427 -0
  58. package/src/compaction/shake.ts +446 -0
  59. package/src/compaction/tool-protection.ts +65 -0
  60. package/src/compaction/utils.ts +343 -0
  61. package/src/compaction.ts +1 -0
  62. package/src/index.ts +26 -0
  63. package/src/pause.ts +107 -0
  64. package/src/proxy.ts +391 -0
  65. package/src/replay-policy.ts +13 -0
  66. package/src/run-collector.ts +631 -0
  67. package/src/telemetry.ts +2078 -0
  68. package/src/thinking.ts +20 -0
  69. package/src/tokenizer.ts +27 -0
  70. package/src/types.ts +875 -0
  71. package/src/utils/yield.ts +183 -0
@@ -0,0 +1,141 @@
1
+ import type { ImageContent, MessageAttribution, ServiceTierByFamily, TextContent } from "@linxiraos/pi-ai";
2
+ import type { AgentMessage } from "../types";
3
+
4
+ export interface SessionEntryBase {
5
+ type: string;
6
+ id: string;
7
+ parentId: string | null;
8
+ timestamp: string;
9
+ }
10
+
11
+ export interface SessionMessageEntry extends SessionEntryBase {
12
+ type: "message";
13
+ message: AgentMessage;
14
+ }
15
+
16
+ export interface ThinkingLevelChangeEntry extends SessionEntryBase {
17
+ type: "thinking_level_change";
18
+ thinkingLevel?: string | null;
19
+ }
20
+
21
+ export interface ModelChangeEntry extends SessionEntryBase {
22
+ type: "model_change";
23
+ /** Model in "provider/modelId" format */
24
+ model: string;
25
+ /** Role: "default", "smol", "slow", etc. Undefined treated as "default" */
26
+ role?: string;
27
+ }
28
+
29
+ export interface ServiceTierChangeEntry extends SessionEntryBase {
30
+ type: "service_tier_change";
31
+ serviceTier: ServiceTierByFamily | null;
32
+ }
33
+
34
+ export interface CompactionEntry<T = unknown> extends SessionEntryBase {
35
+ type: "compaction";
36
+ summary: string;
37
+ shortSummary?: string;
38
+ firstKeptEntryId: string;
39
+ tokensBefore: number;
40
+ /** Extension-specific data (e.g., ArtifactIndex, version markers for structured compaction) */
41
+ details?: T;
42
+ /** Hook-provided data to persist across compaction */
43
+ preserveData?: Record<string, unknown>;
44
+ /** True if generated by an extension, undefined/false if pi-generated (backward compatible) */
45
+ fromExtension?: boolean;
46
+ /**
47
+ * Dead-end warning from the post-pass progress guard: the pass completed
48
+ * but freed too little for maintenance to continue. Rendered on the
49
+ * compaction divider.
50
+ */
51
+ warning?: string;
52
+ }
53
+
54
+ export interface BranchSummaryEntry<T = unknown> extends SessionEntryBase {
55
+ type: "branch_summary";
56
+ fromId: string;
57
+ summary: string;
58
+ /** Extension-specific data (not sent to LLM) */
59
+ details?: T;
60
+ /** True if generated by an extension, false if pi-generated */
61
+ fromExtension?: boolean;
62
+ }
63
+
64
+ export interface CustomMessageEntry<T = unknown> extends SessionEntryBase {
65
+ type: "custom_message";
66
+ customType: string;
67
+ content: string | (TextContent | ImageContent)[];
68
+ details?: T;
69
+ display: boolean;
70
+ /** Who initiated this message for billing/attribution semantics. */
71
+ attribution?: MessageAttribution;
72
+ }
73
+
74
+ export interface CustomEntry<T = unknown> extends SessionEntryBase {
75
+ type: "custom";
76
+ customType: string;
77
+ data?: T;
78
+ }
79
+
80
+ export interface LabelEntry extends SessionEntryBase {
81
+ type: "label";
82
+ targetId: string;
83
+ label: string | undefined;
84
+ }
85
+
86
+ export interface TitleChangeEntry extends SessionEntryBase {
87
+ type: "title_change";
88
+ title: string;
89
+ previousTitle?: string;
90
+ source: "auto" | "user";
91
+ trigger?: string;
92
+ }
93
+
94
+ export interface TtsrInjectionEntry extends SessionEntryBase {
95
+ type: "ttsr_injection";
96
+ /** Names of rules that were injected */
97
+ injectedRules: string[];
98
+ }
99
+
100
+ export interface SessionInitEntry extends SessionEntryBase {
101
+ type: "session_init";
102
+ /** Full system prompt sent to the model */
103
+ systemPrompt: string;
104
+ /** Initial task/user message */
105
+ task: string;
106
+ /** Tools available to the agent */
107
+ tools: string[];
108
+ /** Output schema if structured output was requested */
109
+ outputSchema?: unknown;
110
+ }
111
+
112
+ export interface ModeChangeEntry extends SessionEntryBase {
113
+ type: "mode_change";
114
+ /** Current mode name, or "none" when exiting a mode */
115
+ mode: string;
116
+ /** Optional mode-specific data (e.g. plan file path) */
117
+ data?: Record<string, unknown>;
118
+ }
119
+
120
+ export interface CustomCompactionSessionEntries {}
121
+
122
+ export type SessionEntry =
123
+ | SessionMessageEntry
124
+ | ThinkingLevelChangeEntry
125
+ | ModelChangeEntry
126
+ | ServiceTierChangeEntry
127
+ | CompactionEntry
128
+ | BranchSummaryEntry
129
+ | CustomEntry
130
+ | CustomMessageEntry
131
+ | LabelEntry
132
+ | TitleChangeEntry
133
+ | TtsrInjectionEntry
134
+ | SessionInitEntry
135
+ | ModeChangeEntry
136
+ | CustomCompactionSessionEntries[keyof CustomCompactionSessionEntries];
137
+
138
+ export interface ReadonlySessionManager {
139
+ getBranch(leafId?: string | null): SessionEntry[];
140
+ getEntry(id: string): SessionEntry | undefined;
141
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Compaction error types.
3
+ *
4
+ * `CompactionCancelledError` is the canonical signal raised when a compaction
5
+ * is explicitly aborted — operator Esc, extension hook returning `cancel`,
6
+ * programmatic `session.abortCompaction()` call, or any other deliberate
7
+ * abort source. Downstream callers (e.g. `executeCompaction`) discriminate
8
+ * cancellation from other failures via `instanceof CompactionCancelledError`
9
+ * rather than introspecting error messages or `name` fields — the typed
10
+ * sentinel makes classification source-agnostic and refactor-stable.
11
+ */
12
+
13
+ export class CompactionCancelledError extends Error {
14
+ override readonly name = "CompactionCancelledError" as const;
15
+
16
+ constructor(message = "Compaction cancelled") {
17
+ super(message);
18
+ }
19
+ }
20
+
21
+ /**
22
+ * A provider-native compaction request failed after every native protocol
23
+ * available for the selected model was exhausted.
24
+ *
25
+ * The cause stays attached so AI error classification can still recognize
26
+ * authentication failures. Non-auth failures remain distinguishable from
27
+ * ordinary summarization errors and must not fall through to another provider.
28
+ */
29
+ export class NativeCompactionError extends Error {
30
+ override readonly name = "NativeCompactionError" as const;
31
+
32
+ constructor(cause: unknown) {
33
+ super(cause instanceof Error ? cause.message : String(cause), { cause });
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Outcome of a compaction attempt, surfaced by `CommandController.executeCompaction`
39
+ * so callers (e.g. the plan-mode approval flow) can distinguish a deliberate abort
40
+ * from an unrelated failure.
41
+ *
42
+ * "ok" — compaction completed; transcript was summarized.
43
+ * "cancelled" — `CompactionCancelledError` was raised. Operator Esc, extension
44
+ * hook, programmatic abort — all source-agnostic.
45
+ * "failed" — any other rejection from `session.compact()`.
46
+ */
47
+ export type CompactionOutcome = "ok" | "cancelled" | "failed";
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Compaction and summarization utilities.
3
+ */
4
+
5
+ export * from "./branch-summarization";
6
+ export * from "./compaction";
7
+ export * from "./entries";
8
+ export * from "./errors";
9
+ export * from "./message-cache";
10
+ export * from "./messages";
11
+ export * from "./openai";
12
+ export * from "./pruning";
13
+ export * from "./shake";
14
+ export * from "./utils";
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Per-message memoization for the two hot history walks: token estimation
3
+ * ({@link estimateTokens}) and LLM conversion (the coding-agent's `convertToLlm`).
4
+ *
5
+ * Long sessions re-walk a settled `AgentMessage[]` every turn, re-tokenizing and
6
+ * re-converting historical objects that only the newest suffix can change. These
7
+ * caches key on message *identity* so a settled message is counted/converted once
8
+ * and reused until an owner rewrites it.
9
+ *
10
+ * Correctness rests on two invariants:
11
+ *
12
+ * 1. **Settle gate.** A streaming assistant is mutated under one identity while
13
+ * its `usage`/`stopReason` are provisional (the seed carries zeroed usage and
14
+ * a placeholder `stopReason`). Caching it would freeze a mid-stream count, so
15
+ * estimation only caches assistants that are settled — real `usage`
16
+ * (`totalTokens > 0`) with a terminal `stopReason` that is not `"aborted"` /
17
+ * `"error"`. Unsettled assistants never read or insert. Non-assistant roles
18
+ * are immutable once appended and cache by identity.
19
+ * 2. **Owner invalidation.** `pruneToolOutputs` / `pruneSupersededToolResults`,
20
+ * `applyShakeRegion`, and `stripImagesFromMessage` rewrite message content in
21
+ * place under a stable identity. Each MUST call {@link invalidateMessageCache}
22
+ * on the mutated message before the next convert/estimate pass so both caches
23
+ * drop the stale entry. The convert cache lives in another package, so it
24
+ * subscribes via {@link registerMessageCacheInvalidator}.
25
+ */
26
+ import type { AssistantMessage } from "@linxiraos/pi-ai";
27
+ import type { AgentMessage } from "../types";
28
+
29
+ /** External cache invalidators (e.g. the coding-agent `convertToLlm` memo). */
30
+ const externalInvalidators = new Set<(message: AgentMessage) => void>();
31
+
32
+ /**
33
+ * Register a cache tied to message identity so owner mutations in this package
34
+ * (prune/shake) can invalidate it across the package boundary. Returns an
35
+ * unregister function. The coding-agent `convertToLlm` memo registers here.
36
+ */
37
+ export function registerMessageCacheInvalidator(invalidate: (message: AgentMessage) => void): () => void {
38
+ externalInvalidators.add(invalidate);
39
+ return () => {
40
+ externalInvalidators.delete(invalidate);
41
+ };
42
+ }
43
+
44
+ // Dual option-split estimate caches: the compaction floor passes
45
+ // `excludeEncryptedReasoning` (dropping opaque provider reasoning), so a message
46
+ // has two distinct estimates that must not collide in one map.
47
+ //
48
+ // These are WeakMaps, not symbol-tagged properties, deliberately: callers spread
49
+ // messages to derive throwaway variants for counting — `estimateBranchSummaryTokens`
50
+ // does `estimateTokens({ ...message, content: truncated })`. A symbol-keyed cache
51
+ // value rides along an object spread, so the truncated clone would inherit (and
52
+ // return) the full-content estimate. Keying strictly on identity keeps the cache
53
+ // off spread copies, which get their own fresh count.
54
+ const estimateCacheDefault = new WeakMap<AgentMessage, number>();
55
+ const estimateCacheFloored = new WeakMap<AgentMessage, number>();
56
+
57
+ /**
58
+ * True when this message's estimate is safe to cache by identity. Non-assistants
59
+ * are immutable once appended; assistants are cached only once settled (see the
60
+ * settle-gate invariant above).
61
+ */
62
+ export function isEstimateCacheable(message: AgentMessage): boolean {
63
+ if (message.role !== "assistant") return true;
64
+ const assistant = message as AssistantMessage;
65
+ return (
66
+ assistant.stopReason !== "aborted" &&
67
+ assistant.stopReason !== "error" &&
68
+ assistant.usage != null &&
69
+ assistant.usage.totalTokens > 0
70
+ );
71
+ }
72
+
73
+ /** Read a cached estimate for the given option split, or `undefined` on miss. */
74
+ export function readEstimateCache(message: AgentMessage, excludeEncryptedReasoning: boolean): number | undefined {
75
+ return (excludeEncryptedReasoning ? estimateCacheFloored : estimateCacheDefault).get(message);
76
+ }
77
+
78
+ /** Store an estimate for the given option split. */
79
+ export function writeEstimateCache(message: AgentMessage, excludeEncryptedReasoning: boolean, value: number): void {
80
+ (excludeEncryptedReasoning ? estimateCacheFloored : estimateCacheDefault).set(message, value);
81
+ }
82
+
83
+ /**
84
+ * Drop every cached derivation of `message` after an in-place rewrite. Owners of
85
+ * mutation (prune, shake, strip-images) call this at the mutation seam so the
86
+ * next convert/estimate pass recomputes from the new content.
87
+ */
88
+ export function invalidateMessageCache(message: AgentMessage): void {
89
+ estimateCacheDefault.delete(message);
90
+ estimateCacheFloored.delete(message);
91
+ for (const invalidate of externalInvalidators) invalidate(message);
92
+ }
@@ -0,0 +1,241 @@
1
+ import type {
2
+ ImageContent,
3
+ Message,
4
+ MessageAttribution,
5
+ ProviderPayload,
6
+ TextContent,
7
+ ToolResultMessage,
8
+ } from "@linxiraos/pi-ai";
9
+ import { prompt } from "@linxiraos/pi-utils";
10
+ import type { AgentMessage } from "../types";
11
+ import branchSummaryContextPrompt from "./prompts/branch-summary-context.md" with { type: "text" };
12
+ import compactionSummaryContextPrompt from "./prompts/compaction-summary-context.md" with { type: "text" };
13
+
14
+ const COMPACTION_SUMMARY_TEMPLATE = compactionSummaryContextPrompt;
15
+ const BRANCH_SUMMARY_TEMPLATE = branchSummaryContextPrompt;
16
+
17
+ export interface CustomMessage<T = unknown> {
18
+ role: "custom";
19
+ customType: string;
20
+ content: string | (TextContent | ImageContent)[];
21
+ display: boolean;
22
+ details?: T;
23
+ /** Who initiated this message for billing/attribution semantics. */
24
+ attribution?: MessageAttribution;
25
+ timestamp: number;
26
+ }
27
+
28
+ /** Legacy hook message type (pre-extensions). Kept for session migration. */
29
+ export interface HookMessage<T = unknown> {
30
+ role: "hookMessage";
31
+ customType: string;
32
+ content: string | (TextContent | ImageContent)[];
33
+ display: boolean;
34
+ details?: T;
35
+ /** Who initiated this message for billing/attribution semantics. */
36
+ attribution?: MessageAttribution;
37
+ timestamp: number;
38
+ }
39
+
40
+ export interface BranchSummaryMessage {
41
+ role: "branchSummary";
42
+ summary: string;
43
+ fromId: string;
44
+ timestamp: number;
45
+ }
46
+
47
+ export interface CompactionSummaryMessage {
48
+ role: "compactionSummary";
49
+ summary: string;
50
+ shortSummary?: string;
51
+ tokensBefore: number;
52
+ providerPayload?: ProviderPayload;
53
+ /** Runtime-only ordered archive blocks for snapcompact: old text region,
54
+ * imaged middle, then new text region. When present, `summary` is already
55
+ * the final lead-in text (no legacy wrapper applied). */
56
+ blocks?: (TextContent | ImageContent)[];
57
+ /** Snapcompact image blocks, kept for display counts / legacy consumers. */
58
+ images?: ImageContent[];
59
+ /** Post-pass dead-end warning attached to this compaction (progress guard). */
60
+ warning?: string;
61
+ timestamp: number;
62
+ }
63
+
64
+ export type CoreCompactionMessage = CustomMessage | HookMessage | BranchSummaryMessage | CompactionSummaryMessage;
65
+
66
+ declare module "../types" {
67
+ interface CustomAgentMessages {
68
+ custom: CustomMessage;
69
+ hookMessage: HookMessage;
70
+ branchSummary: BranchSummaryMessage;
71
+ compactionSummary: CompactionSummaryMessage;
72
+ }
73
+ }
74
+ export type ConvertToLlm = (messages: AgentMessage[]) => Message[];
75
+
76
+ function getPrunedToolResultContent(message: ToolResultMessage): (TextContent | ImageContent)[] {
77
+ if (message.prunedAt === undefined) {
78
+ return message.content;
79
+ }
80
+ const textBlocks = message.content.filter((content): content is TextContent => content.type === "text");
81
+ const text = textBlocks.map(block => block.text).join("") || "[Output truncated]";
82
+ return [{ type: "text", text }];
83
+ }
84
+
85
+ export function renderBranchSummaryContext(summary: string): string {
86
+ return prompt.render(BRANCH_SUMMARY_TEMPLATE, { summary });
87
+ }
88
+
89
+ export function renderCompactionSummaryContext(summary: string): string {
90
+ return prompt.render(COMPACTION_SUMMARY_TEMPLATE, { summary });
91
+ }
92
+
93
+ export function createBranchSummaryMessage(summary: string, fromId: string, timestamp: string): BranchSummaryMessage {
94
+ return {
95
+ role: "branchSummary",
96
+ summary,
97
+ fromId,
98
+ timestamp: new Date(timestamp).getTime(),
99
+ };
100
+ }
101
+
102
+ export function createCompactionSummaryMessage(
103
+ summary: string,
104
+ tokensBefore: number,
105
+ timestamp: string,
106
+ shortSummary?: string,
107
+ providerPayload?: ProviderPayload,
108
+ images?: ImageContent[],
109
+ blocks?: (TextContent | ImageContent)[],
110
+ warning?: string,
111
+ ): CompactionSummaryMessage {
112
+ const imageBlocks =
113
+ blocks?.filter((block): block is ImageContent => block.type === "image") ??
114
+ (images && images.length > 0 ? images : undefined);
115
+ return {
116
+ role: "compactionSummary",
117
+ summary,
118
+ shortSummary,
119
+ tokensBefore,
120
+ providerPayload,
121
+ blocks: blocks && blocks.length > 0 ? blocks : undefined,
122
+ images: imageBlocks && imageBlocks.length > 0 ? imageBlocks : undefined,
123
+ warning,
124
+ timestamp: new Date(timestamp).getTime(),
125
+ };
126
+ }
127
+
128
+ export function createCustomMessage(
129
+ customType: string,
130
+ content: string | (TextContent | ImageContent)[],
131
+ display: boolean,
132
+ details: unknown | undefined,
133
+ timestamp: string,
134
+ attribution?: MessageAttribution,
135
+ ): CustomMessage {
136
+ return {
137
+ role: "custom",
138
+ customType,
139
+ content,
140
+ display,
141
+ details,
142
+ attribution,
143
+ timestamp: new Date(timestamp).getTime(),
144
+ };
145
+ }
146
+
147
+ function isCoreCompactionMessage(message: AgentMessage): message is AgentMessage & CoreCompactionMessage {
148
+ return (
149
+ message.role === "custom" ||
150
+ message.role === "hookMessage" ||
151
+ message.role === "branchSummary" ||
152
+ message.role === "compactionSummary"
153
+ );
154
+ }
155
+
156
+ /**
157
+ * Transform a single core-domain agent message to its LLM form; `undefined`
158
+ * drops it from the provider request.
159
+ *
160
+ * Single source of truth for the core roles (user/developer/assistant/
161
+ * toolResult) and the compaction messages owned by this package. Embedders
162
+ * with their own app messages (e.g. the coding agent) handle their custom
163
+ * roles and delegate every core role here — duplicating these cases is how
164
+ * snapcompact frames once silently fell off the provider request.
165
+ */
166
+ export function convertMessageToLlm(message: AgentMessage): Message | undefined {
167
+ if (isCoreCompactionMessage(message)) {
168
+ switch (message.role) {
169
+ case "custom":
170
+ case "hookMessage": {
171
+ const content =
172
+ typeof message.content === "string"
173
+ ? [{ type: "text" as const, text: message.content }]
174
+ : message.content;
175
+ return {
176
+ role: "developer",
177
+ content,
178
+ attribution: message.attribution,
179
+ timestamp: message.timestamp,
180
+ };
181
+ }
182
+ case "branchSummary":
183
+ return {
184
+ role: "user",
185
+ content: [
186
+ {
187
+ type: "text" as const,
188
+ text: renderBranchSummaryContext(message.summary),
189
+ },
190
+ ],
191
+ attribution: "agent",
192
+ timestamp: message.timestamp,
193
+ };
194
+ case "compactionSummary":
195
+ return {
196
+ role: "user",
197
+ content:
198
+ message.blocks !== undefined
199
+ ? [{ type: "text" as const, text: message.summary }, ...message.blocks]
200
+ : [
201
+ {
202
+ type: "text" as const,
203
+ text: renderCompactionSummaryContext(message.summary),
204
+ },
205
+ ...(message.images ?? []),
206
+ ],
207
+ attribution: "agent",
208
+ providerPayload: message.providerPayload,
209
+ timestamp: message.timestamp,
210
+ };
211
+ }
212
+ }
213
+
214
+ switch (message.role) {
215
+ case "user":
216
+ return { ...message, attribution: message.attribution ?? "user" };
217
+ case "developer":
218
+ return { ...message, attribution: message.attribution ?? "agent" };
219
+ case "assistant":
220
+ return message;
221
+ case "toolResult":
222
+ return {
223
+ ...message,
224
+ content: getPrunedToolResultContent(message as ToolResultMessage),
225
+ attribution: message.attribution ?? "agent",
226
+ };
227
+ default:
228
+ return undefined;
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Default compaction-domain transformer.
234
+ *
235
+ * Embedders with their own app messages should pass a richer transformer through
236
+ * `SummaryOptions.convertToLlm`; this default intentionally preserves only the
237
+ * core LLM roles and the compaction messages owned by this package.
238
+ */
239
+ export function defaultConvertToLlm(messages: AgentMessage[]): Message[] {
240
+ return messages.map(convertMessageToLlm).filter(message => message !== undefined);
241
+ }