@bubblebrain-ai/bubble 0.0.46 → 0.0.47
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/dist/agent/child-runner.js +11 -0
- package/dist/agent/internal-reminder-sanitizer.d.ts +7 -0
- package/dist/agent/internal-reminder-sanitizer.js +13 -0
- package/dist/agent/subagent/router.d.ts +65 -0
- package/dist/agent/subagent/router.js +198 -0
- package/dist/agent/subagent/runtime.d.ts +203 -0
- package/dist/agent/subagent/runtime.js +789 -0
- package/dist/agent/subagent-control.d.ts +5 -0
- package/dist/agent/subagent-store.d.ts +18 -2
- package/dist/agent/subagent-store.js +32 -1
- package/dist/agent/tool-intent.d.ts +10 -0
- package/dist/agent/tool-intent.js +18 -0
- package/dist/agent/tool-visibility.d.ts +10 -0
- package/dist/agent/tool-visibility.js +6 -0
- package/dist/agent/workflow/runs.d.ts +72 -0
- package/dist/agent/workflow/runs.js +157 -0
- package/dist/agent.d.ts +78 -96
- package/dist/agent.js +266 -1072
- package/dist/context/compact-llm.js +14 -5
- package/dist/context/compact.d.ts +9 -0
- package/dist/context/compact.js +36 -10
- package/dist/context/compaction-files.d.ts +44 -0
- package/dist/context/compaction-files.js +148 -0
- package/dist/context/llm-compactor.js +24 -5
- package/dist/feishu/agent-host/run-driver.js +3 -0
- package/dist/feishu/serve.js +13 -9
- package/dist/goal/tools.js +12 -3
- package/dist/main.js +24 -19
- package/dist/model-catalog.js +36 -2
- package/dist/model-config.d.ts +2 -0
- package/dist/model-data.generated.d.ts +16 -0
- package/dist/model-data.generated.js +275 -0
- package/dist/model-pricing.d.ts +11 -7
- package/dist/model-pricing.js +0 -0
- package/dist/oauth/grok.d.ts +1 -1
- package/dist/oauth/grok.js +5 -2
- package/dist/oauth/storage.d.ts +9 -2
- package/dist/oauth/storage.js +16 -9
- package/dist/orchestrator/default-hooks.d.ts +20 -0
- package/dist/orchestrator/default-hooks.js +47 -295
- package/dist/orchestrator/hooks.d.ts +6 -39
- package/dist/permission/mode.js +1 -1
- package/dist/prompt/reminders.d.ts +9 -35
- package/dist/prompt/reminders.js +30 -136
- package/dist/prompt/task-reminders.d.ts +0 -31
- package/dist/prompt/task-reminders.js +0 -87
- package/dist/provider-ai-sdk.d.ts +2 -0
- package/dist/provider-ai-sdk.js +1 -0
- package/dist/provider-anthropic.d.ts +2 -0
- package/dist/provider-anthropic.js +3 -1
- package/dist/provider-ark-responses.d.ts +2 -0
- package/dist/provider-ark-responses.js +1 -0
- package/dist/provider-registry.d.ts +13 -0
- package/dist/provider-registry.js +83 -0
- package/dist/provider-transform.js +37 -1
- package/dist/provider.d.ts +7 -0
- package/dist/provider.js +13 -1
- package/dist/sdk/index.d.ts +152 -0
- package/dist/sdk/index.js +400 -0
- package/dist/session.js +9 -1
- package/dist/slash-commands/commands.js +33 -19
- package/dist/tools/exit-plan-mode.js +8 -0
- package/dist/tui/detect-theme.d.ts +1 -0
- package/dist/tui/detect-theme.js +11 -5
- package/dist/tui-ink/app.d.ts +2 -6
- package/dist/tui-ink/app.js +11 -18
- package/dist/tui-ink/display-history.d.ts +0 -2
- package/dist/tui-ink/display-history.js +0 -52
- package/dist/tui-ink/footer.js +2 -2
- package/dist/tui-ink/message-list.js +62 -23
- package/dist/tui-ink/run.js +10 -5
- package/dist/tui-ink/sync-output.d.ts +17 -0
- package/dist/tui-ink/sync-output.js +35 -0
- package/dist/tui-ink/theme.d.ts +0 -1
- package/dist/tui-ink/theme.js +0 -2
- package/dist/tui-ink/trace-groups.js +1 -1
- package/dist/types.d.ts +21 -1
- package/package.json +16 -2
- package/dist/agent/discovery-barrier.d.ts +0 -21
- package/dist/agent/discovery-barrier.js +0 -173
- package/dist/agent/evidence-tracker.d.ts +0 -15
- package/dist/agent/evidence-tracker.js +0 -93
- package/dist/agent/execution-governor.d.ts +0 -36
- package/dist/agent/execution-governor.js +0 -218
- package/dist/agent/task-classifier.d.ts +0 -3
- package/dist/agent/task-classifier.js +0 -114
- package/dist/agent/task-size.d.ts +0 -9
- package/dist/agent/task-size.js +0 -33
- package/dist/agent/tool-arbiter.d.ts +0 -7
- package/dist/agent/tool-arbiter.js +0 -33
- package/dist/orchestrator/workflow.d.ts +0 -10
- package/dist/orchestrator/workflow.js +0 -22
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* message. Falls back to the heuristic `compactMessages` if the LLM call
|
|
7
7
|
* fails.
|
|
8
8
|
*/
|
|
9
|
-
import { buildCompactionSummaryMessage, clonePinnedUserMessage, compactMessages as compactMessagesHeuristic, findFirstRealUserIndex, isCompactionSummaryMessage, isRealUserMessage, splitLeadingContext, } from "./compact.js";
|
|
9
|
+
import { buildCompactionSummaryMessage, clonePinnedUserMessage, collectCompactionFileOps, compactMessages as compactMessagesHeuristic, findFirstRealUserIndex, isCompactionSummaryMessage, isRealUserMessage, messageText, splitLeadingContext, } from "./compact.js";
|
|
10
|
+
import { sanitizeInternalReminderBlocks } from "../agent/internal-reminder-sanitizer.js";
|
|
11
|
+
import { appendFileBlocks, stripFileBlocks } from "./compaction-files.js";
|
|
10
12
|
export const COMPACT_SYSTEM_PROMPT = `You are a conversation summarizer. Your job is to produce a structured
|
|
11
13
|
summary of an earlier portion of a software-engineering assistant's
|
|
12
14
|
conversation so that the assistant can continue working without the full
|
|
@@ -65,13 +67,16 @@ export async function compactMessagesWithLLM(messages, options) {
|
|
|
65
67
|
// pin the heuristic and llm-compactor paths already had).
|
|
66
68
|
const pinnedIndex = findFirstRealUserIndex(oldMessages);
|
|
67
69
|
const pinnedMessage = pinnedIndex >= 0 ? oldMessages[pinnedIndex] : undefined;
|
|
70
|
+
// File blocks are stripped from prior summaries before summarization: the
|
|
71
|
+
// deterministic merge below owns the file lists end to end.
|
|
68
72
|
const summarizable = [
|
|
69
73
|
...priorSummaries.map((m) => ({
|
|
70
74
|
role: "user",
|
|
71
|
-
content: `[Prior compaction summary]\n${
|
|
75
|
+
content: `[Prior compaction summary]\n${stripFileBlocks(messageText(m))}`,
|
|
72
76
|
})),
|
|
73
77
|
...oldMessages.filter((_, index) => index !== pinnedIndex),
|
|
74
78
|
];
|
|
79
|
+
const fileOps = collectCompactionFileOps(oldMessages, priorSummaries);
|
|
75
80
|
let summary;
|
|
76
81
|
try {
|
|
77
82
|
summary = await generateSummary(summarizable, options);
|
|
@@ -79,16 +84,20 @@ export async function compactMessagesWithLLM(messages, options) {
|
|
|
79
84
|
catch {
|
|
80
85
|
return compactMessagesHeuristic(messages, { keepRecentTurns, maxSummaryItems: options.maxSummaryItems });
|
|
81
86
|
}
|
|
82
|
-
|
|
87
|
+
// Models quote their input, and the transcript can contain projected
|
|
88
|
+
// reminder blocks; this summary is persisted, so scrub markup first.
|
|
89
|
+
summary = sanitizeInternalReminderBlocks(summary).trim();
|
|
90
|
+
if (!summary) {
|
|
83
91
|
return compactMessagesHeuristic(messages, { keepRecentTurns, maxSummaryItems: options.maxSummaryItems });
|
|
84
92
|
}
|
|
93
|
+
const summaryWithFiles = appendFileBlocks(summary, fileOps);
|
|
85
94
|
return {
|
|
86
95
|
compacted: true,
|
|
87
|
-
summary,
|
|
96
|
+
summary: summaryWithFiles,
|
|
88
97
|
messages: [
|
|
89
98
|
...leading,
|
|
90
99
|
...(pinnedMessage ? [clonePinnedUserMessage(pinnedMessage)] : []),
|
|
91
|
-
buildCompactionSummaryMessage(
|
|
100
|
+
buildCompactionSummaryMessage(summaryWithFiles),
|
|
92
101
|
...keptMessages,
|
|
93
102
|
],
|
|
94
103
|
droppedEntries: oldMessages.length,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Message } from "../types.js";
|
|
2
2
|
import type { SessionLogEntry } from "../session-types.js";
|
|
3
|
+
import { type CompactionFileOps } from "./compaction-files.js";
|
|
3
4
|
/**
|
|
4
5
|
* Compaction must never summarize away what the user actually asked for:
|
|
5
6
|
* requirements that appear late in a long instruction ("commit when done",
|
|
@@ -10,6 +11,7 @@ import type { SessionLogEntry } from "../session-types.js";
|
|
|
10
11
|
export declare const PINNED_INSTRUCTION_MAX_CHARS = 8192;
|
|
11
12
|
export declare const COMPACTION_SUMMARY_PREFIX = "Previous conversation summary:";
|
|
12
13
|
export declare const SUBTURN_SUMMARY_PREFIX = "Earlier in this turn (compacted to free context):";
|
|
14
|
+
export declare function messageText(message: Message): string;
|
|
13
15
|
/**
|
|
14
16
|
* Identity check for compaction summaries across every form they can take:
|
|
15
17
|
* the first-class meta form, the legacy raw-system form, and the projected
|
|
@@ -68,6 +70,13 @@ export type SessionCompactionPlan = {
|
|
|
68
70
|
keptEntries: SessionLogEntry[];
|
|
69
71
|
};
|
|
70
72
|
export declare function planSessionCompaction(entries: SessionLogEntry[], options?: CompactOptions): SessionCompactionPlan;
|
|
73
|
+
/**
|
|
74
|
+
* Cumulative file ops for a compaction: the blocks prior summaries carried,
|
|
75
|
+
* unioned with the ops found in the messages being evicted now. Every
|
|
76
|
+
* compaction path — LLM, heuristic, sub-turn, session-entry — must thread
|
|
77
|
+
* this through, or the "cumulative" record resets on that path.
|
|
78
|
+
*/
|
|
79
|
+
export declare function collectCompactionFileOps(evicted: Message[], priorSummaries: Message[]): CompactionFileOps;
|
|
71
80
|
/**
|
|
72
81
|
* Assemble the post-compaction entry list from a plan and a (possibly
|
|
73
82
|
* LLM-generated) summary string. The summary entry is keyed off the full
|
package/dist/context/compact.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { isInternalBlockContent } from "../agent/internal-reminder-sanitizer.js";
|
|
1
|
+
import { isInternalBlockContent, sanitizeInternalReminderBlocks } from "../agent/internal-reminder-sanitizer.js";
|
|
2
|
+
import { appendFileBlocks, extractFileOps, mergeFileOps, parseFileBlocks, stripFileBlocks, } from "./compaction-files.js";
|
|
2
3
|
/**
|
|
3
4
|
* Compaction must never summarize away what the user actually asked for:
|
|
4
5
|
* requirements that appear late in a long instruction ("commit when done",
|
|
@@ -23,7 +24,7 @@ const PROJECTED_SUMMARY_PREFIXES = [
|
|
|
23
24
|
`<bubble_internal_context kind="runtime-system">\n${COMPACTION_SUMMARY_PREFIX}`,
|
|
24
25
|
`<bubble_internal_context kind="runtime-system">\n${SUBTURN_SUMMARY_PREFIX}`,
|
|
25
26
|
];
|
|
26
|
-
function messageText(message) {
|
|
27
|
+
export function messageText(message) {
|
|
27
28
|
if (typeof message.content === "string")
|
|
28
29
|
return message.content;
|
|
29
30
|
if (Array.isArray(message.content)) {
|
|
@@ -139,6 +140,15 @@ export function planSessionCompaction(entries, options = {}) {
|
|
|
139
140
|
keptEntries: activeEntries.slice(keepStartIndex),
|
|
140
141
|
};
|
|
141
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Cumulative file ops for a compaction: the blocks prior summaries carried,
|
|
145
|
+
* unioned with the ops found in the messages being evicted now. Every
|
|
146
|
+
* compaction path — LLM, heuristic, sub-turn, session-entry — must thread
|
|
147
|
+
* this through, or the "cumulative" record resets on that path.
|
|
148
|
+
*/
|
|
149
|
+
export function collectCompactionFileOps(evicted, priorSummaries) {
|
|
150
|
+
return mergeFileOps(...priorSummaries.map((message) => parseFileBlocks(messageText(message))), extractFileOps(evicted));
|
|
151
|
+
}
|
|
142
152
|
/**
|
|
143
153
|
* Assemble the post-compaction entry list from a plan and a (possibly
|
|
144
154
|
* LLM-generated) summary string. The summary entry is keyed off the full
|
|
@@ -148,7 +158,9 @@ export function buildCompactedEntries(entries, plan, summary) {
|
|
|
148
158
|
const summaryEntry = {
|
|
149
159
|
id: nextSummaryId(entries),
|
|
150
160
|
type: "summary",
|
|
151
|
-
|
|
161
|
+
// Sink-level backstop: whatever producer built this string (LLM or
|
|
162
|
+
// heuristic), internal markup never lands in the session file.
|
|
163
|
+
summary: sanitizeInternalReminderBlocks(summary),
|
|
152
164
|
timestamp: Date.now(),
|
|
153
165
|
};
|
|
154
166
|
return [...plan.metadataEntries, summaryEntry, ...plan.keptEntries];
|
|
@@ -167,10 +179,12 @@ export function compactSessionEntries(entries, options = {}) {
|
|
|
167
179
|
if (!summary) {
|
|
168
180
|
return { compacted: false };
|
|
169
181
|
}
|
|
182
|
+
const oldMessages = entriesToMessages(plan.oldEntries);
|
|
183
|
+
const summaryWithFiles = appendFileBlocks(summary, collectCompactionFileOps(oldMessages.filter((message) => !isCompactionSummaryMessage(message)), oldMessages.filter(isCompactionSummaryMessage)));
|
|
170
184
|
return {
|
|
171
185
|
compacted: true,
|
|
172
|
-
summary,
|
|
173
|
-
entries: buildCompactedEntries(entries, plan,
|
|
186
|
+
summary: summaryWithFiles,
|
|
187
|
+
entries: buildCompactedEntries(entries, plan, summaryWithFiles),
|
|
174
188
|
droppedEntries: plan.oldEntries.length,
|
|
175
189
|
};
|
|
176
190
|
}
|
|
@@ -182,6 +196,7 @@ export function compactMessages(messages, options = {}) {
|
|
|
182
196
|
// template-grade (goal truncated to 140 chars, constant next-steps line);
|
|
183
197
|
// the pinned original instruction is the durable record, not old summaries.
|
|
184
198
|
const { leading, body } = splitLeadingContext(messages);
|
|
199
|
+
const priorSummaries = body.filter(isCompactionSummaryMessage);
|
|
185
200
|
const bodyMessages = body.filter((message) => !isCompactionSummaryMessage(message));
|
|
186
201
|
// Turn boundaries anchor on REAL user messages only. Projected reminders
|
|
187
202
|
// and summary envelopes used to inflate the turn count, shrinking the keep
|
|
@@ -211,15 +226,16 @@ export function compactMessages(messages, options = {}) {
|
|
|
211
226
|
if (!summary) {
|
|
212
227
|
return { compacted: false };
|
|
213
228
|
}
|
|
229
|
+
const summaryWithFiles = appendFileBlocks(summary, collectCompactionFileOps(oldMessages, priorSummaries));
|
|
214
230
|
const compactedMessages = [
|
|
215
231
|
...leading.map((message) => cloneMessage(message)),
|
|
216
232
|
...(pinnedMessage ? [clonePinnedUserMessage(pinnedMessage)] : []),
|
|
217
|
-
buildCompactionSummaryMessage(
|
|
233
|
+
buildCompactionSummaryMessage(summaryWithFiles),
|
|
218
234
|
...keptMessages.map((message) => cloneMessage(message)),
|
|
219
235
|
];
|
|
220
236
|
return {
|
|
221
237
|
compacted: true,
|
|
222
|
-
summary,
|
|
238
|
+
summary: summaryWithFiles,
|
|
223
239
|
messages: compactedMessages,
|
|
224
240
|
droppedEntries: summaryInput.length,
|
|
225
241
|
};
|
|
@@ -271,6 +287,9 @@ export function compactCurrentTurnToolGroups(messages, options = {}) {
|
|
|
271
287
|
if (!freshSummary)
|
|
272
288
|
return { compacted: false };
|
|
273
289
|
const summary = mergeSubturnSummaryLines(priorSubturnSummaries, freshSummary, maxSummaryItems);
|
|
290
|
+
// Evicted groups' file ops would otherwise vanish before any multi-turn
|
|
291
|
+
// compactor runs; blocks on the sub-turn summary get merged upward later.
|
|
292
|
+
const summaryWithFiles = appendFileBlocks(summary, collectCompactionFileOps(evictable.flatMap((g) => [g.assistant, ...g.toolResults]), priorSubturnSummaries));
|
|
274
293
|
const survivingGroups = groups.filter((g) => !evictable.includes(g));
|
|
275
294
|
const flatSurvivors = [];
|
|
276
295
|
for (const g of survivingGroups) {
|
|
@@ -284,7 +303,7 @@ export function compactCurrentTurnToolGroups(messages, options = {}) {
|
|
|
284
303
|
{
|
|
285
304
|
role: "meta",
|
|
286
305
|
kind: "subturn-compaction-summary",
|
|
287
|
-
content: `${SUBTURN_SUMMARY_PREFIX}\n${
|
|
306
|
+
content: `${SUBTURN_SUMMARY_PREFIX}\n${summaryWithFiles}`,
|
|
288
307
|
},
|
|
289
308
|
...flatSurvivors,
|
|
290
309
|
];
|
|
@@ -313,7 +332,9 @@ function mergeSubturnSummaryLines(priorSummaries, freshSummary, maxItems) {
|
|
|
313
332
|
merged.push(line);
|
|
314
333
|
};
|
|
315
334
|
for (const prior of priorSummaries) {
|
|
316
|
-
|
|
335
|
+
// File blocks are merged structurally by collectCompactionFileOps, not as
|
|
336
|
+
// prose lines — strip them so paths don't leak into the line dedupe.
|
|
337
|
+
for (const line of stripFileBlocks(messageText(prior)).split("\n")) {
|
|
317
338
|
if (line.startsWith(SUBTURN_SUMMARY_PREFIX) || line.startsWith("<bubble_internal_"))
|
|
318
339
|
continue;
|
|
319
340
|
push(line);
|
|
@@ -397,7 +418,12 @@ function buildMessageSummary(messages, maxSummaryItems) {
|
|
|
397
418
|
if (messages.length === 0) {
|
|
398
419
|
return "";
|
|
399
420
|
}
|
|
400
|
-
|
|
421
|
+
// Internal blocks (goal kicks, task wakes, projected reminders) must not
|
|
422
|
+
// become the Goal/Progress lines: 140-char truncation would turn them into
|
|
423
|
+
// markup FRAGMENTS, which defeat the anchored display filters downstream.
|
|
424
|
+
const userMessages = messages.filter((message) => message.role === "user"
|
|
425
|
+
&& typeof message.content === "string"
|
|
426
|
+
&& !isInternalBlockContent(message.content));
|
|
401
427
|
const assistantMessages = messages.filter((message) => message.role === "assistant");
|
|
402
428
|
const toolCalls = collectToolCalls(messages);
|
|
403
429
|
const relevantFiles = collectRelevantFiles(toolCalls);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cumulative file-operation tracking across compactions.
|
|
3
|
+
*
|
|
4
|
+
* The LLM summary handles semantics (progress, decisions, next steps); which
|
|
5
|
+
* files were touched is an enumerable fact the model reliably drops after a
|
|
6
|
+
* couple of rolling summaries. So it never goes through the model: at each
|
|
7
|
+
* compaction, deterministic code extracts file ops from the evicted messages,
|
|
8
|
+
* unions them with the lists the previous summary carried, and appends the
|
|
9
|
+
* result as structured blocks after the summary text.
|
|
10
|
+
*
|
|
11
|
+
* Robustness rules (each earned by a review finding):
|
|
12
|
+
* - appendFileBlocks sanitizes its input first, so a summary can never carry
|
|
13
|
+
* two generations of blocks — or model-echoed tags — at once.
|
|
14
|
+
* - Only tool calls with a successful result count: a rejected or failed edit
|
|
15
|
+
* must not report the file as modified.
|
|
16
|
+
* - Lists are capped; modified entries are the ones worth keeping complete.
|
|
17
|
+
* - Serialization is sorted and deduped so identical sets are identical bytes.
|
|
18
|
+
*/
|
|
19
|
+
import type { Message } from "../types.js";
|
|
20
|
+
export interface CompactionFileOps {
|
|
21
|
+
read: string[];
|
|
22
|
+
modified: string[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* File ops mentioned by tool calls in the given (about-to-be-evicted)
|
|
26
|
+
* messages. A call only counts when its tool result is present and not an
|
|
27
|
+
* error — a rejected edit or failed read must not enter the record.
|
|
28
|
+
*/
|
|
29
|
+
export declare function extractFileOps(messages: Message[]): CompactionFileOps;
|
|
30
|
+
/** Parse every block a previous compaction summary carried (empty when absent). */
|
|
31
|
+
export declare function parseFileBlocks(text: string): CompactionFileOps;
|
|
32
|
+
/**
|
|
33
|
+
* Remove all blocks AND stray tags, e.g. before feeding a prior summary back
|
|
34
|
+
* to the model. Whitespace is only reflowed when something was removed.
|
|
35
|
+
*/
|
|
36
|
+
export declare function stripFileBlocks(text: string): string;
|
|
37
|
+
/** Union, deduped and sorted; a file that was ever modified never lists as read-only. */
|
|
38
|
+
export declare function mergeFileOps(...ops: CompactionFileOps[]): CompactionFileOps;
|
|
39
|
+
/**
|
|
40
|
+
* Append the blocks to a summary text. Sanitizes the summary first so the
|
|
41
|
+
* output always contains exactly one generation of blocks, even when the
|
|
42
|
+
* model echoed tags into its own prose. No-op when both lists are empty.
|
|
43
|
+
*/
|
|
44
|
+
export declare function appendFileBlocks(summary: string, ops: CompactionFileOps): string;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cumulative file-operation tracking across compactions.
|
|
3
|
+
*
|
|
4
|
+
* The LLM summary handles semantics (progress, decisions, next steps); which
|
|
5
|
+
* files were touched is an enumerable fact the model reliably drops after a
|
|
6
|
+
* couple of rolling summaries. So it never goes through the model: at each
|
|
7
|
+
* compaction, deterministic code extracts file ops from the evicted messages,
|
|
8
|
+
* unions them with the lists the previous summary carried, and appends the
|
|
9
|
+
* result as structured blocks after the summary text.
|
|
10
|
+
*
|
|
11
|
+
* Robustness rules (each earned by a review finding):
|
|
12
|
+
* - appendFileBlocks sanitizes its input first, so a summary can never carry
|
|
13
|
+
* two generations of blocks — or model-echoed tags — at once.
|
|
14
|
+
* - Only tool calls with a successful result count: a rejected or failed edit
|
|
15
|
+
* must not report the file as modified.
|
|
16
|
+
* - Lists are capped; modified entries are the ones worth keeping complete.
|
|
17
|
+
* - Serialization is sorted and deduped so identical sets are identical bytes.
|
|
18
|
+
*/
|
|
19
|
+
const READ_TOOLS = new Set(["read"]);
|
|
20
|
+
const MODIFY_TOOLS = new Set(["edit", "write"]);
|
|
21
|
+
/** Cap per list. Modified is the higher-value list; both stay bounded. */
|
|
22
|
+
const MAX_FILES_PER_LIST = 200;
|
|
23
|
+
const MORE_MARKER = /^\(\+\d+ more\)$/;
|
|
24
|
+
const READ_BLOCKS = /<read-files>[\s\S]*?<\/read-files>/g;
|
|
25
|
+
const MODIFIED_BLOCKS = /<modified-files>[\s\S]*?<\/modified-files>/g;
|
|
26
|
+
/** Stray (unpaired / model-echoed) tags left after full blocks are removed. */
|
|
27
|
+
const STRAY_TAGS = /<\/?(?:read|modified)-files>/g;
|
|
28
|
+
/**
|
|
29
|
+
* File ops mentioned by tool calls in the given (about-to-be-evicted)
|
|
30
|
+
* messages. A call only counts when its tool result is present and not an
|
|
31
|
+
* error — a rejected edit or failed read must not enter the record.
|
|
32
|
+
*/
|
|
33
|
+
export function extractFileOps(messages) {
|
|
34
|
+
const succeeded = new Set();
|
|
35
|
+
for (const message of messages) {
|
|
36
|
+
if (message.role === "tool" && !message.isError)
|
|
37
|
+
succeeded.add(message.toolCallId);
|
|
38
|
+
}
|
|
39
|
+
const read = new Set();
|
|
40
|
+
const modified = new Set();
|
|
41
|
+
for (const message of messages) {
|
|
42
|
+
if (message.role !== "assistant" || !message.toolCalls)
|
|
43
|
+
continue;
|
|
44
|
+
for (const toolCall of message.toolCalls) {
|
|
45
|
+
if (!succeeded.has(toolCall.id))
|
|
46
|
+
continue;
|
|
47
|
+
const target = READ_TOOLS.has(toolCall.name)
|
|
48
|
+
? read
|
|
49
|
+
: MODIFY_TOOLS.has(toolCall.name)
|
|
50
|
+
? modified
|
|
51
|
+
: undefined;
|
|
52
|
+
if (!target)
|
|
53
|
+
continue;
|
|
54
|
+
const path = pathFromArguments(toolCall.arguments);
|
|
55
|
+
if (path)
|
|
56
|
+
target.add(path);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { read: [...read], modified: [...modified] };
|
|
60
|
+
}
|
|
61
|
+
/** Parse every block a previous compaction summary carried (empty when absent). */
|
|
62
|
+
export function parseFileBlocks(text) {
|
|
63
|
+
return {
|
|
64
|
+
read: parseBlocks(text, READ_BLOCKS, "<read-files>", "</read-files>"),
|
|
65
|
+
modified: parseBlocks(text, MODIFIED_BLOCKS, "<modified-files>", "</modified-files>"),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Remove all blocks AND stray tags, e.g. before feeding a prior summary back
|
|
70
|
+
* to the model. Whitespace is only reflowed when something was removed.
|
|
71
|
+
*/
|
|
72
|
+
export function stripFileBlocks(text) {
|
|
73
|
+
const without = text.replace(READ_BLOCKS, "").replace(MODIFIED_BLOCKS, "").replace(STRAY_TAGS, "");
|
|
74
|
+
if (without === text)
|
|
75
|
+
return text;
|
|
76
|
+
return without.replace(/\n{3,}/g, "\n\n").trim();
|
|
77
|
+
}
|
|
78
|
+
/** Union, deduped and sorted; a file that was ever modified never lists as read-only. */
|
|
79
|
+
export function mergeFileOps(...ops) {
|
|
80
|
+
const read = new Set();
|
|
81
|
+
const modified = new Set();
|
|
82
|
+
for (const op of ops) {
|
|
83
|
+
for (const path of op.modified)
|
|
84
|
+
modified.add(normalizePath(path));
|
|
85
|
+
for (const path of op.read)
|
|
86
|
+
read.add(normalizePath(path));
|
|
87
|
+
}
|
|
88
|
+
for (const path of modified)
|
|
89
|
+
read.delete(path);
|
|
90
|
+
return { read: [...read].sort(), modified: [...modified].sort() };
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Append the blocks to a summary text. Sanitizes the summary first so the
|
|
94
|
+
* output always contains exactly one generation of blocks, even when the
|
|
95
|
+
* model echoed tags into its own prose. No-op when both lists are empty.
|
|
96
|
+
*/
|
|
97
|
+
export function appendFileBlocks(summary, ops) {
|
|
98
|
+
const clean = stripFileBlocks(summary);
|
|
99
|
+
const blocks = [];
|
|
100
|
+
if (ops.read.length > 0)
|
|
101
|
+
blocks.push(`<read-files>\n${capped(ops.read)}\n</read-files>`);
|
|
102
|
+
if (ops.modified.length > 0)
|
|
103
|
+
blocks.push(`<modified-files>\n${capped(ops.modified)}\n</modified-files>`);
|
|
104
|
+
if (blocks.length === 0)
|
|
105
|
+
return clean;
|
|
106
|
+
return `${clean.trimEnd()}\n\n${blocks.join("\n")}`;
|
|
107
|
+
}
|
|
108
|
+
function capped(paths) {
|
|
109
|
+
if (paths.length <= MAX_FILES_PER_LIST)
|
|
110
|
+
return paths.join("\n");
|
|
111
|
+
const kept = paths.slice(0, MAX_FILES_PER_LIST);
|
|
112
|
+
return `${kept.join("\n")}\n(+${paths.length - MAX_FILES_PER_LIST} more)`;
|
|
113
|
+
}
|
|
114
|
+
function parseBlocks(text, pattern, open, close) {
|
|
115
|
+
const out = new Set();
|
|
116
|
+
for (const match of text.match(pattern) ?? []) {
|
|
117
|
+
const inner = match.slice(open.length, match.length - close.length);
|
|
118
|
+
for (const line of inner.split("\n")) {
|
|
119
|
+
const trimmed = line.trim();
|
|
120
|
+
if (trimmed && !MORE_MARKER.test(trimmed))
|
|
121
|
+
out.add(trimmed);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return [...out];
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Best-effort normalization so "./src/a.ts" and "src/a.ts" dedupe. Relative vs
|
|
128
|
+
* absolute spellings of the same file can still coexist — resolving that needs
|
|
129
|
+
* a cwd this layer doesn't have; models are consistent enough within a session
|
|
130
|
+
* that the union stays useful.
|
|
131
|
+
*/
|
|
132
|
+
function normalizePath(path) {
|
|
133
|
+
let out = path.trim().replace(/\/{2,}/g, "/");
|
|
134
|
+
while (out.startsWith("./"))
|
|
135
|
+
out = out.slice(2);
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
function pathFromArguments(rawArguments) {
|
|
139
|
+
if (!rawArguments)
|
|
140
|
+
return undefined;
|
|
141
|
+
try {
|
|
142
|
+
const parsed = JSON.parse(rawArguments);
|
|
143
|
+
return typeof parsed.path === "string" && parsed.path.trim() ? parsed.path.trim() : undefined;
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
//
|
|
10
10
|
// Failure modes are explicit: returns { compacted: false, reason } so the
|
|
11
11
|
// caller can fall back to algorithmic compaction without an exception.
|
|
12
|
+
import { sanitizeInternalReminderBlocks } from "../agent/internal-reminder-sanitizer.js";
|
|
12
13
|
import { estimateContextTokens } from "./budget.js";
|
|
13
|
-
import {
|
|
14
|
+
import { appendFileBlocks, stripFileBlocks } from "./compaction-files.js";
|
|
15
|
+
import { collectCompactionFileOps, messageText, buildCompactionSummaryMessage, clonePinnedUserMessage, findFirstRealUserIndex, isCompactionSummaryMessage, isRealUserMessage, splitLeadingContext, } from "./compact.js";
|
|
14
16
|
export const LLM_COMPACTION_PROMPT = `You are performing a CONTEXT CHECKPOINT COMPACTION. Create a handoff summary for another LLM that will resume the task.
|
|
15
17
|
|
|
16
18
|
Include:
|
|
@@ -81,15 +83,21 @@ export async function compactWithLLM(messages, options) {
|
|
|
81
83
|
const keptGroups = groups.slice(groups.length - keptGroupCount);
|
|
82
84
|
// What we'll send to the model to summarize: prior summaries (semantic
|
|
83
85
|
// rolling), prior turns, and the older groups in the current turn. The
|
|
84
|
-
// pinned first instruction is excluded — it survives verbatim.
|
|
86
|
+
// pinned first instruction is excluded — it survives verbatim. File blocks
|
|
87
|
+
// are stripped from prior summaries here: the deterministic merge below owns
|
|
88
|
+
// the file lists, and feeding them to the model only invites a lossy echo.
|
|
85
89
|
const toSummarize = [
|
|
86
90
|
...priorSummaries.map((m) => ({
|
|
87
91
|
role: "user",
|
|
88
|
-
content: `[Prior compaction summary]\n${
|
|
92
|
+
content: `[Prior compaction summary]\n${stripFileBlocks(messageText(m))}`,
|
|
89
93
|
})),
|
|
90
94
|
...summarizablePriorTurns,
|
|
91
95
|
...evictedGroups.flatMap((g) => [g.assistant, ...g.toolResults]),
|
|
92
96
|
];
|
|
97
|
+
// Cumulative file tracking (deterministic, never via the model): union the
|
|
98
|
+
// lists the prior summaries carried with the ops in what we're evicting now.
|
|
99
|
+
// Tool results ride along so failed/rejected calls don't count as touches.
|
|
100
|
+
const fileOps = collectCompactionFileOps([...summarizablePriorTurns, ...evictedGroups.flatMap((g) => [g.assistant, ...g.toolResults])], priorSummaries);
|
|
93
101
|
if (toSummarize.length === 0) {
|
|
94
102
|
return { compacted: false, reason: "nothing to evict" };
|
|
95
103
|
}
|
|
@@ -113,6 +121,14 @@ export async function compactWithLLM(messages, options) {
|
|
|
113
121
|
if (!summaryText || summaryText.trim().length === 0) {
|
|
114
122
|
return { compacted: false, reason: "compactor returned empty summary" };
|
|
115
123
|
}
|
|
124
|
+
// The summarizer's input transcript can contain projected reminder blocks
|
|
125
|
+
// (resident history holds them after pruned-mode projection), and models
|
|
126
|
+
// quote their input. This summary is persisted via onCompactionApplied, so
|
|
127
|
+
// scrub markup before it can reach the session file.
|
|
128
|
+
summaryText = sanitizeInternalReminderBlocks(summaryText).trim();
|
|
129
|
+
if (!summaryText) {
|
|
130
|
+
return { compacted: false, reason: "compactor returned only internal markup" };
|
|
131
|
+
}
|
|
116
132
|
// New history shape (prefix-cache-friendly: preserved system+meta stay at the
|
|
117
133
|
// absolute prefix unchanged; summary is injected after as a user-role envelope
|
|
118
134
|
// so it can't pollute the cacheable system-prompt prefix):
|
|
@@ -130,13 +146,16 @@ export async function compactWithLLM(messages, options) {
|
|
|
130
146
|
const compacted = [
|
|
131
147
|
...leading.map(cloneMessage),
|
|
132
148
|
...(pinnedFirstUser ? [clonePinnedUserMessage(pinnedFirstUser)] : []),
|
|
133
|
-
buildCompactionSummaryMessage(`${LLM_SUMMARY_PREFIX}\n${summaryText
|
|
149
|
+
buildCompactionSummaryMessage(appendFileBlocks(`${LLM_SUMMARY_PREFIX}\n${summaryText}`, fileOps)),
|
|
134
150
|
cloneMessage(lastUser),
|
|
135
151
|
...flatKept,
|
|
136
152
|
];
|
|
137
153
|
return {
|
|
138
154
|
compacted: true,
|
|
139
|
-
|
|
155
|
+
// Same payload the resident message carries (minus the envelope prefix —
|
|
156
|
+
// session replay adds its own "Previous conversation summary:" header), so
|
|
157
|
+
// the persisted checkpoint matches the in-memory state, file blocks included.
|
|
158
|
+
summary: appendFileBlocks(summaryText, fileOps),
|
|
140
159
|
messages: compacted,
|
|
141
160
|
};
|
|
142
161
|
}
|
|
@@ -135,6 +135,9 @@ export class RunDriver {
|
|
|
135
135
|
recordMemoryCitations(session.cwd, message.content);
|
|
136
136
|
}
|
|
137
137
|
},
|
|
138
|
+
onCompactionApplied: (summary) => {
|
|
139
|
+
session.manager.applyLLMCompaction(summary);
|
|
140
|
+
},
|
|
138
141
|
onToolResult: (toolName, result) => {
|
|
139
142
|
if (toolName !== "skill" || result.isError)
|
|
140
143
|
return;
|
package/dist/feishu/serve.js
CHANGED
|
@@ -91,15 +91,19 @@ export async function serveFeishu(opts = {}) {
|
|
|
91
91
|
if (mcpLoaded.servers.length > 0) {
|
|
92
92
|
await mcpManager.start();
|
|
93
93
|
}
|
|
94
|
-
const createProvider = (providerId, apiKey, baseURL, promptCacheKey) =>
|
|
95
|
-
providerId
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
94
|
+
const createProvider = (providerId, apiKey, baseURL, promptCacheKey) => {
|
|
95
|
+
const profile = providerRegistry.getConfigured().find((provider) => provider.id === providerId);
|
|
96
|
+
return createProviderInstance({
|
|
97
|
+
providerId,
|
|
98
|
+
apiKey,
|
|
99
|
+
baseURL,
|
|
100
|
+
promptCacheKey,
|
|
101
|
+
protocol: profile?.protocol,
|
|
102
|
+
headers: profile?.headers,
|
|
103
|
+
openAICodexAuth: providerRegistry.createOpenAICodexAuthAdapter(providerId),
|
|
104
|
+
grokAuth: providerRegistry.createGrokAuthAdapter(providerId),
|
|
105
|
+
});
|
|
106
|
+
};
|
|
103
107
|
const createProviderForRoute = async (route, promptCacheKey) => {
|
|
104
108
|
const target = providerRegistry.getConfigured().find((p) => p.id === route.providerId);
|
|
105
109
|
if (!target?.apiKey) {
|
package/dist/goal/tools.js
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* goal turn's prompt, so a get_goal call could only return what the model was
|
|
9
9
|
* just told.
|
|
10
10
|
*/
|
|
11
|
-
const UPDATE_GOAL_DESCRIPTION = `
|
|
12
|
-
Set status to "complete" only when the objective has actually been achieved and no required work remains — never merely because the budget is nearly exhausted or you are stopping.
|
|
11
|
+
const UPDATE_GOAL_DESCRIPTION = `Errors unless the user has set an active goal via /goal — do NOT call this to wrap up an ordinary turn; most sessions have no goal.
|
|
12
|
+
Update the active thread goal's status. Set status to "complete" only when the objective has actually been achieved and no required work remains — never merely because the budget is nearly exhausted or you are stopping.
|
|
13
13
|
Set status to "blocked" only when the same blocking condition has repeated for at least three consecutive goal turns (counting the original turn and automatic continuations) and you cannot make meaningful progress without user input or an external-state change. Do not use "blocked" because work is hard, slow, uncertain, or incomplete.
|
|
14
14
|
You cannot pause, resume, or set a budget through this tool; those are controlled by the user.`;
|
|
15
15
|
export function createGoalTools(store) {
|
|
@@ -29,7 +29,16 @@ export function createGoalTools(store) {
|
|
|
29
29
|
additionalProperties: false,
|
|
30
30
|
},
|
|
31
31
|
effect: "unknown",
|
|
32
|
-
|
|
32
|
+
// Goalless sessions must not see this tool AT ALL: a completion-marker
|
|
33
|
+
// in the function list gets spuriously called at turn end even by strong
|
|
34
|
+
// models at high effort (observed with gpt-5.6-sol) — wording does not
|
|
35
|
+
// beat the trained close-out-the-task prior. The enabled() gate removes
|
|
36
|
+
// it from the provider's tool list until a goal is active (re-evaluated
|
|
37
|
+
// every model call, so /goal mid-session takes effect immediately), and
|
|
38
|
+
// there is deliberately no promptSnippet since the static system prompt
|
|
39
|
+
// cannot track goal state. Goal turns inject full usage guidance
|
|
40
|
+
// separately (goal/prompts.ts), so the legitimate path loses nothing.
|
|
41
|
+
enabled: () => store.isActive(),
|
|
33
42
|
async execute(args) {
|
|
34
43
|
const goal = store.snapshot();
|
|
35
44
|
if (!goal)
|
package/dist/main.js
CHANGED
|
@@ -94,20 +94,25 @@ async function main() {
|
|
|
94
94
|
thinkingLevel: args.thinkingLevel,
|
|
95
95
|
promptCacheKey: sessionPromptCacheKey,
|
|
96
96
|
protocol: defaultProvider.protocol,
|
|
97
|
+
headers: defaultProvider.headers,
|
|
97
98
|
openAICodexAuth: registry.createOpenAICodexAuthAdapter(defaultProvider.id),
|
|
98
99
|
grokAuth: registry.createGrokAuthAdapter(defaultProvider.id),
|
|
99
100
|
})
|
|
100
101
|
: createUnavailableProvider(unavailableProviderMessage);
|
|
101
|
-
const createProvider = (providerId, apiKey, baseURL) =>
|
|
102
|
-
providerId
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
102
|
+
const createProvider = (providerId, apiKey, baseURL) => {
|
|
103
|
+
const profile = registry.getConfigured().find((provider) => provider.id === providerId);
|
|
104
|
+
return createProviderInstance({
|
|
105
|
+
providerId,
|
|
106
|
+
apiKey,
|
|
107
|
+
baseURL,
|
|
108
|
+
thinkingLevel: args.thinkingLevel,
|
|
109
|
+
promptCacheKey: sessionPromptCacheKey,
|
|
110
|
+
protocol: profile?.protocol,
|
|
111
|
+
headers: profile?.headers,
|
|
112
|
+
openAICodexAuth: registry.createOpenAICodexAuthAdapter(providerId),
|
|
113
|
+
grokAuth: registry.createGrokAuthAdapter(providerId),
|
|
114
|
+
});
|
|
115
|
+
};
|
|
111
116
|
const createProviderForRoute = async (route) => {
|
|
112
117
|
const providerId = route.providerId;
|
|
113
118
|
if (!providerId) {
|
|
@@ -196,18 +201,13 @@ async function main() {
|
|
|
196
201
|
for (const d of mcpLoaded.diagnostics) {
|
|
197
202
|
console.error(chalk.yellow(`[mcp:${d.scope}] ${d.path}: ${d.message}`));
|
|
198
203
|
}
|
|
199
|
-
|
|
204
|
+
// Silent diagnostics: connect failures are captured per-server and shown by
|
|
205
|
+
// /mcp on demand. Printing them at startup (or mid-session, where stderr
|
|
206
|
+
// writes corrupt the Ink display) only duplicates that surface.
|
|
207
|
+
const mcpManager = new McpManager({ servers: mcpLoaded.servers, onDiagnostic: () => { } });
|
|
200
208
|
let externalRuntime;
|
|
201
209
|
if (mcpLoaded.servers.length > 0) {
|
|
202
210
|
await mcpManager.start();
|
|
203
|
-
// Only surface failures at startup. Successful connections would push the
|
|
204
|
-
// welcome screen above the visible area on small terminals. /mcp shows the
|
|
205
|
-
// full status.
|
|
206
|
-
for (const state of mcpManager.getStates()) {
|
|
207
|
-
if (state.status.kind === "failed") {
|
|
208
|
-
console.error(chalk.yellow(`[mcp] ${state.name}: failed — ${state.status.error}`));
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
211
|
tools.push(...mcpManager.getToolEntries());
|
|
212
212
|
}
|
|
213
213
|
// Expose MCP prompts as slash commands. Queried live at each lookup so
|
|
@@ -420,6 +420,11 @@ async function main() {
|
|
|
420
420
|
recordMemoryCitations(args.cwd, message.content);
|
|
421
421
|
}
|
|
422
422
|
},
|
|
423
|
+
// Auto-compaction summaries are meta messages (dropped above); persist the
|
|
424
|
+
// compacted state as a session summary entry so it survives resume.
|
|
425
|
+
onCompactionApplied: (summary) => {
|
|
426
|
+
sessionManager?.applyLLMCompaction(summary);
|
|
427
|
+
},
|
|
423
428
|
onToolResult: (toolName, result) => {
|
|
424
429
|
if (!sessionManager)
|
|
425
430
|
return;
|