byterover-cli 3.10.1 → 3.10.2
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/infra/agent/service-initializer.js +8 -2
- package/dist/agent/infra/llm/agent-llm-service.d.ts +9 -9
- package/dist/agent/infra/llm/agent-llm-service.js +28 -18
- package/dist/agent/infra/llm/generators/ai-sdk-content-generator.d.ts +10 -1
- package/dist/agent/infra/llm/generators/ai-sdk-content-generator.js +21 -4
- package/dist/agent/infra/llm/generators/ai-sdk-message-converter.d.ts +4 -0
- package/dist/agent/infra/llm/generators/ai-sdk-message-converter.js +8 -1
- package/dist/agent/infra/map/abstract-generator.d.ts +29 -0
- package/dist/agent/infra/map/abstract-generator.js +161 -0
- package/dist/agent/infra/map/abstract-queue.d.ts +7 -0
- package/dist/agent/infra/map/abstract-queue.js +100 -26
- package/dist/agent/infra/system-prompt/contributors/file-contributor.js +6 -2
- package/dist/agent/infra/tools/tool-manager.d.ts +10 -1
- package/dist/agent/infra/tools/tool-manager.js +10 -1
- package/dist/server/infra/dream/dream-state-schema.d.ts +35 -0
- package/dist/server/infra/dream/dream-state-schema.js +15 -0
- package/dist/server/infra/dream/dream-state-service.d.ts +22 -0
- package/dist/server/infra/dream/dream-state-service.js +62 -3
- package/dist/server/infra/dream/dream-trigger.js +6 -2
- package/dist/server/infra/executor/curate-executor.d.ts +16 -0
- package/dist/server/infra/executor/curate-executor.js +76 -5
- package/dist/server/infra/executor/dream-executor.d.ts +16 -0
- package/dist/server/infra/executor/dream-executor.js +44 -7
- package/dist/server/infra/transport/handlers/provider-handler.js +20 -3
- package/dist/tui/features/auth/api/get-auth-state.js +6 -3
- package/dist/tui/features/auth/components/auth-initializer.js +4 -2
- package/oclif.manifest.json +413 -413
- package/package.json +1 -1
|
@@ -121,12 +121,18 @@ export async function createCipherAgentServices(config, agentEventBus) {
|
|
|
121
121
|
basePath: promptsBasePath,
|
|
122
122
|
validateConfig: true,
|
|
123
123
|
});
|
|
124
|
-
// Register default contributors
|
|
124
|
+
// Register default contributors.
|
|
125
|
+
//
|
|
126
|
+
// Note: dateTime is intentionally NOT in the system prompt. Anthropic
|
|
127
|
+
// prompt caching does token-level prefix matching, so a per-iteration
|
|
128
|
+
// refreshed timestamp here would invalidate the cache for everything
|
|
129
|
+
// past it. dateTime is instead injected into the first user message
|
|
130
|
+
// by AgentLLMService, where it lives after the cache breakpoints and
|
|
131
|
+
// does not poison the cached prefix.
|
|
125
132
|
systemPromptManager.registerContributors([
|
|
126
133
|
{ enabled: true, filepath: 'system-prompt.yml', id: 'base', priority: 0, type: 'file' },
|
|
127
134
|
{ enabled: true, id: 'env', priority: 10, type: 'environment' },
|
|
128
135
|
{ enabled: true, id: 'memories', priority: 20, type: 'memory' },
|
|
129
|
-
{ enabled: true, id: 'datetime', priority: 30, type: 'dateTime' },
|
|
130
136
|
]);
|
|
131
137
|
// Register context tree structure contributor for query/curate commands
|
|
132
138
|
// This injects the .brv/context-tree structure into the system prompt,
|
|
@@ -14,6 +14,15 @@ import { SessionEventBus } from '../events/event-emitter.js';
|
|
|
14
14
|
import { ContextManager, type FileData, type ImageData } from './context/context-manager.js';
|
|
15
15
|
import { type ThinkingConfig } from './thought-parser.js';
|
|
16
16
|
import { type TruncationConfig } from './tool-output-processor.js';
|
|
17
|
+
/**
|
|
18
|
+
* Build a `<dateTime>...</dateTime>\n\n` prefix for a user-message body.
|
|
19
|
+
*
|
|
20
|
+
* Per-call timestamps must NOT enter the system prompt (they would poison
|
|
21
|
+
* the prefix cache). They are injected into the user message instead, at
|
|
22
|
+
* the boundaries where the model legitimately needs fresh time context:
|
|
23
|
+
* the iter-0 input, and after a rolling-checkpoint history clear.
|
|
24
|
+
*/
|
|
25
|
+
export declare function buildDateTimePrefix(now?: Date): string;
|
|
17
26
|
/**
|
|
18
27
|
* Configuration for ByteRover LLM service
|
|
19
28
|
*/
|
|
@@ -390,15 +399,6 @@ export declare class AgentLLMService implements ILLMService {
|
|
|
390
399
|
* @param textInput - Original user input text (for continuation prompt)
|
|
391
400
|
*/
|
|
392
401
|
private performRollingCheckpoint;
|
|
393
|
-
/**
|
|
394
|
-
* Replace the DateTime section in a cached system prompt with a fresh timestamp.
|
|
395
|
-
* DateTimeContributor wraps its output in <dateTime>...</dateTime> XML tags,
|
|
396
|
-
* enabling reliable regex replacement without rebuilding the entire prompt.
|
|
397
|
-
*
|
|
398
|
-
* @param cachedPrompt - Previously cached system prompt
|
|
399
|
-
* @returns Updated prompt with fresh DateTime
|
|
400
|
-
*/
|
|
401
|
-
private refreshDateTime;
|
|
402
402
|
/**
|
|
403
403
|
* Check if a rolling checkpoint should trigger.
|
|
404
404
|
* Triggers every N iterations for curate/query commands, or when token utilization is high.
|
|
@@ -21,6 +21,17 @@ import { OpenRouterTokenizer } from './tokenizers/openrouter-tokenizer.js';
|
|
|
21
21
|
import { ToolOutputProcessor } from './tool-output-processor.js';
|
|
22
22
|
/** Target utilization ratio for message tokens (leaves headroom for response) */
|
|
23
23
|
const TARGET_MESSAGE_TOKEN_UTILIZATION = 0.7;
|
|
24
|
+
/**
|
|
25
|
+
* Build a `<dateTime>...</dateTime>\n\n` prefix for a user-message body.
|
|
26
|
+
*
|
|
27
|
+
* Per-call timestamps must NOT enter the system prompt (they would poison
|
|
28
|
+
* the prefix cache). They are injected into the user message instead, at
|
|
29
|
+
* the boundaries where the model legitimately needs fresh time context:
|
|
30
|
+
* the iter-0 input, and after a rolling-checkpoint history clear.
|
|
31
|
+
*/
|
|
32
|
+
export function buildDateTimePrefix(now = new Date()) {
|
|
33
|
+
return `<dateTime>Current date and time: ${now.toISOString()}</dateTime>\n\n`;
|
|
34
|
+
}
|
|
24
35
|
/**
|
|
25
36
|
* ByteRover LLM Service.
|
|
26
37
|
*
|
|
@@ -652,8 +663,11 @@ export class AgentLLMService {
|
|
|
652
663
|
this.memoryDirtyFlag = false;
|
|
653
664
|
}
|
|
654
665
|
else {
|
|
655
|
-
// Cache hit: reuse base prompt
|
|
656
|
-
|
|
666
|
+
// Cache hit: reuse base prompt verbatim. The cached prompt has no
|
|
667
|
+
// dateTime section to refresh — dateTime is injected into the
|
|
668
|
+
// first user message instead so the system prefix stays byte-stable
|
|
669
|
+
// across iterations and prompt caching can engage cleanly.
|
|
670
|
+
basePrompt = this.cachedBasePrompt;
|
|
657
671
|
}
|
|
658
672
|
let systemPrompt = basePrompt;
|
|
659
673
|
// Determine which reflection prompt to add (only highest priority is chosen)
|
|
@@ -687,9 +701,13 @@ export class AgentLLMService {
|
|
|
687
701
|
const systemPromptTokens = this.generator.estimateTokensSync(systemPrompt);
|
|
688
702
|
// Add user message and compress context within mutex lock
|
|
689
703
|
return this.mutex.withLock(async () => {
|
|
690
|
-
// Add user message to context only on the first iteration
|
|
704
|
+
// Add user message to context only on the first iteration. The
|
|
705
|
+
// dateTime block is prefixed here (not in the system prompt) so
|
|
706
|
+
// the cached system prefix stays byte-stable across iterations
|
|
707
|
+
// and Anthropic/OpenAI/Google prefix caches can engage cleanly.
|
|
691
708
|
if (iterationCount === 0) {
|
|
692
|
-
|
|
709
|
+
const inputWithDateTime = `${buildDateTimePrefix()}${textInput}`;
|
|
710
|
+
await this.contextManager.addUserMessage(inputWithDateTime, imageData, fileData);
|
|
693
711
|
}
|
|
694
712
|
// Rolling checkpoint: periodically save progress and clear history for RLM commands.
|
|
695
713
|
// This prevents unbounded token accumulation during long curation/query tasks.
|
|
@@ -1179,8 +1197,12 @@ export class AgentLLMService {
|
|
|
1179
1197
|
this.sandboxService.setSandboxVariable(sessionId, checkpointVar, progressSummary);
|
|
1180
1198
|
// Clear conversation history
|
|
1181
1199
|
await this.contextManager.clearHistory();
|
|
1182
|
-
// Re-inject continuation prompt with variable reference
|
|
1183
|
-
|
|
1200
|
+
// Re-inject continuation prompt with variable reference.
|
|
1201
|
+
// Prepend the dateTime block: clearHistory wiped the iter-0 user
|
|
1202
|
+
// message that originally carried it, and the iter-0 guard upstream
|
|
1203
|
+
// prevents re-injection. Without this, every iteration after the
|
|
1204
|
+
// first checkpoint loses time context for the rest of the run.
|
|
1205
|
+
const continuationPrompt = buildDateTimePrefix() + [
|
|
1184
1206
|
`Continue task. Iteration checkpoint at turn ${iterationCount}.`,
|
|
1185
1207
|
`Previous progress stored in variable: ${checkpointVar}`,
|
|
1186
1208
|
`Original task: ${textInput.slice(0, 200)}${textInput.length > 200 ? '...' : ''}`,
|
|
@@ -1191,18 +1213,6 @@ export class AgentLLMService {
|
|
|
1191
1213
|
message: `Rolling checkpoint at iteration ${iterationCount}: history cleared, progress saved to ${checkpointVar}`,
|
|
1192
1214
|
});
|
|
1193
1215
|
}
|
|
1194
|
-
/**
|
|
1195
|
-
* Replace the DateTime section in a cached system prompt with a fresh timestamp.
|
|
1196
|
-
* DateTimeContributor wraps its output in <dateTime>...</dateTime> XML tags,
|
|
1197
|
-
* enabling reliable regex replacement without rebuilding the entire prompt.
|
|
1198
|
-
*
|
|
1199
|
-
* @param cachedPrompt - Previously cached system prompt
|
|
1200
|
-
* @returns Updated prompt with fresh DateTime
|
|
1201
|
-
*/
|
|
1202
|
-
refreshDateTime(cachedPrompt) {
|
|
1203
|
-
const freshDateTime = `<dateTime>Current date and time: ${new Date().toISOString()}</dateTime>`;
|
|
1204
|
-
return cachedPrompt.replace(/<dateTime>[\S\s]*?<\/dateTime>/, freshDateTime);
|
|
1205
|
-
}
|
|
1206
1216
|
/**
|
|
1207
1217
|
* Check if a rolling checkpoint should trigger.
|
|
1208
1218
|
* Triggers every N iterations for curate/query commands, or when token utilization is high.
|
|
@@ -4,8 +4,17 @@
|
|
|
4
4
|
* Universal IContentGenerator adapter wrapping any AI SDK LanguageModel.
|
|
5
5
|
* Replaces per-provider content generators with one unified implementation.
|
|
6
6
|
*/
|
|
7
|
-
import type { LanguageModel } from 'ai';
|
|
7
|
+
import type { LanguageModel, ModelMessage } from 'ai';
|
|
8
8
|
import type { GenerateContentChunk, GenerateContentRequest, GenerateContentResponse, IContentGenerator } from '../../../core/interfaces/i-content-generator.js';
|
|
9
|
+
/**
|
|
10
|
+
* Prepend the system prompt as a system-role message carrying
|
|
11
|
+
* `providerOptions.anthropic.cacheControl: ephemeral`. AI SDK's top-level
|
|
12
|
+
* `system: string` parameter does not propagate providerOptions, so the
|
|
13
|
+
* only way to attach Anthropic cache_control to the system block is to
|
|
14
|
+
* pass it through the messages array. Non-Anthropic providers ignore the
|
|
15
|
+
* `anthropic` namespace.
|
|
16
|
+
*/
|
|
17
|
+
export declare function prependCachedSystemMessage(systemPrompt: string | undefined, messages: ModelMessage[]): ModelMessage[];
|
|
9
18
|
/**
|
|
10
19
|
* Configuration for AiSdkContentGenerator.
|
|
11
20
|
*/
|
|
@@ -8,6 +8,25 @@ import { generateText, streamText } from 'ai';
|
|
|
8
8
|
import { StreamChunkType } from '../../../core/interfaces/i-content-generator.js';
|
|
9
9
|
import { toAiSdkTools, toModelMessages } from './ai-sdk-message-converter.js';
|
|
10
10
|
const DEFAULT_CHARS_PER_TOKEN = 4;
|
|
11
|
+
/**
|
|
12
|
+
* Prepend the system prompt as a system-role message carrying
|
|
13
|
+
* `providerOptions.anthropic.cacheControl: ephemeral`. AI SDK's top-level
|
|
14
|
+
* `system: string` parameter does not propagate providerOptions, so the
|
|
15
|
+
* only way to attach Anthropic cache_control to the system block is to
|
|
16
|
+
* pass it through the messages array. Non-Anthropic providers ignore the
|
|
17
|
+
* `anthropic` namespace.
|
|
18
|
+
*/
|
|
19
|
+
export function prependCachedSystemMessage(systemPrompt, messages) {
|
|
20
|
+
if (!systemPrompt) {
|
|
21
|
+
return messages;
|
|
22
|
+
}
|
|
23
|
+
const systemMessage = {
|
|
24
|
+
content: systemPrompt,
|
|
25
|
+
providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' } } },
|
|
26
|
+
role: 'system',
|
|
27
|
+
};
|
|
28
|
+
return [systemMessage, ...messages];
|
|
29
|
+
}
|
|
11
30
|
/**
|
|
12
31
|
* Universal content generator that wraps any AI SDK LanguageModel.
|
|
13
32
|
*
|
|
@@ -27,7 +46,7 @@ export class AiSdkContentGenerator {
|
|
|
27
46
|
return Math.ceil(content.length / this.charsPerToken);
|
|
28
47
|
}
|
|
29
48
|
async generateContent(request) {
|
|
30
|
-
const messages = toModelMessages(request.contents);
|
|
49
|
+
const messages = prependCachedSystemMessage(request.systemPrompt, toModelMessages(request.contents));
|
|
31
50
|
const tools = toAiSdkTools(request.tools);
|
|
32
51
|
const result = await generateText({
|
|
33
52
|
maxOutputTokens: request.config.maxTokens,
|
|
@@ -35,7 +54,6 @@ export class AiSdkContentGenerator {
|
|
|
35
54
|
messages,
|
|
36
55
|
model: this.model,
|
|
37
56
|
temperature: request.config.temperature,
|
|
38
|
-
...(request.systemPrompt && { system: request.systemPrompt }),
|
|
39
57
|
...(tools && { tools }),
|
|
40
58
|
...(request.config.topK !== undefined && { topK: request.config.topK }),
|
|
41
59
|
...(request.config.topP !== undefined && { topP: request.config.topP }),
|
|
@@ -68,7 +86,7 @@ export class AiSdkContentGenerator {
|
|
|
68
86
|
};
|
|
69
87
|
}
|
|
70
88
|
async *generateContentStream(request) {
|
|
71
|
-
const messages = toModelMessages(request.contents);
|
|
89
|
+
const messages = prependCachedSystemMessage(request.systemPrompt, toModelMessages(request.contents));
|
|
72
90
|
const tools = toAiSdkTools(request.tools);
|
|
73
91
|
const result = streamText({
|
|
74
92
|
maxOutputTokens: request.config.maxTokens,
|
|
@@ -76,7 +94,6 @@ export class AiSdkContentGenerator {
|
|
|
76
94
|
messages,
|
|
77
95
|
model: this.model,
|
|
78
96
|
temperature: request.config.temperature,
|
|
79
|
-
...(request.systemPrompt && { system: request.systemPrompt }),
|
|
80
97
|
...(tools && { tools }),
|
|
81
98
|
...(request.config.topK !== undefined && { topK: request.config.topK }),
|
|
82
99
|
...(request.config.topP !== undefined && { topP: request.config.topP }),
|
|
@@ -16,5 +16,9 @@ export declare function toModelMessages(messages: InternalMessage[]): ModelMessa
|
|
|
16
16
|
/**
|
|
17
17
|
* Convert our ToolSet to AI SDK tool definitions.
|
|
18
18
|
* Tools are declared without `execute` — our agentic loop handles execution.
|
|
19
|
+
*
|
|
20
|
+
* The last tool gets `providerOptions.anthropic.cacheControl: ephemeral`,
|
|
21
|
+
* which makes Anthropic cache the entire tool block (and the system prompt
|
|
22
|
+
* before it). Non-Anthropic providers ignore the `anthropic` namespace.
|
|
19
23
|
*/
|
|
20
24
|
export declare function toAiSdkTools(tools?: InternalToolSet): Record<string, ReturnType<typeof aiSdkTool>> | undefined;
|
|
@@ -46,16 +46,23 @@ export function toModelMessages(messages) {
|
|
|
46
46
|
/**
|
|
47
47
|
* Convert our ToolSet to AI SDK tool definitions.
|
|
48
48
|
* Tools are declared without `execute` — our agentic loop handles execution.
|
|
49
|
+
*
|
|
50
|
+
* The last tool gets `providerOptions.anthropic.cacheControl: ephemeral`,
|
|
51
|
+
* which makes Anthropic cache the entire tool block (and the system prompt
|
|
52
|
+
* before it). Non-Anthropic providers ignore the `anthropic` namespace.
|
|
49
53
|
*/
|
|
50
54
|
export function toAiSdkTools(tools) {
|
|
51
55
|
if (!tools || Object.keys(tools).length === 0) {
|
|
52
56
|
return undefined;
|
|
53
57
|
}
|
|
58
|
+
const entries = Object.entries(tools);
|
|
54
59
|
const result = {};
|
|
55
|
-
for (const [name, def] of
|
|
60
|
+
for (const [index, [name, def]] of entries.entries()) {
|
|
61
|
+
const isLast = index === entries.length - 1;
|
|
56
62
|
result[name] = aiSdkTool({
|
|
57
63
|
description: def.description ?? '',
|
|
58
64
|
inputSchema: jsonSchema(def.parameters),
|
|
65
|
+
...(isLast && { providerOptions: { anthropic: { cacheControl: { type: 'ephemeral' } } } }),
|
|
59
66
|
});
|
|
60
67
|
}
|
|
61
68
|
return result;
|
|
@@ -8,6 +8,16 @@ export interface AbstractGenerateResult {
|
|
|
8
8
|
/** L1: key points + structure (~1500 tokens) */
|
|
9
9
|
overviewContent: string;
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Result from a batched abstract generation. One entry per input item, in
|
|
13
|
+
* input order. Empty string fields signal the model failed to produce content
|
|
14
|
+
* for that path — the caller's existing fail-open semantics still apply.
|
|
15
|
+
*/
|
|
16
|
+
export interface BatchedAbstractItem {
|
|
17
|
+
abstractContent: string;
|
|
18
|
+
contextPath: string;
|
|
19
|
+
overviewContent: string;
|
|
20
|
+
}
|
|
11
21
|
/**
|
|
12
22
|
* Generate L0 abstract and L1 overview for a knowledge file.
|
|
13
23
|
*
|
|
@@ -20,3 +30,22 @@ export interface AbstractGenerateResult {
|
|
|
20
30
|
* @returns Abstract and overview content strings
|
|
21
31
|
*/
|
|
22
32
|
export declare function generateFileAbstracts(fullContent: string, generator: IContentGenerator): Promise<AbstractGenerateResult>;
|
|
33
|
+
/**
|
|
34
|
+
* Generate L0 abstracts and L1 overviews for N knowledge files in two batched
|
|
35
|
+
* LLM calls (one batch for all L0s, one for all L1s) instead of 2N per-file
|
|
36
|
+
* calls.
|
|
37
|
+
*
|
|
38
|
+
* Two parallel calls; each call carries all input files in an XML envelope
|
|
39
|
+
* and the model is instructed to return one element per file. Output is
|
|
40
|
+
* parsed by path tag and matched back to the input order. Files the model
|
|
41
|
+
* fails to produce content for receive empty strings (caller's existing
|
|
42
|
+
* fail-open semantics still apply).
|
|
43
|
+
*
|
|
44
|
+
* Caller is responsible for capping batch size; this function does not split
|
|
45
|
+
* its input. Recommended cap is 5 files per call to keep the L1 batch's
|
|
46
|
+
* output budget under ~8K tokens.
|
|
47
|
+
*/
|
|
48
|
+
export declare function generateFileAbstractsBatch(items: ReadonlyArray<{
|
|
49
|
+
contextPath: string;
|
|
50
|
+
fullContent: string;
|
|
51
|
+
}>, generator: IContentGenerator): Promise<BatchedAbstractItem[]>;
|
|
@@ -31,6 +31,110 @@ ${content}
|
|
|
31
31
|
}
|
|
32
32
|
/** Truncate content before embedding in LLM prompts to avoid exceeding model context windows during bulk ingest. */
|
|
33
33
|
const MAX_ABSTRACT_CONTENT_CHARS = 20_000;
|
|
34
|
+
/**
|
|
35
|
+
* Per-file truncation when N files share a single batched call. Matches the
|
|
36
|
+
* non-batched cap (20 KB) so each file gets the same view of its content
|
|
37
|
+
* regardless of batched vs per-file mode — total batched user content scales
|
|
38
|
+
* linearly with N. Avoids quality regression on long-file curates that batched
|
|
39
|
+
* mode would otherwise see.
|
|
40
|
+
*/
|
|
41
|
+
const MAX_BATCHED_CONTENT_CHARS_PER_FILE = MAX_ABSTRACT_CONTENT_CHARS;
|
|
42
|
+
/** L0 batch output budget: 5 files × ~80 tokens + framing tags ≈ 600 tokens. */
|
|
43
|
+
const BATCH_L0_MAX_OUTPUT_TOKENS = 800;
|
|
44
|
+
/** L1 batch output budget: 5 files × ~1500 tokens + framing tags ≈ 8000 tokens. */
|
|
45
|
+
const BATCH_L1_MAX_OUTPUT_TOKENS = 8500;
|
|
46
|
+
const BATCHED_ABSTRACT_SYSTEM_PROMPT = `You are a technical documentation assistant.
|
|
47
|
+
You produce precise one-line summaries of knowledge documents in a strict XML format.
|
|
48
|
+
Output ONLY the XML — no preamble, no commentary, no markdown fences.`;
|
|
49
|
+
const BATCHED_OVERVIEW_SYSTEM_PROMPT = `You are a technical documentation assistant.
|
|
50
|
+
You produce structured overviews of knowledge documents in a strict XML format.
|
|
51
|
+
Output ONLY the XML — no preamble, no commentary, no markdown fences.`;
|
|
52
|
+
function escapeXmlAttr(value) {
|
|
53
|
+
return value.replaceAll('&', '&').replaceAll('"', '"').replaceAll('<', '<').replaceAll('>', '>');
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Wrap raw file content in a CDATA section so XML/HTML/JSX/markdown that
|
|
57
|
+
* mentions `</document>` or `</file>` (perfectly normal for docs that describe
|
|
58
|
+
* those formats) cannot terminate the envelope and conflate files. The inner
|
|
59
|
+
* `]]>` escape is the standard CDATA-in-CDATA trick: split the sequence so it
|
|
60
|
+
* never appears verbatim inside the active section.
|
|
61
|
+
*/
|
|
62
|
+
function wrapCdata(content) {
|
|
63
|
+
return `<![CDATA[${content.replaceAll(']]>', ']]]]><![CDATA[>')}]]>`;
|
|
64
|
+
}
|
|
65
|
+
function buildBatchedAbstractPrompt(items) {
|
|
66
|
+
const filesXml = items.map((it) => `<file path="${escapeXmlAttr(it.contextPath)}">
|
|
67
|
+
<document>${wrapCdata(it.content)}</document>
|
|
68
|
+
</file>`).join('\n');
|
|
69
|
+
return `For each of the following knowledge documents, produce a ONE-LINE summary (max 80 tokens) that is a complete sentence capturing the core topic and key insight.
|
|
70
|
+
|
|
71
|
+
Output format — emit exactly one <file> element per input file, with the same path attribute:
|
|
72
|
+
<file path="<path>"><abstract>One-line summary.</abstract></file>
|
|
73
|
+
|
|
74
|
+
Output only these XML elements, in any order. No preamble, no markdown fences.
|
|
75
|
+
|
|
76
|
+
<files>
|
|
77
|
+
${filesXml}
|
|
78
|
+
</files>`;
|
|
79
|
+
}
|
|
80
|
+
function buildBatchedOverviewPrompt(items) {
|
|
81
|
+
const filesXml = items.map((it) => `<file path="${escapeXmlAttr(it.contextPath)}">
|
|
82
|
+
<document>${wrapCdata(it.content)}</document>
|
|
83
|
+
</file>`).join('\n');
|
|
84
|
+
return `For each of the following knowledge documents, produce a structured overview (markdown, under 1500 tokens) that includes:
|
|
85
|
+
- Key points (3-7 bullet points)
|
|
86
|
+
- Structure / sections summary
|
|
87
|
+
- Any notable entities, patterns, or decisions mentioned
|
|
88
|
+
|
|
89
|
+
Output format — emit exactly one <file> element per input file, with the same path attribute:
|
|
90
|
+
<file path="<path>"><overview>
|
|
91
|
+
- bullet 1
|
|
92
|
+
- bullet 2
|
|
93
|
+
...
|
|
94
|
+
</overview></file>
|
|
95
|
+
|
|
96
|
+
Output only these XML elements, in any order. No preamble, no markdown fences.
|
|
97
|
+
|
|
98
|
+
<files>
|
|
99
|
+
${filesXml}
|
|
100
|
+
</files>`;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Extract <abstract>...</abstract> per <file path="..."> from the model output.
|
|
104
|
+
* Tolerant: ignores extra whitespace, supports nested newlines inside the inner
|
|
105
|
+
* tag. Returns a Map keyed by path. Paths that don't appear are absent.
|
|
106
|
+
*
|
|
107
|
+
* Anchored on `<file path="...">` openers (not `</file>` closers) so a model
|
|
108
|
+
* overview that mentions `</file>` literally in prose — perfectly normal for
|
|
109
|
+
* docs about XML, JSX, or build systems — cannot prematurely terminate the
|
|
110
|
+
* outer match and orphan the inner tag. Each opener owns the response slice
|
|
111
|
+
* up to the next opener (or end-of-string), and the inner regex extracts
|
|
112
|
+
* the payload from that slice.
|
|
113
|
+
*/
|
|
114
|
+
function parseBatchedTags(response, innerTag) {
|
|
115
|
+
const result = new Map();
|
|
116
|
+
const fileOpenerRe = /<file\s+path="([^"]*)"[^>]*>/g;
|
|
117
|
+
const innerRe = new RegExp(`<${innerTag}>([\\s\\S]*?)<\\/${innerTag}>`);
|
|
118
|
+
const openers = [];
|
|
119
|
+
let m;
|
|
120
|
+
while ((m = fileOpenerRe.exec(response)) !== null) {
|
|
121
|
+
openers.push({ bodyStart: fileOpenerRe.lastIndex, rawPath: m[1] });
|
|
122
|
+
}
|
|
123
|
+
for (const [i, opener] of openers.entries()) {
|
|
124
|
+
// Each opener's slice runs from its end to the start of the next opener
|
|
125
|
+
// (or end-of-string). Within that slice, the inner regex picks up the
|
|
126
|
+
// payload. A literal `</file>` in prose has no special meaning here.
|
|
127
|
+
const sliceEnd = i + 1 < openers.length ? openers[i + 1].bodyStart : response.length;
|
|
128
|
+
const slice = response.slice(opener.bodyStart, sliceEnd);
|
|
129
|
+
const inner = innerRe.exec(slice);
|
|
130
|
+
if (inner) {
|
|
131
|
+
const path = opener.rawPath
|
|
132
|
+
.replaceAll('&', '&').replaceAll('"', '"').replaceAll('<', '<').replaceAll('>', '>');
|
|
133
|
+
result.set(path, inner[1].trim());
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
34
138
|
/**
|
|
35
139
|
* Generate L0 abstract and L1 overview for a knowledge file.
|
|
36
140
|
*
|
|
@@ -65,3 +169,60 @@ export async function generateFileAbstracts(fullContent, generator) {
|
|
|
65
169
|
overviewContent: overviewText.trim(),
|
|
66
170
|
};
|
|
67
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Generate L0 abstracts and L1 overviews for N knowledge files in two batched
|
|
174
|
+
* LLM calls (one batch for all L0s, one for all L1s) instead of 2N per-file
|
|
175
|
+
* calls.
|
|
176
|
+
*
|
|
177
|
+
* Two parallel calls; each call carries all input files in an XML envelope
|
|
178
|
+
* and the model is instructed to return one element per file. Output is
|
|
179
|
+
* parsed by path tag and matched back to the input order. Files the model
|
|
180
|
+
* fails to produce content for receive empty strings (caller's existing
|
|
181
|
+
* fail-open semantics still apply).
|
|
182
|
+
*
|
|
183
|
+
* Caller is responsible for capping batch size; this function does not split
|
|
184
|
+
* its input. Recommended cap is 5 files per call to keep the L1 batch's
|
|
185
|
+
* output budget under ~8K tokens.
|
|
186
|
+
*/
|
|
187
|
+
export async function generateFileAbstractsBatch(items, generator) {
|
|
188
|
+
if (items.length === 0)
|
|
189
|
+
return [];
|
|
190
|
+
// Dedup by contextPath, keeping the LAST occurrence's content. The queue is
|
|
191
|
+
// FIFO so later items carry the most recent fullContent — and the disk file
|
|
192
|
+
// already reflects that write, so the abstract must summarize the latest
|
|
193
|
+
// state rather than an intermediate one. Without this dedup, duplicate paths
|
|
194
|
+
// emit two `<file path>` blocks the model may answer in either order; the
|
|
195
|
+
// tag parser keys on path and Map-collapses, leaving non-deterministic
|
|
196
|
+
// results for the duplicates.
|
|
197
|
+
const byPath = new Map();
|
|
198
|
+
for (const it of items) {
|
|
199
|
+
byPath.set(it.contextPath, {
|
|
200
|
+
content: it.fullContent.slice(0, MAX_BATCHED_CONTENT_CHARS_PER_FILE),
|
|
201
|
+
contextPath: it.contextPath,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
const truncated = [...byPath.values()];
|
|
205
|
+
const [abstractText, overviewText] = await Promise.all([
|
|
206
|
+
streamToText(generator, {
|
|
207
|
+
config: { maxTokens: BATCH_L0_MAX_OUTPUT_TOKENS, temperature: 0 },
|
|
208
|
+
contents: [{ content: buildBatchedAbstractPrompt(truncated), role: 'user' }],
|
|
209
|
+
model: 'default',
|
|
210
|
+
systemPrompt: BATCHED_ABSTRACT_SYSTEM_PROMPT,
|
|
211
|
+
taskId: randomUUID(),
|
|
212
|
+
}),
|
|
213
|
+
streamToText(generator, {
|
|
214
|
+
config: { maxTokens: BATCH_L1_MAX_OUTPUT_TOKENS, temperature: 0 },
|
|
215
|
+
contents: [{ content: buildBatchedOverviewPrompt(truncated), role: 'user' }],
|
|
216
|
+
model: 'default',
|
|
217
|
+
systemPrompt: BATCHED_OVERVIEW_SYSTEM_PROMPT,
|
|
218
|
+
taskId: randomUUID(),
|
|
219
|
+
}),
|
|
220
|
+
]);
|
|
221
|
+
const abstracts = parseBatchedTags(abstractText, 'abstract');
|
|
222
|
+
const overviews = parseBatchedTags(overviewText, 'overview');
|
|
223
|
+
return items.map((it) => ({
|
|
224
|
+
abstractContent: (abstracts.get(it.contextPath) ?? '').trim(),
|
|
225
|
+
contextPath: it.contextPath,
|
|
226
|
+
overviewContent: (overviews.get(it.contextPath) ?? '').trim(),
|
|
227
|
+
}));
|
|
228
|
+
}
|
|
@@ -20,6 +20,13 @@ export interface AbstractQueueStatus {
|
|
|
20
20
|
export declare class AbstractGenerationQueue {
|
|
21
21
|
private readonly projectRoot;
|
|
22
22
|
private readonly maxAttempts;
|
|
23
|
+
/**
|
|
24
|
+
* When true, scheduleNext fires the next batch even if pending is below
|
|
25
|
+
* BATCH_SIZE_CAP. Set by drain(); reset once the queue is fully idle.
|
|
26
|
+
* Without this, items below the cap would be buffered indefinitely with
|
|
27
|
+
* no flush trigger when a curate writes fewer files than the cap.
|
|
28
|
+
*/
|
|
29
|
+
private drainRequested;
|
|
23
30
|
private drainResolvers;
|
|
24
31
|
private failed;
|
|
25
32
|
private generator;
|