@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.
- package/CHANGELOG.md +1263 -0
- package/README.md +473 -0
- package/dist/types/agent-loop.d.ts +137 -0
- package/dist/types/agent.d.ts +465 -0
- package/dist/types/append-only-context.d.ts +131 -0
- package/dist/types/compaction/branch-summarization.d.ts +101 -0
- package/dist/types/compaction/compaction-v2-streaming.d.ts +85 -0
- package/dist/types/compaction/compaction.d.ts +307 -0
- package/dist/types/compaction/entries.d.ts +111 -0
- package/dist/types/compaction/errors.d.ts +38 -0
- package/dist/types/compaction/index.d.ts +13 -0
- package/dist/types/compaction/message-cache.d.ts +23 -0
- package/dist/types/compaction/messages.d.ts +79 -0
- package/dist/types/compaction/openai.d.ts +117 -0
- package/dist/types/compaction/pruning.d.ts +105 -0
- package/dist/types/compaction/shake.d.ts +97 -0
- package/dist/types/compaction/tool-protection.d.ts +19 -0
- package/dist/types/compaction/utils.d.ts +61 -0
- package/dist/types/compaction.d.ts +1 -0
- package/dist/types/index.d.ts +13 -0
- package/dist/types/pause.d.ts +46 -0
- package/dist/types/proxy.d.ts +91 -0
- package/dist/types/replay-policy.d.ts +5 -0
- package/dist/types/run-collector.d.ts +196 -0
- package/dist/types/telemetry.d.ts +608 -0
- package/dist/types/thinking.d.ts +18 -0
- package/dist/types/tokenizer.d.ts +2 -0
- package/dist/types/types.d.ts +781 -0
- package/dist/types/utils/yield.d.ts +71 -0
- package/package.json +79 -0
- package/src/agent-loop.ts +2926 -0
- package/src/agent.ts +1748 -0
- package/src/append-only-context.ts +348 -0
- package/src/compaction/branch-summarization.ts +369 -0
- package/src/compaction/compaction-v2-streaming.ts +846 -0
- package/src/compaction/compaction.ts +1733 -0
- package/src/compaction/entries.ts +141 -0
- package/src/compaction/errors.ts +47 -0
- package/src/compaction/index.ts +14 -0
- package/src/compaction/message-cache.ts +92 -0
- package/src/compaction/messages.ts +241 -0
- package/src/compaction/openai.ts +992 -0
- package/src/compaction/prompts/auto-handoff-threshold-focus.md +1 -0
- package/src/compaction/prompts/branch-summary-context.md +5 -0
- package/src/compaction/prompts/branch-summary-preamble.md +2 -0
- package/src/compaction/prompts/branch-summary.md +30 -0
- package/src/compaction/prompts/compaction-short-summary.md +9 -0
- package/src/compaction/prompts/compaction-summary-context.md +5 -0
- package/src/compaction/prompts/compaction-summary.md +38 -0
- package/src/compaction/prompts/compaction-turn-prefix.md +17 -0
- package/src/compaction/prompts/compaction-update-summary.md +45 -0
- package/src/compaction/prompts/context-window-truncated-output.md +1 -0
- package/src/compaction/prompts/file-operations.md +5 -0
- package/src/compaction/prompts/handoff-document.md +49 -0
- package/src/compaction/prompts/snapcompact-archive-context.md +3 -0
- package/src/compaction/prompts/summarization-system.md +3 -0
- package/src/compaction/pruning.ts +427 -0
- package/src/compaction/shake.ts +446 -0
- package/src/compaction/tool-protection.ts +65 -0
- package/src/compaction/utils.ts +343 -0
- package/src/compaction.ts +1 -0
- package/src/index.ts +26 -0
- package/src/pause.ts +107 -0
- package/src/proxy.ts +391 -0
- package/src/replay-policy.ts +13 -0
- package/src/run-collector.ts +631 -0
- package/src/telemetry.ts +2078 -0
- package/src/thinking.ts +20 -0
- package/src/tokenizer.ts +27 -0
- package/src/types.ts +875 -0
- 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
|
+
}
|