@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.
Files changed (55) hide show
  1. package/CHANGELOG.md +482 -0
  2. package/README.md +473 -0
  3. package/dist/types/agent-loop.d.ts +55 -0
  4. package/dist/types/agent.d.ts +334 -0
  5. package/dist/types/append-only-context.d.ts +113 -0
  6. package/dist/types/compaction/branch-summarization.d.ts +94 -0
  7. package/dist/types/compaction/compaction.d.ts +166 -0
  8. package/dist/types/compaction/entries.d.ts +103 -0
  9. package/dist/types/compaction/errors.d.ts +26 -0
  10. package/dist/types/compaction/index.d.ts +11 -0
  11. package/dist/types/compaction/messages.d.ts +61 -0
  12. package/dist/types/compaction/openai.d.ts +58 -0
  13. package/dist/types/compaction/pruning.d.ts +18 -0
  14. package/dist/types/compaction/utils.d.ts +32 -0
  15. package/dist/types/compaction.d.ts +1 -0
  16. package/dist/types/harmony-leak.d.ts +99 -0
  17. package/dist/types/index.d.ts +10 -0
  18. package/dist/types/proxy.d.ts +84 -0
  19. package/dist/types/run-collector.d.ts +196 -0
  20. package/dist/types/telemetry.d.ts +588 -0
  21. package/dist/types/thinking.d.ts +17 -0
  22. package/dist/types/types.d.ts +407 -0
  23. package/package.json +75 -0
  24. package/src/agent-loop.ts +1279 -0
  25. package/src/agent.ts +1399 -0
  26. package/src/append-only-context.ts +297 -0
  27. package/src/compaction/branch-summarization.ts +339 -0
  28. package/src/compaction/compaction.ts +1065 -0
  29. package/src/compaction/entries.ts +133 -0
  30. package/src/compaction/errors.ts +31 -0
  31. package/src/compaction/index.ts +12 -0
  32. package/src/compaction/messages.ts +212 -0
  33. package/src/compaction/openai.ts +552 -0
  34. package/src/compaction/prompts/auto-handoff-threshold-focus.md +1 -0
  35. package/src/compaction/prompts/branch-summary-context.md +5 -0
  36. package/src/compaction/prompts/branch-summary-preamble.md +2 -0
  37. package/src/compaction/prompts/branch-summary.md +30 -0
  38. package/src/compaction/prompts/compaction-short-summary.md +9 -0
  39. package/src/compaction/prompts/compaction-summary-context.md +5 -0
  40. package/src/compaction/prompts/compaction-summary.md +38 -0
  41. package/src/compaction/prompts/compaction-turn-prefix.md +17 -0
  42. package/src/compaction/prompts/compaction-update-summary.md +45 -0
  43. package/src/compaction/prompts/file-operations.md +10 -0
  44. package/src/compaction/prompts/handoff-document.md +49 -0
  45. package/src/compaction/prompts/summarization-system.md +3 -0
  46. package/src/compaction/pruning.ts +92 -0
  47. package/src/compaction/utils.ts +185 -0
  48. package/src/compaction.ts +1 -0
  49. package/src/harmony-leak.ts +427 -0
  50. package/src/index.ts +19 -0
  51. package/src/proxy.ts +326 -0
  52. package/src/run-collector.ts +631 -0
  53. package/src/telemetry.ts +2018 -0
  54. package/src/thinking.ts +19 -0
  55. 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,10 @@
1
+ {{#if readFiles.length}}
2
+ {{#xml "read-files"}}
3
+ {{join readFiles "\n"}}
4
+ {{/xml}}
5
+ {{/if}}
6
+ {{#if modifiedFiles.length}}
7
+ {{#xml "modified-files"}}
8
+ {{join modifiedFiles "\n"}}
9
+ {{/xml}}
10
+ {{/if}}
@@ -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,3 @@
1
+ Summarize conversations between users and AI coding assistants. Produce structured summaries in the exact specified format.
2
+
3
+ Do NOT continue the conversation. Do NOT respond to questions in the conversation. Output ONLY the structured summary.
@@ -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";