@gajae-code/agent-core 0.1.1
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 +482 -0
- package/README.md +473 -0
- package/dist/types/agent-loop.d.ts +55 -0
- package/dist/types/agent.d.ts +334 -0
- package/dist/types/append-only-context.d.ts +113 -0
- package/dist/types/compaction/branch-summarization.d.ts +94 -0
- package/dist/types/compaction/compaction.d.ts +166 -0
- package/dist/types/compaction/entries.d.ts +103 -0
- package/dist/types/compaction/errors.d.ts +26 -0
- package/dist/types/compaction/index.d.ts +11 -0
- package/dist/types/compaction/messages.d.ts +61 -0
- package/dist/types/compaction/openai.d.ts +58 -0
- package/dist/types/compaction/pruning.d.ts +18 -0
- package/dist/types/compaction/utils.d.ts +32 -0
- package/dist/types/compaction.d.ts +1 -0
- package/dist/types/harmony-leak.d.ts +99 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/proxy.d.ts +84 -0
- package/dist/types/run-collector.d.ts +196 -0
- package/dist/types/telemetry.d.ts +588 -0
- package/dist/types/thinking.d.ts +17 -0
- package/dist/types/types.d.ts +407 -0
- package/package.json +75 -0
- package/src/agent-loop.ts +1279 -0
- package/src/agent.ts +1399 -0
- package/src/append-only-context.ts +297 -0
- package/src/compaction/branch-summarization.ts +339 -0
- package/src/compaction/compaction.ts +1065 -0
- package/src/compaction/entries.ts +133 -0
- package/src/compaction/errors.ts +31 -0
- package/src/compaction/index.ts +12 -0
- package/src/compaction/messages.ts +212 -0
- package/src/compaction/openai.ts +552 -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/file-operations.md +10 -0
- package/src/compaction/prompts/handoff-document.md +49 -0
- package/src/compaction/prompts/summarization-system.md +3 -0
- package/src/compaction/pruning.ts +92 -0
- package/src/compaction/utils.ts +185 -0
- package/src/compaction.ts +1 -0
- package/src/harmony-leak.ts +427 -0
- package/src/index.ts +19 -0
- package/src/proxy.ts +326 -0
- package/src/run-collector.ts +631 -0
- package/src/telemetry.ts +2018 -0
- package/src/thinking.ts +19 -0
- package/src/types.ts +467 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
You MUST incorporate new messages above into the existing handoff summary in <previous-summary> tags, used by another LLM to resume task.
|
|
2
|
+
RULES:
|
|
3
|
+
- MUST preserve all information from previous summary
|
|
4
|
+
- MUST add new progress, decisions, and context from new messages
|
|
5
|
+
- MUST update Progress: move items from "In Progress" to "Done" when completed
|
|
6
|
+
- MUST update "Next Steps" based on what was accomplished
|
|
7
|
+
- MUST preserve exact file paths, function names, and error messages
|
|
8
|
+
- You MAY remove anything no longer relevant
|
|
9
|
+
|
|
10
|
+
IMPORTANT: If new messages end with unanswered question or request to user, you MUST add it to Critical Context (replacing any previous pending question if answered).
|
|
11
|
+
|
|
12
|
+
You MUST use this format (omit sections if not applicable):
|
|
13
|
+
|
|
14
|
+
## Goal
|
|
15
|
+
[Preserve existing goals; add new ones if task expanded]
|
|
16
|
+
|
|
17
|
+
## Constraints & Preferences
|
|
18
|
+
- [Preserve existing; add new ones discovered]
|
|
19
|
+
|
|
20
|
+
## Progress
|
|
21
|
+
|
|
22
|
+
### Done
|
|
23
|
+
- [x] [Include previously done and newly completed items]
|
|
24
|
+
|
|
25
|
+
### In Progress
|
|
26
|
+
- [ ] [Current work—update based on progress]
|
|
27
|
+
|
|
28
|
+
### Blocked
|
|
29
|
+
- [Current blockers—remove if resolved]
|
|
30
|
+
|
|
31
|
+
## Key Decisions
|
|
32
|
+
- **[Decision]**: [Brief rationale] (preserve all previous, add new)
|
|
33
|
+
|
|
34
|
+
## Next Steps
|
|
35
|
+
1. [Update based on current state]
|
|
36
|
+
|
|
37
|
+
## Critical Context
|
|
38
|
+
- [Preserve important context; add new if needed]
|
|
39
|
+
|
|
40
|
+
## Additional Notes
|
|
41
|
+
[Other important info not fitting above]
|
|
42
|
+
|
|
43
|
+
You MUST output only the structured summary; you NEVER include extra text.
|
|
44
|
+
|
|
45
|
+
Sections MUST be kept concise. You MUST preserve relevant tool outputs/command results. You MUST include repository state changes (branch, uncommitted changes) if mentioned.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<critical>
|
|
2
|
+
Write a handoff document for another instance of yourself.
|
|
3
|
+
The handoff MUST be sufficient for seamless continuation without access to this conversation.
|
|
4
|
+
Output ONLY the handoff document. No preamble, no commentary, no wrapper text.
|
|
5
|
+
</critical>
|
|
6
|
+
|
|
7
|
+
<instruction>
|
|
8
|
+
Capture exact technical state, not abstractions.
|
|
9
|
+
- File paths, symbol names, commands run
|
|
10
|
+
- Test results, observed failures
|
|
11
|
+
- Decisions made
|
|
12
|
+
- Partial work affecting the next step
|
|
13
|
+
</instruction>
|
|
14
|
+
|
|
15
|
+
<output>
|
|
16
|
+
Use exactly this structure:
|
|
17
|
+
|
|
18
|
+
## Goal
|
|
19
|
+
[What the user is trying to accomplish]
|
|
20
|
+
|
|
21
|
+
## Constraints & Preferences
|
|
22
|
+
- [Any constraints, preferences, or requirements mentioned]
|
|
23
|
+
|
|
24
|
+
## Progress
|
|
25
|
+
### Done
|
|
26
|
+
- [x] [Completed tasks with specifics]
|
|
27
|
+
|
|
28
|
+
### In Progress
|
|
29
|
+
- [ ] [Current work if any]
|
|
30
|
+
|
|
31
|
+
### Pending
|
|
32
|
+
- [ ] [Tasks mentioned but not started]
|
|
33
|
+
|
|
34
|
+
## Key Decisions
|
|
35
|
+
- **[Decision]**: [Rationale]
|
|
36
|
+
|
|
37
|
+
## Critical Context
|
|
38
|
+
- Code snippets, file paths, function/type names, error messages, data essential to continue
|
|
39
|
+
- Repository state if relevant
|
|
40
|
+
|
|
41
|
+
## Next Steps
|
|
42
|
+
1. [What should happen next]
|
|
43
|
+
</output>
|
|
44
|
+
|
|
45
|
+
{{#if additionalFocus}}
|
|
46
|
+
<instruction>
|
|
47
|
+
Additional focus: {{additionalFocus}}
|
|
48
|
+
</instruction>
|
|
49
|
+
{{/if}}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool output pruning utilities for compaction.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ToolResultMessage } from "@gajae-code/ai";
|
|
6
|
+
import type { AgentMessage } from "../types";
|
|
7
|
+
import { estimateTokens } from "./compaction";
|
|
8
|
+
import type { SessionEntry, SessionMessageEntry } from "./entries";
|
|
9
|
+
|
|
10
|
+
export interface PruneConfig {
|
|
11
|
+
/** Keep the most recent tool output tokens intact. */
|
|
12
|
+
protectTokens: number;
|
|
13
|
+
/** Only prune if total savings meets this threshold. */
|
|
14
|
+
minimumSavings: number;
|
|
15
|
+
/** Tool names that should never be pruned. */
|
|
16
|
+
protectedTools: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const DEFAULT_PRUNE_CONFIG: PruneConfig = {
|
|
20
|
+
protectTokens: 40_000,
|
|
21
|
+
minimumSavings: 20_000,
|
|
22
|
+
protectedTools: ["skill", "read"],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export interface PruneResult {
|
|
26
|
+
prunedCount: number;
|
|
27
|
+
tokensSaved: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createPrunedNotice(tokens: number): string {
|
|
31
|
+
return `[Output truncated - ${tokens} tokens]`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getToolResultMessage(entry: SessionEntry): ToolResultMessage | undefined {
|
|
35
|
+
if (entry.type !== "message") return undefined;
|
|
36
|
+
const message = entry.message as AgentMessage;
|
|
37
|
+
if (message.role !== "toolResult") return undefined;
|
|
38
|
+
return message as ToolResultMessage;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function estimatePrunedSavings(tokens: number): number {
|
|
42
|
+
const noticeTokens = Math.ceil(createPrunedNotice(tokens).length / 4);
|
|
43
|
+
return Math.max(0, tokens - noticeTokens);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function pruneToolOutputs(entries: SessionEntry[], config: PruneConfig = DEFAULT_PRUNE_CONFIG): PruneResult {
|
|
47
|
+
let accumulatedTokens = 0;
|
|
48
|
+
let tokensSaved = 0;
|
|
49
|
+
let prunedCount = 0;
|
|
50
|
+
|
|
51
|
+
const candidates: Array<{ entry: SessionMessageEntry; tokens: number }> = [];
|
|
52
|
+
|
|
53
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
54
|
+
const entry = entries[i];
|
|
55
|
+
const message = getToolResultMessage(entry);
|
|
56
|
+
if (!message) continue;
|
|
57
|
+
|
|
58
|
+
const tokens = estimateTokens(message as AgentMessage);
|
|
59
|
+
const isProtected = config.protectedTools.includes(message.toolName);
|
|
60
|
+
|
|
61
|
+
if (message.prunedAt !== undefined) {
|
|
62
|
+
accumulatedTokens += tokens;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (accumulatedTokens < config.protectTokens || isProtected) {
|
|
67
|
+
accumulatedTokens += tokens;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
candidates.push({ entry: entry as SessionMessageEntry, tokens });
|
|
72
|
+
accumulatedTokens += tokens;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (const candidate of candidates) {
|
|
76
|
+
tokensSaved += estimatePrunedSavings(candidate.tokens);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (tokensSaved < config.minimumSavings || candidates.length === 0) {
|
|
80
|
+
return { prunedCount: 0, tokensSaved: 0 };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const prunedAt = Date.now();
|
|
84
|
+
for (const candidate of candidates) {
|
|
85
|
+
const message = candidate.entry.message as ToolResultMessage;
|
|
86
|
+
message.content = [{ type: "text", text: createPrunedNotice(candidate.tokens) }];
|
|
87
|
+
message.prunedAt = prunedAt;
|
|
88
|
+
prunedCount++;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { prunedCount, tokensSaved };
|
|
92
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for compaction and branch summarization.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Message } from "@gajae-code/ai";
|
|
6
|
+
import { prompt } from "@gajae-code/utils";
|
|
7
|
+
import type { AgentMessage } from "../types";
|
|
8
|
+
import fileOperationsTemplate from "./prompts/file-operations.md" with { type: "text" };
|
|
9
|
+
import summarizationSystemPrompt from "./prompts/summarization-system.md" with { type: "text" };
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// File Operation Tracking
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
export interface FileOperations {
|
|
16
|
+
read: Set<string>;
|
|
17
|
+
written: Set<string>;
|
|
18
|
+
edited: Set<string>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createFileOps(): FileOperations {
|
|
22
|
+
return {
|
|
23
|
+
read: new Set(),
|
|
24
|
+
written: new Set(),
|
|
25
|
+
edited: new Set(),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Extract file operations from tool calls in an assistant message.
|
|
31
|
+
*/
|
|
32
|
+
export function extractFileOpsFromMessage(message: AgentMessage, fileOps: FileOperations): void {
|
|
33
|
+
if (message.role !== "assistant") return;
|
|
34
|
+
if (!("content" in message) || !Array.isArray(message.content)) return;
|
|
35
|
+
|
|
36
|
+
for (const block of message.content) {
|
|
37
|
+
if (typeof block !== "object" || block === null) continue;
|
|
38
|
+
if (!("type" in block) || block.type !== "toolCall") continue;
|
|
39
|
+
if (!("arguments" in block) || !("name" in block)) continue;
|
|
40
|
+
|
|
41
|
+
const args = block.arguments as Record<string, unknown> | undefined;
|
|
42
|
+
if (!args) continue;
|
|
43
|
+
|
|
44
|
+
const path = typeof args.path === "string" ? args.path : undefined;
|
|
45
|
+
if (!path) continue;
|
|
46
|
+
|
|
47
|
+
switch (block.name) {
|
|
48
|
+
case "read":
|
|
49
|
+
fileOps.read.add(path);
|
|
50
|
+
break;
|
|
51
|
+
case "write":
|
|
52
|
+
fileOps.written.add(path);
|
|
53
|
+
break;
|
|
54
|
+
case "edit":
|
|
55
|
+
fileOps.edited.add(path);
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Compute final file lists from file operations.
|
|
63
|
+
* Returns readFiles (files only read, not modified) and modifiedFiles.
|
|
64
|
+
*/
|
|
65
|
+
export function computeFileLists(fileOps: FileOperations): { readFiles: string[]; modifiedFiles: string[] } {
|
|
66
|
+
const modified = new Set([...fileOps.edited, ...fileOps.written]);
|
|
67
|
+
const readOnly = [...fileOps.read].filter(f => !modified.has(f)).sort();
|
|
68
|
+
const modifiedFiles = [...modified].sort();
|
|
69
|
+
return { readFiles: readOnly, modifiedFiles };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Format file operations as XML tags for summary.
|
|
74
|
+
*/
|
|
75
|
+
const FILE_OPERATION_SUMMARY_LIMIT = 20;
|
|
76
|
+
|
|
77
|
+
function truncateFileList(files: string[]): string[] {
|
|
78
|
+
if (files.length <= FILE_OPERATION_SUMMARY_LIMIT) return files;
|
|
79
|
+
const omitted = files.length - FILE_OPERATION_SUMMARY_LIMIT;
|
|
80
|
+
return [...files.slice(0, FILE_OPERATION_SUMMARY_LIMIT), `… (${omitted} more files omitted)`];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function stripFileOperationTags(summary: string): string {
|
|
84
|
+
const withoutReadFiles = summary.replace(/<read-files>[\s\S]*?<\/read-files>\s*/g, "");
|
|
85
|
+
const withoutModifiedFiles = withoutReadFiles.replace(/<modified-files>[\s\S]*?<\/modified-files>\s*/g, "");
|
|
86
|
+
return withoutModifiedFiles.trimEnd();
|
|
87
|
+
}
|
|
88
|
+
export function formatFileOperations(readFiles: string[], modifiedFiles: string[]): string {
|
|
89
|
+
if (readFiles.length === 0 && modifiedFiles.length === 0) return "";
|
|
90
|
+
return prompt.render(fileOperationsTemplate, {
|
|
91
|
+
readFiles: truncateFileList(readFiles),
|
|
92
|
+
modifiedFiles: truncateFileList(modifiedFiles),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function upsertFileOperations(summary: string, readFiles: string[], modifiedFiles: string[]): string {
|
|
97
|
+
const baseSummary = stripFileOperationTags(summary);
|
|
98
|
+
const fileOperations = formatFileOperations(readFiles, modifiedFiles);
|
|
99
|
+
if (!fileOperations) return baseSummary;
|
|
100
|
+
if (!baseSummary) return fileOperations;
|
|
101
|
+
return `${baseSummary}\n\n${fileOperations}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ============================================================================
|
|
105
|
+
// Message Serialization
|
|
106
|
+
// ============================================================================
|
|
107
|
+
|
|
108
|
+
/** Maximum characters for a tool result in serialized summaries. */
|
|
109
|
+
const TOOL_RESULT_MAX_CHARS = 2000;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Truncate text to a maximum character length for summarization.
|
|
113
|
+
* Keeps the beginning and appends a truncation marker.
|
|
114
|
+
*/
|
|
115
|
+
function truncateForSummary(text: string, maxChars: number): string {
|
|
116
|
+
if (text.length <= maxChars) return text;
|
|
117
|
+
const truncatedChars = text.length - maxChars;
|
|
118
|
+
return `${text.slice(0, maxChars)}\n\n[... ${truncatedChars} more characters truncated]`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Serialize LLM messages to text for summarization.
|
|
123
|
+
* This prevents the model from treating it as a conversation to continue.
|
|
124
|
+
* Call convertToLlm() first to handle custom message types.
|
|
125
|
+
*/
|
|
126
|
+
export function serializeConversation(messages: Message[]): string {
|
|
127
|
+
const parts: string[] = [];
|
|
128
|
+
|
|
129
|
+
for (const msg of messages) {
|
|
130
|
+
if (msg.role === "user") {
|
|
131
|
+
const content =
|
|
132
|
+
typeof msg.content === "string"
|
|
133
|
+
? msg.content
|
|
134
|
+
: msg.content
|
|
135
|
+
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
136
|
+
.map(c => c.text)
|
|
137
|
+
.join("");
|
|
138
|
+
if (content) parts.push(`[User]: ${content}`);
|
|
139
|
+
} else if (msg.role === "assistant") {
|
|
140
|
+
const textParts: string[] = [];
|
|
141
|
+
const thinkingParts: string[] = [];
|
|
142
|
+
const toolCalls: string[] = [];
|
|
143
|
+
|
|
144
|
+
for (const block of msg.content) {
|
|
145
|
+
if (block.type === "text") {
|
|
146
|
+
textParts.push(block.text);
|
|
147
|
+
} else if (block.type === "thinking") {
|
|
148
|
+
thinkingParts.push(block.thinking);
|
|
149
|
+
} else if (block.type === "toolCall") {
|
|
150
|
+
const args = block.arguments as Record<string, unknown>;
|
|
151
|
+
const argsStr = Object.entries(args)
|
|
152
|
+
.map(([k, v]) => `${k}=${JSON.stringify(v)}`)
|
|
153
|
+
.join(", ");
|
|
154
|
+
toolCalls.push(`${block.name}(${argsStr})`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (thinkingParts.length > 0) {
|
|
159
|
+
parts.push(`[Assistant thinking]: ${thinkingParts.join("\n")}`);
|
|
160
|
+
}
|
|
161
|
+
if (textParts.length > 0) {
|
|
162
|
+
parts.push(`[Assistant]: ${textParts.join("\n")}`);
|
|
163
|
+
}
|
|
164
|
+
if (toolCalls.length > 0) {
|
|
165
|
+
parts.push(`[Assistant tool calls]: ${toolCalls.join("; ")}`);
|
|
166
|
+
}
|
|
167
|
+
} else if (msg.role === "toolResult") {
|
|
168
|
+
const content = msg.content
|
|
169
|
+
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
170
|
+
.map(c => c.text)
|
|
171
|
+
.join("");
|
|
172
|
+
if (content) {
|
|
173
|
+
parts.push(`[Tool result]: ${truncateForSummary(content, TOOL_RESULT_MAX_CHARS)}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return parts.join("\n\n");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ============================================================================
|
|
182
|
+
// Summarization System Prompt
|
|
183
|
+
// ============================================================================
|
|
184
|
+
|
|
185
|
+
export const SUMMARIZATION_SYSTEM_PROMPT = prompt.render(summarizationSystemPrompt);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./compaction/index";
|