@axplusb/kepler 2.3.0 → 2.3.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/package.json +1 -1
- package/src/core/compact-history.mjs +127 -0
- package/src/core/error-guidance.mjs +287 -0
- package/src/core/jsonl-writer.mjs +9 -0
- package/src/core/local-store.mjs +20 -4
- package/src/core/resume-mode.mjs +11 -1
- package/src/terminal/repl.mjs +168 -16
- package/src/ui/formatter.mjs +8 -10
package/package.json
CHANGED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const SUMMARY_PREFIX = 'Session continuity summary after /compact:';
|
|
2
|
+
|
|
3
|
+
export function parseCompactTailCount(rest = '', fallback = 8) {
|
|
4
|
+
const text = String(rest || '').trim();
|
|
5
|
+
const match = text.match(/(?:^|\s)(?:--tail=|--tail\s+)?(\d+)(?:\s|$)/);
|
|
6
|
+
const n = match ? Number(match[1]) : Number(fallback);
|
|
7
|
+
if (!Number.isFinite(n)) return fallback;
|
|
8
|
+
return Math.max(2, Math.min(50, Math.floor(n)));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isCompactHistory(history = []) {
|
|
12
|
+
return typeof history?.[2]?.content === 'string'
|
|
13
|
+
&& history[2].content.startsWith(SUMMARY_PREFIX);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function extractCompactSummary(history = []) {
|
|
17
|
+
const content = String(history?.[2]?.content || '');
|
|
18
|
+
if (!content.startsWith(SUMMARY_PREFIX)) return '';
|
|
19
|
+
return content.slice(SUMMARY_PREFIX.length).trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function prepareCompactHistory({
|
|
23
|
+
agentHistory = [],
|
|
24
|
+
tailCount = 8,
|
|
25
|
+
minSourceMessages = 2,
|
|
26
|
+
} = {}) {
|
|
27
|
+
const history = Array.isArray(agentHistory)
|
|
28
|
+
? agentHistory.filter(msg => msg && typeof msg.content === 'string' && msg.content.trim())
|
|
29
|
+
: [];
|
|
30
|
+
const beforeCount = history.length;
|
|
31
|
+
const retainedCount = Math.min(Math.max(2, Number(tailCount) || 8), Math.max(0, beforeCount));
|
|
32
|
+
const prefixCount = isCompactHistory(history) ? 3 : 0;
|
|
33
|
+
const sourceEnd = Math.max(prefixCount, beforeCount - retainedCount);
|
|
34
|
+
const sourceMessages = history.slice(prefixCount, sourceEnd);
|
|
35
|
+
const tail = history.slice(sourceEnd);
|
|
36
|
+
|
|
37
|
+
if (sourceMessages.length < minSourceMessages) {
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
reason: beforeCount <= prefixCount + retainedCount
|
|
41
|
+
? 'not enough history beyond the retained tail'
|
|
42
|
+
: 'not enough compactable messages',
|
|
43
|
+
beforeCount,
|
|
44
|
+
prefixCount,
|
|
45
|
+
sourceMessages,
|
|
46
|
+
tail,
|
|
47
|
+
retainedCount,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
ok: true,
|
|
53
|
+
beforeCount,
|
|
54
|
+
prefixCount,
|
|
55
|
+
sourceMessages,
|
|
56
|
+
tail,
|
|
57
|
+
retainedCount: tail.length,
|
|
58
|
+
previousSummary: extractCompactSummary(history),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function applyCompactSummary({
|
|
63
|
+
prepared,
|
|
64
|
+
summary,
|
|
65
|
+
sessionId = '',
|
|
66
|
+
cwd = '',
|
|
67
|
+
originalRequest = '',
|
|
68
|
+
previousSourceMessageCount = 0,
|
|
69
|
+
now = new Date(),
|
|
70
|
+
} = {}) {
|
|
71
|
+
if (!prepared?.ok) throw new Error(prepared?.reason || 'history is not compactable');
|
|
72
|
+
const compactSummary = String(summary || '').trim();
|
|
73
|
+
if (!compactSummary) throw new Error('summary is required');
|
|
74
|
+
const priorCount = Math.max(0, Number(previousSourceMessageCount) || 0);
|
|
75
|
+
const sourceMessageCount = priorCount + prepared.sourceMessages.length;
|
|
76
|
+
const timestamp = now instanceof Date ? now.toISOString() : String(now || new Date().toISOString());
|
|
77
|
+
const firstUser = originalRequest || firstUserMessage(prepared.sourceMessages) || firstUserMessage(prepared.tail) || '(unknown)';
|
|
78
|
+
|
|
79
|
+
const metadata = [
|
|
80
|
+
'Compact metadata.',
|
|
81
|
+
sessionId ? `Session: ${sessionId}` : '',
|
|
82
|
+
cwd ? `Project: ${cwd}` : '',
|
|
83
|
+
`Compacted at: ${timestamp}`,
|
|
84
|
+
`Covered live messages: ${sourceMessageCount}`,
|
|
85
|
+
`Retained tail messages: ${prepared.tail.length}`,
|
|
86
|
+
].filter(Boolean).join('\n');
|
|
87
|
+
|
|
88
|
+
const agentHistory = [
|
|
89
|
+
{ role: 'user', content: metadata },
|
|
90
|
+
{ role: 'user', content: `Original user request from this compacted session:\n${firstUser}` },
|
|
91
|
+
{ role: 'user', content: `${SUMMARY_PREFIX}\n${compactSummary}` },
|
|
92
|
+
...prepared.tail,
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
agentHistory,
|
|
97
|
+
summary: compactSummary,
|
|
98
|
+
sourceMessageCount,
|
|
99
|
+
previousSourceMessageCount: priorCount,
|
|
100
|
+
beforeCount: prepared.beforeCount,
|
|
101
|
+
afterCount: agentHistory.length,
|
|
102
|
+
retainedCount: prepared.tail.length,
|
|
103
|
+
compactedCount: prepared.sourceMessages.length,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function localCompactSummary(messages = []) {
|
|
108
|
+
const list = Array.isArray(messages) ? messages : [];
|
|
109
|
+
const userMessages = list.filter(m => m.role === 'user').map(m => String(m.content || '').trim()).filter(Boolean);
|
|
110
|
+
const assistantMessages = list.filter(m => m.role === 'assistant').map(m => String(m.content || '').trim()).filter(Boolean);
|
|
111
|
+
return [
|
|
112
|
+
'Local compact summary from live conversation context.',
|
|
113
|
+
userMessages.length ? `User requests (${userMessages.length}):` : '',
|
|
114
|
+
...userMessages.slice(-8).map(text => `- ${truncate(text, 500)}`),
|
|
115
|
+
assistantMessages.length ? `Assistant progress (${assistantMessages.length}):` : '',
|
|
116
|
+
...assistantMessages.slice(-8).map(text => `- ${truncate(text, 700)}`),
|
|
117
|
+
].filter(Boolean).join('\n');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function firstUserMessage(messages = []) {
|
|
121
|
+
return messages.find(m => m?.role === 'user' && typeof m.content === 'string')?.content || '';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function truncate(text, max) {
|
|
125
|
+
const value = String(text || '').replace(/\s+/g, ' ').trim();
|
|
126
|
+
return value.length > max ? value.slice(0, max - 3) + '...' : value;
|
|
127
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
function compact(value) {
|
|
2
|
+
return String(value || '').trim();
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function lower(value) {
|
|
6
|
+
return compact(value).toLowerCase();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const PROVIDER_ALIASES = new Map([
|
|
10
|
+
['openrouter', 'openrouter'],
|
|
11
|
+
['openrouterv2', 'openrouter'],
|
|
12
|
+
['openroutergateway', 'openrouter'],
|
|
13
|
+
['openrouterv2gateway', 'openrouter'],
|
|
14
|
+
['anthropic', 'anthropic'],
|
|
15
|
+
['claude', 'anthropic'],
|
|
16
|
+
['claudegateway', 'anthropic'],
|
|
17
|
+
['openai', 'openai'],
|
|
18
|
+
['openaigateway', 'openai'],
|
|
19
|
+
['google', 'googleai'],
|
|
20
|
+
['googleai', 'googleai'],
|
|
21
|
+
['googlegateway', 'googleai'],
|
|
22
|
+
['googleaigateway', 'googleai'],
|
|
23
|
+
['gemini', 'googleai'],
|
|
24
|
+
['azure', 'azureopenai'],
|
|
25
|
+
['azureopenai', 'azureopenai'],
|
|
26
|
+
['azureopenaigateway', 'azureopenai'],
|
|
27
|
+
['bedrock', 'bedrock'],
|
|
28
|
+
['aws', 'bedrock'],
|
|
29
|
+
['bedrockgateway', 'bedrock'],
|
|
30
|
+
['databricks', 'databricks'],
|
|
31
|
+
['databricksgateway', 'databricks'],
|
|
32
|
+
['custom', 'custom'],
|
|
33
|
+
['byom', 'custom'],
|
|
34
|
+
['openai-compatible', 'custom'],
|
|
35
|
+
['deepseek', 'deepseek'],
|
|
36
|
+
['deepseekgateway', 'deepseek'],
|
|
37
|
+
['dashscope', 'dashscope'],
|
|
38
|
+
['dashscopegateway', 'dashscope'],
|
|
39
|
+
['qwen', 'dashscope'],
|
|
40
|
+
['zhipu', 'zhipu'],
|
|
41
|
+
['zhipugateway', 'zhipu'],
|
|
42
|
+
['glm', 'zhipu'],
|
|
43
|
+
['moonshot', 'moonshot'],
|
|
44
|
+
['moonshotgateway', 'moonshot'],
|
|
45
|
+
['kimi', 'moonshot'],
|
|
46
|
+
['xai', 'xai'],
|
|
47
|
+
['xaigateway', 'xai'],
|
|
48
|
+
['grok', 'xai'],
|
|
49
|
+
['mistral', 'mistral'],
|
|
50
|
+
['mistralgateway', 'mistral'],
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
const PROVIDER_GUIDANCE = {
|
|
54
|
+
openrouter: {
|
|
55
|
+
label: 'OpenRouter',
|
|
56
|
+
lines: [
|
|
57
|
+
'Check that your OpenRouter API key is saved and has enough credits/quota.',
|
|
58
|
+
'Verify the selected model is available on OpenRouter and that provider routing is allowed for it.',
|
|
59
|
+
'If settings were just updated, run /login or restart the CLI so provider settings sync again.',
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
anthropic: {
|
|
63
|
+
label: 'Anthropic',
|
|
64
|
+
lines: [
|
|
65
|
+
'Check that your Anthropic API key is saved and active.',
|
|
66
|
+
'Verify the selected Claude model is enabled for your Anthropic account and region.',
|
|
67
|
+
'If settings were just updated, run /login or restart the CLI so provider settings sync again.',
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
openai: {
|
|
71
|
+
label: 'OpenAI',
|
|
72
|
+
lines: [
|
|
73
|
+
'Check that your OpenAI API key is saved and has access to the selected model.',
|
|
74
|
+
'Verify project billing, model permissions, and rate limits in your OpenAI account.',
|
|
75
|
+
'If settings were just updated, run /login or restart the CLI so provider settings sync again.',
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
googleai: {
|
|
79
|
+
label: 'Google AI',
|
|
80
|
+
lines: [
|
|
81
|
+
'Check that your Google AI API key is saved and active.',
|
|
82
|
+
'Verify the selected Gemini model is available for that key and region.',
|
|
83
|
+
'If settings were just updated, run /login or restart the CLI so provider settings sync again.',
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
azureopenai: {
|
|
87
|
+
label: 'Azure OpenAI',
|
|
88
|
+
lines: [
|
|
89
|
+
'Check that Azure OpenAI API key, endpoint, API version, and deployment/model name are saved.',
|
|
90
|
+
'Verify the deployment exists in Azure AI Foundry and the key has access to that resource.',
|
|
91
|
+
'If settings were just updated, run /login or restart the CLI so provider settings sync again.',
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
bedrock: {
|
|
95
|
+
label: 'AWS Bedrock',
|
|
96
|
+
lines: [
|
|
97
|
+
'Check that AWS Access Key ID, Secret Access Key, optional Session Token, and Region are saved.',
|
|
98
|
+
'Verify the IAM principal can call bedrock-runtime:InvokeModel for the selected model and region.',
|
|
99
|
+
'If settings were just updated, run /login or restart the CLI so provider settings sync again.',
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
databricks: {
|
|
103
|
+
label: 'Databricks',
|
|
104
|
+
lines: [
|
|
105
|
+
'Check that your Databricks host/workspace URL and token are saved.',
|
|
106
|
+
'Verify the selected serving endpoint exists and the token can query it.',
|
|
107
|
+
'If settings were just updated, run /login or restart the CLI so provider settings sync again.',
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
custom: {
|
|
111
|
+
label: 'custom OpenAI-compatible provider',
|
|
112
|
+
lines: [
|
|
113
|
+
'Check that base URL, API key, and model name are saved for the custom provider.',
|
|
114
|
+
'Verify the endpoint supports OpenAI-compatible chat completions for the selected model.',
|
|
115
|
+
'If settings were just updated, run /login or restart the CLI so provider settings sync again.',
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
deepseek: {
|
|
119
|
+
label: 'DeepSeek',
|
|
120
|
+
lines: [
|
|
121
|
+
'Check that your DeepSeek API key is saved and active.',
|
|
122
|
+
'Verify the selected DeepSeek model is available and your account has quota.',
|
|
123
|
+
'If settings were just updated, run /login or restart the CLI so provider settings sync again.',
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
dashscope: {
|
|
127
|
+
label: 'DashScope/Qwen',
|
|
128
|
+
lines: [
|
|
129
|
+
'Check that your DashScope API key is saved and active.',
|
|
130
|
+
'Verify the selected Qwen model is available for your account and region.',
|
|
131
|
+
'If settings were just updated, run /login or restart the CLI so provider settings sync again.',
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
zhipu: {
|
|
135
|
+
label: 'Zhipu/GLM',
|
|
136
|
+
lines: [
|
|
137
|
+
'Check that your Zhipu API key is saved and active.',
|
|
138
|
+
'Verify the selected GLM model is enabled and your account has quota.',
|
|
139
|
+
'If settings were just updated, run /login or restart the CLI so provider settings sync again.',
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
moonshot: {
|
|
143
|
+
label: 'Moonshot/Kimi',
|
|
144
|
+
lines: [
|
|
145
|
+
'Check that your Moonshot API key is saved and active.',
|
|
146
|
+
'Verify the selected Kimi model is enabled and your account has quota.',
|
|
147
|
+
'If settings were just updated, run /login or restart the CLI so provider settings sync again.',
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
xai: {
|
|
151
|
+
label: 'xAI',
|
|
152
|
+
lines: [
|
|
153
|
+
'Check that your xAI API key is saved and active.',
|
|
154
|
+
'Verify the selected Grok model is enabled and your account has quota.',
|
|
155
|
+
'If settings were just updated, run /login or restart the CLI so provider settings sync again.',
|
|
156
|
+
],
|
|
157
|
+
},
|
|
158
|
+
mistral: {
|
|
159
|
+
label: 'Mistral',
|
|
160
|
+
lines: [
|
|
161
|
+
'Check that your Mistral API key is saved and active.',
|
|
162
|
+
'Verify the selected Mistral model is enabled and your account has quota.',
|
|
163
|
+
'If settings were just updated, run /login or restart the CLI so provider settings sync again.',
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
export function isBedrockMissingCredentials(data = {}) {
|
|
169
|
+
const haystack = [
|
|
170
|
+
data.message,
|
|
171
|
+
data.error,
|
|
172
|
+
data.code,
|
|
173
|
+
data.phase,
|
|
174
|
+
data.provider,
|
|
175
|
+
data.gateway,
|
|
176
|
+
data.gateway_type,
|
|
177
|
+
data.exception_type,
|
|
178
|
+
].map(lower).join(' ');
|
|
179
|
+
|
|
180
|
+
const mentionsBedrock = haystack.includes('bedrock');
|
|
181
|
+
const mentionsAwsCreds = haystack.includes('aws credentials')
|
|
182
|
+
|| haystack.includes('aws credential')
|
|
183
|
+
|| haystack.includes('aws_access_key')
|
|
184
|
+
|| haystack.includes('aws_secret')
|
|
185
|
+
|| haystack.includes('access key')
|
|
186
|
+
|| haystack.includes('secret access key');
|
|
187
|
+
|
|
188
|
+
return mentionsBedrock && mentionsAwsCreds;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function normalizeGatewayProvider(data = {}) {
|
|
192
|
+
const direct = [
|
|
193
|
+
data.provider,
|
|
194
|
+
data.gateway,
|
|
195
|
+
data.gateway_type,
|
|
196
|
+
data.gatewayType,
|
|
197
|
+
].map(normalizeProviderToken).find(Boolean);
|
|
198
|
+
if (direct) return direct;
|
|
199
|
+
|
|
200
|
+
const haystack = [
|
|
201
|
+
data.message,
|
|
202
|
+
data.error,
|
|
203
|
+
data.code,
|
|
204
|
+
data.phase,
|
|
205
|
+
data.operation,
|
|
206
|
+
data.exception_type,
|
|
207
|
+
data.model,
|
|
208
|
+
].map(lower).join(' ');
|
|
209
|
+
|
|
210
|
+
for (const [alias, provider] of PROVIDER_ALIASES.entries()) {
|
|
211
|
+
if (haystack.includes(alias)) return provider;
|
|
212
|
+
}
|
|
213
|
+
return '';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function formatAgentErrorGuidance(data = {}) {
|
|
217
|
+
const message = compact(data.message || data.error || 'Agent execution failed.');
|
|
218
|
+
const code = compact(data.code);
|
|
219
|
+
const phase = compact(data.phase);
|
|
220
|
+
const provider = normalizeGatewayProvider(data);
|
|
221
|
+
const providerMeta = provider || compact(data.provider || data.gateway || data.gateway_type);
|
|
222
|
+
const taskId = compact(data.task_id);
|
|
223
|
+
const retryAfter = data.retry_after != null ? compact(data.retry_after) : '';
|
|
224
|
+
const retryable = data.retryable === true;
|
|
225
|
+
|
|
226
|
+
if (isBedrockMissingCredentials(data)) {
|
|
227
|
+
return {
|
|
228
|
+
title: 'AWS Bedrock credentials are missing.',
|
|
229
|
+
lines: [
|
|
230
|
+
'Kepler reached the Bedrock gateway, but the backend did not receive AWS Access Key ID and Secret Access Key.',
|
|
231
|
+
'Open Kepler/AppStak model settings, re-save the AWS Bedrock provider with Access Key ID, Secret Access Key, and Region, then retry.',
|
|
232
|
+
'If settings were just updated, run /login or restart the CLI so provider settings sync again.',
|
|
233
|
+
],
|
|
234
|
+
meta: buildMeta({ code, phase, provider: provider || 'bedrock', taskId, retryAfter, retryable }),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const lines = [];
|
|
239
|
+
const providerGuidance = PROVIDER_GUIDANCE[provider];
|
|
240
|
+
if (phase === 'gateway' || code.includes('gateway')) {
|
|
241
|
+
const label = providerGuidance?.label || 'provider';
|
|
242
|
+
lines.push(`The ${label} gateway failed before the agent could respond.`);
|
|
243
|
+
if (providerGuidance) {
|
|
244
|
+
lines.push(...providerGuidance.lines);
|
|
245
|
+
} else {
|
|
246
|
+
lines.push('Check the selected provider, model, and BYOK credentials in settings, then retry.');
|
|
247
|
+
}
|
|
248
|
+
} else if (/authentication|token/i.test(message)) {
|
|
249
|
+
lines.push('Run /login to re-authenticate.');
|
|
250
|
+
} else if (/api key|openrouter/i.test(message)) {
|
|
251
|
+
lines.push('Run /config to set up or refresh your provider settings.');
|
|
252
|
+
} else if (/backend|network/i.test(message)) {
|
|
253
|
+
lines.push('Check that the backend is reachable, then retry.');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (retryable && retryAfter) {
|
|
257
|
+
lines.push(`This looks retryable after ${retryAfter}s.`);
|
|
258
|
+
} else if (retryable) {
|
|
259
|
+
lines.push('This looks retryable.');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
title: message,
|
|
264
|
+
lines,
|
|
265
|
+
meta: buildMeta({ code, phase, provider: providerMeta, taskId, retryAfter, retryable }),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function normalizeProviderToken(value) {
|
|
270
|
+
const raw = lower(value);
|
|
271
|
+
if (!raw) return '';
|
|
272
|
+
const compacted = raw.replace(/[^a-z0-9]/g, '');
|
|
273
|
+
return PROVIDER_ALIASES.get(raw)
|
|
274
|
+
|| PROVIDER_ALIASES.get(compacted)
|
|
275
|
+
|| '';
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function buildMeta({ code, phase, provider, taskId, retryAfter, retryable }) {
|
|
279
|
+
const parts = [];
|
|
280
|
+
if (provider) parts.push(`provider=${provider}`);
|
|
281
|
+
if (phase) parts.push(`phase=${phase}`);
|
|
282
|
+
if (code) parts.push(`code=${code}`);
|
|
283
|
+
if (retryable) parts.push(`retryable=true`);
|
|
284
|
+
if (retryAfter) parts.push(`retry_after=${retryAfter}s`);
|
|
285
|
+
if (taskId) parts.push(`task=${taskId}`);
|
|
286
|
+
return parts;
|
|
287
|
+
}
|
|
@@ -317,6 +317,15 @@ export class JsonlWriter {
|
|
|
317
317
|
}
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
async flush() {
|
|
321
|
+
await this._flush();
|
|
322
|
+
if (this._flushPromise) await this._flushPromise;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
get transcriptPath() {
|
|
326
|
+
return this._transcriptPath;
|
|
327
|
+
}
|
|
328
|
+
|
|
320
329
|
// ── Internal ──
|
|
321
330
|
|
|
322
331
|
_resetTurn() {
|
package/src/core/local-store.mjs
CHANGED
|
@@ -531,10 +531,10 @@ export function buildResumeHistory(detail, mode = 'compact') {
|
|
|
531
531
|
);
|
|
532
532
|
|
|
533
533
|
// PRD-068 §5.14.4: resume mode picker.
|
|
534
|
-
// 'full'
|
|
535
|
-
// '
|
|
536
|
-
//
|
|
537
|
-
// 'summary'
|
|
534
|
+
// 'full' — every turn sent verbatim (unchanged)
|
|
535
|
+
// 'checkpoint-full' — latest summary checkpoint + every message after it
|
|
536
|
+
// 'tail-N' — recap block + last N conversation messages
|
|
537
|
+
// 'summary' — recap block only. Cheapest continuity, biggest lossiness.
|
|
538
538
|
let agentHistory;
|
|
539
539
|
let sourceMessages = fullAgentHistory;
|
|
540
540
|
let summaryMessageIndex = -1;
|
|
@@ -543,6 +543,22 @@ export function buildResumeHistory(detail, mode = 'compact') {
|
|
|
543
543
|
if (mode === 'full') {
|
|
544
544
|
agentHistory = fullAgentHistory;
|
|
545
545
|
summaryCoveredMessageCount = fullAgentHistory.length;
|
|
546
|
+
} else if (mode === 'checkpoint-full') {
|
|
547
|
+
sourceMessages = [];
|
|
548
|
+
const tail = fullAgentHistory.slice(summaryCheckpointMessageCount);
|
|
549
|
+
activeSummary = priorSummary
|
|
550
|
+
|| summaryForMessages(fullAgentHistory.slice(0, summaryCheckpointMessageCount), {
|
|
551
|
+
fallback: summary,
|
|
552
|
+
label: 'Summary checkpoint',
|
|
553
|
+
});
|
|
554
|
+
summaryCoveredMessageCount = summaryCheckpointMessageCount;
|
|
555
|
+
agentHistory = [
|
|
556
|
+
{ role: 'user', content: metadata },
|
|
557
|
+
{ role: 'user', content: `Original user request from this resumed session:\n${originalRequest || '(unknown)'}` },
|
|
558
|
+
{ role: 'user', content: activeSummary || summary },
|
|
559
|
+
...tail,
|
|
560
|
+
];
|
|
561
|
+
summaryMessageIndex = 2;
|
|
546
562
|
} else if (mode === 'recap+tail' || /^tail-\d+$/.test(String(mode || ''))) {
|
|
547
563
|
const tailTurns = tailTurnsForMode(mode, detail);
|
|
548
564
|
const { tail, startIndex } = tailHistorySliceByRecentMessages(fullAgentHistory, tailTurns);
|
package/src/core/resume-mode.mjs
CHANGED
|
@@ -104,7 +104,7 @@ export function decideResumeMode({
|
|
|
104
104
|
* Estimate the context tokens for a candidate mode. Used by the tri-choice
|
|
105
105
|
* overlay to render "62k / 14k / 2k" per option before the user commits.
|
|
106
106
|
*
|
|
107
|
-
* @param {'full' | 'summary' | 'tail-10' | 'tail-20' | 'recap+tail'} choice
|
|
107
|
+
* @param {'full' | 'checkpoint-full' | 'summary' | 'tail-10' | 'tail-20' | 'recap+tail'} choice
|
|
108
108
|
* @param {number} fullTokens — projected transcript size in full mode
|
|
109
109
|
* @param {object} [opts]
|
|
110
110
|
* @returns {number} projected tokens for the chosen mode
|
|
@@ -116,11 +116,21 @@ export function projectedTokensForChoice(choice, fullTokens, opts = {}) {
|
|
|
116
116
|
tailBaseTokens = 2000,
|
|
117
117
|
perTailTurnTokens = 500,
|
|
118
118
|
summaryBaseTokens = 2000,
|
|
119
|
+
resumeSummary = null,
|
|
119
120
|
} = opts;
|
|
120
121
|
|
|
121
122
|
switch (choice) {
|
|
122
123
|
case 'full':
|
|
123
124
|
return Math.max(0, Number(fullTokens) || 0);
|
|
125
|
+
case 'checkpoint-full': {
|
|
126
|
+
const full = Math.max(0, Number(fullTokens) || 0);
|
|
127
|
+
const fullMessages = Math.max(0, Number(resumeSummary?.fullMessageCount) || 0);
|
|
128
|
+
const covered = Math.max(0, Number(resumeSummary?.sourceMessageCount) || 0);
|
|
129
|
+
if (!fullMessages || covered <= 0) return full;
|
|
130
|
+
const remainingMessages = Math.max(0, fullMessages - covered);
|
|
131
|
+
if (remainingMessages === 0) return summaryBaseTokens;
|
|
132
|
+
return summaryBaseTokens + (perTailTurnTokens * remainingMessages);
|
|
133
|
+
}
|
|
124
134
|
case 'tail-10':
|
|
125
135
|
return tailBaseTokens + (10 * perTailTurnTokens);
|
|
126
136
|
case 'tail-20':
|
package/src/terminal/repl.mjs
CHANGED
|
@@ -40,6 +40,7 @@ import { TarangAuth } from '../auth/tarang-auth.mjs';
|
|
|
40
40
|
import { ApprovalManager } from '../core/approval.mjs';
|
|
41
41
|
import { resolveBackendUrl } from '../core/backend-url.mjs';
|
|
42
42
|
import { formatMessageWindow, lowWindowStatus, messagesRemaining } from '../core/rate-limit-display.mjs';
|
|
43
|
+
import { formatAgentErrorGuidance } from '../core/error-guidance.mjs';
|
|
43
44
|
import { BUILTIN_AGENTS, runAgent } from './agents.mjs';
|
|
44
45
|
import { SessionManager } from '../core/session-manager.mjs';
|
|
45
46
|
import { parseArgs } from '../config/cli-args.mjs';
|
|
@@ -49,6 +50,7 @@ import { buildContextEnvelope } from '../core/context-envelope.mjs';
|
|
|
49
50
|
import { buildResumeHistory, combineResumeSummaries, getRecentSessions, getSessionDetail, getTranscriptProjectRoots } from '../core/local-store.mjs';
|
|
50
51
|
import { decideResumeMode, projectedTokensForChoice, formatTokens as formatCtxTokens } from '../core/resume-mode.mjs';
|
|
51
52
|
import { appendTask, ensureTaskFiles, loadTaskBoard, moveTask, removeTask, taskCounts, TASK_FILES, updateTask } from '../core/tasks.mjs';
|
|
53
|
+
import { applyCompactSummary, localCompactSummary, parseCompactTailCount, prepareCompactHistory } from '../core/compact-history.mjs';
|
|
52
54
|
import { toolDisplayLabel, toolDisplaySummary } from './tool-display.mjs';
|
|
53
55
|
import { createOrbit } from '../state/orbit.mjs';
|
|
54
56
|
import { attachOrbit, unmount as unmountStatusBar } from '../ui/status-bar.mjs';
|
|
@@ -196,6 +198,18 @@ function formatResumeCheckpointStatus(session) {
|
|
|
196
198
|
return c.dim(` · summarized${pct}`);
|
|
197
199
|
}
|
|
198
200
|
|
|
201
|
+
function formatResumeContextStatus(session) {
|
|
202
|
+
const full = formatCtxTokens(session?.contextTokens || 0);
|
|
203
|
+
const marker = session?.resumeSummary;
|
|
204
|
+
if (!marker || !Number(marker.sourceMessageCount)) {
|
|
205
|
+
return c.dim(`${full.padStart(5, ' ')} ctx`);
|
|
206
|
+
}
|
|
207
|
+
const resumable = formatCtxTokens(projectedTokensForChoice('checkpoint-full', session.contextTokens || 0, {
|
|
208
|
+
resumeSummary: marker,
|
|
209
|
+
}));
|
|
210
|
+
return c.dim(`${resumable.padStart(5, ' ')} resumable · ${full} full`) + formatResumeCheckpointStatus(session);
|
|
211
|
+
}
|
|
212
|
+
|
|
199
213
|
function formatRelativeTime(iso) {
|
|
200
214
|
if (!iso) return '';
|
|
201
215
|
const t = Date.parse(iso);
|
|
@@ -250,7 +264,7 @@ async function pickResumableSession(resumable, ctx) {
|
|
|
250
264
|
const ago = formatRelativeTime(s.updatedAt || s.startedAt).padEnd(9, ' ').slice(0, 9);
|
|
251
265
|
const status = endStatusMarker(s.endStatus);
|
|
252
266
|
const msgs = String(s.messageCount).padStart(3, ' ') + ' msgs';
|
|
253
|
-
const ctx =
|
|
267
|
+
const ctx = formatResumeContextStatus(s);
|
|
254
268
|
const cost = formatSessionCost(s.costUsd);
|
|
255
269
|
const partial = s.partial ? c.yellow(' ⚠partial') : '';
|
|
256
270
|
const instr = oneLineInstruction(s.instruction, 48);
|
|
@@ -308,8 +322,14 @@ async function chooseThresholdMode(ctx, decision) {
|
|
|
308
322
|
if (rl) rl.pause();
|
|
309
323
|
|
|
310
324
|
const canFull = decision.mode !== 'no-full-allowed';
|
|
325
|
+
const hasCheckpoint = Boolean(decision.resumeSummary?.sourceMessageCount);
|
|
326
|
+
const firstOption = canFull
|
|
327
|
+
? { key: 'f', value: 'full', label: 'full transcript', enabled: true }
|
|
328
|
+
: hasCheckpoint
|
|
329
|
+
? { key: 'f', value: 'checkpoint-full', label: 'checkpointed transcript', enabled: true }
|
|
330
|
+
: { key: 'f', value: 'full', label: 'full transcript', enabled: false };
|
|
311
331
|
const options = [
|
|
312
|
-
|
|
332
|
+
firstOption,
|
|
313
333
|
{ key: 's', value: 'summary', label: 'summary only', enabled: true },
|
|
314
334
|
{ key: '1', value: 'tail-10', label: 'summary + last 10 turns', enabled: true },
|
|
315
335
|
{ key: '2', value: 'tail-20', label: 'summary + last 20 turns', enabled: true },
|
|
@@ -328,7 +348,8 @@ async function chooseThresholdMode(ctx, decision) {
|
|
|
328
348
|
const projected = formatCtxTokens(decision.projected);
|
|
329
349
|
const win = formatCtxTokens(decision.windowSize);
|
|
330
350
|
const lines = [];
|
|
331
|
-
|
|
351
|
+
const rawLabel = hasCheckpoint ? 'Raw full transcript would use' : 'This session would use';
|
|
352
|
+
lines.push(` ${rawLabel} ${c.brand(`${projected} / ${win}`)} tokens (${pct}%)`);
|
|
332
353
|
if (decision.resumeSummary?.sourceMessageCount) {
|
|
333
354
|
const covered = Number(decision.resumeSummary.sourceMessageCount) || 0;
|
|
334
355
|
const full = Number(decision.resumeSummary.fullMessageCount) || 0;
|
|
@@ -337,14 +358,18 @@ async function chooseThresholdMode(ctx, decision) {
|
|
|
337
358
|
}
|
|
338
359
|
lines.push(canFull
|
|
339
360
|
? ` ${c.yellow('⚠')} ${c.dim('close to the highWatermark — consider a leaner mode:')}`
|
|
340
|
-
:
|
|
361
|
+
: hasCheckpoint
|
|
362
|
+
? ` ${c.yellow('⚠')} ${c.dim('raw full is over hardCap — checkpoint/tail modes are available:')}`
|
|
363
|
+
: ` ${c.red('⛔')} ${c.dim('over hardCap — full mode disabled:')}`);
|
|
341
364
|
lines.push('');
|
|
342
365
|
for (let i = 0; i < options.length; i++) {
|
|
343
366
|
const o = options[i];
|
|
344
367
|
const disabled = !o.enabled;
|
|
345
368
|
const marker = i === selected && !disabled ? c.brand('▸') : ' ';
|
|
346
369
|
const keyTag = c.dim('[') + (disabled ? c.dim(o.key) : c.brand(o.key)) + c.dim(']');
|
|
347
|
-
const proj = formatCtxTokens(projectedTokensForChoice(o.value, decision.projected
|
|
370
|
+
const proj = formatCtxTokens(projectedTokensForChoice(o.value, decision.projected, {
|
|
371
|
+
resumeSummary: decision.resumeSummary,
|
|
372
|
+
}));
|
|
348
373
|
const label = disabled ? c.dim(o.label) : (i === selected ? c.brand(o.label) : o.label);
|
|
349
374
|
const projCol = c.dim(`${proj.padStart(5, ' ')} ctx`);
|
|
350
375
|
const suffix = disabled ? c.dim(' (over hardCap)') : '';
|
|
@@ -389,6 +414,7 @@ async function chooseThresholdMode(ctx, decision) {
|
|
|
389
414
|
}
|
|
390
415
|
|
|
391
416
|
function resumeModeLabel(mode = 'full') {
|
|
417
|
+
if (mode === 'checkpoint-full') return 'checkpointed transcript';
|
|
392
418
|
if (mode === 'summary') return 'summary only';
|
|
393
419
|
const tailTurns = resumeTailTurnCount(mode);
|
|
394
420
|
if (tailTurns) return `summary + last ${tailTurns} turns`;
|
|
@@ -434,7 +460,7 @@ async function previewResumeSession(session, ctx) {
|
|
|
434
460
|
if (scrollOffset > maxOffset) scrollOffset = maxOffset;
|
|
435
461
|
|
|
436
462
|
const lines = [];
|
|
437
|
-
lines.push(` ${c.bold('Preview:')} ${c.brand(session.project || '(unknown)')} ${c.dim('Mode:')} ${c.brand(resumeModeLabel(mode))} ${c.dim(formatCtxTokens(projectedTokensForChoice(mode, session.contextTokens)) + ' ctx')}`);
|
|
463
|
+
lines.push(` ${c.bold('Preview:')} ${c.brand(session.project || '(unknown)')} ${c.dim('Mode:')} ${c.brand(resumeModeLabel(mode))} ${c.dim(formatCtxTokens(projectedTokensForChoice(mode, session.contextTokens, { resumeSummary: session.resumeSummary })) + ' ctx')}`);
|
|
438
464
|
lines.push(` ${c.dim('─'.repeat(60))}`);
|
|
439
465
|
for (let i = scrollOffset; i < Math.min(scrollOffset + rows, totalLines); i++) {
|
|
440
466
|
lines.push(fitAnsiLine(` ${c.dim(contentLines[i] || '')}`, cols - 1));
|
|
@@ -460,7 +486,7 @@ async function previewResumeSession(session, ctx) {
|
|
|
460
486
|
if (key === '[B') { scrollOffset += 1; render(); return; }
|
|
461
487
|
if (key === '[5~') { scrollOffset = Math.max(0, scrollOffset - 10); render(); return; }
|
|
462
488
|
if (key === '[6~') { scrollOffset += 10; render(); return; }
|
|
463
|
-
if (low === 'f') { mode = 'full'; history = rich(); scrollOffset = 0; render(); return; }
|
|
489
|
+
if (low === 'f') { mode = session.resumeSummary?.sourceMessageCount ? 'checkpoint-full' : 'full'; history = rich(); scrollOffset = 0; render(); return; }
|
|
464
490
|
if (low === 's') { mode = 'summary'; history = rich(); scrollOffset = 0; render(); return; }
|
|
465
491
|
if (low === '1') { mode = 'tail-10'; history = rich(); scrollOffset = 0; render(); return; }
|
|
466
492
|
if (low === '2') { mode = 'tail-20'; history = rich(); scrollOffset = 0; render(); return; }
|
|
@@ -659,6 +685,120 @@ async function summarizeResumeTranscript({
|
|
|
659
685
|
}
|
|
660
686
|
}
|
|
661
687
|
|
|
688
|
+
async function compactCurrentSession(ctx, rest = '') {
|
|
689
|
+
const tailCount = parseCompactTailCount(rest, 8);
|
|
690
|
+
const preparedLive = prepareCompactHistory({
|
|
691
|
+
agentHistory: session.agentHistory,
|
|
692
|
+
tailCount,
|
|
693
|
+
});
|
|
694
|
+
if (!preparedLive.ok) {
|
|
695
|
+
process.stderr.write(` ${c.gray(`Nothing to compact — ${preparedLive.reason}.`)}\n`);
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const progress = startResumeProgress('compact');
|
|
700
|
+
progress.update('preparing compact summary', 18);
|
|
701
|
+
let sourceMessages = preparedLive.sourceMessages;
|
|
702
|
+
let priorSummary = preparedLive.previousSummary || '';
|
|
703
|
+
let previousSourceMessageCount = 0;
|
|
704
|
+
let fullMessageCount = preparedLive.beforeCount;
|
|
705
|
+
let projectPath = safeCwd();
|
|
706
|
+
let sourceFrom = 'live';
|
|
707
|
+
let summaryWarning = '';
|
|
708
|
+
|
|
709
|
+
try {
|
|
710
|
+
if (session.id && ctx.jsonlWriter?.flush) {
|
|
711
|
+
progress.update('reading transcript checkpoint', 28);
|
|
712
|
+
await ctx.jsonlWriter.flush();
|
|
713
|
+
const detail = await getSessionDetail(session.id, { filePath: ctx.jsonlWriter.transcriptPath });
|
|
714
|
+
if (detail) {
|
|
715
|
+
const richHistory = buildResumeHistory({ ...detail, recapTailTurns: 8 }, `tail-${tailCount}`);
|
|
716
|
+
if (richHistory.sourceMessages?.length) {
|
|
717
|
+
sourceMessages = richHistory.sourceMessages;
|
|
718
|
+
priorSummary = richHistory.priorSummary || '';
|
|
719
|
+
previousSourceMessageCount = richHistory.summaryCheckpointMessageCount || 0;
|
|
720
|
+
fullMessageCount = richHistory.fullMessageCount || sourceMessages.length;
|
|
721
|
+
projectPath = detail.meta?.project || safeCwd();
|
|
722
|
+
sourceFrom = 'transcript';
|
|
723
|
+
} else if (richHistory.priorSummary) {
|
|
724
|
+
priorSummary = richHistory.priorSummary;
|
|
725
|
+
previousSourceMessageCount = richHistory.summaryCheckpointMessageCount || 0;
|
|
726
|
+
fullMessageCount = richHistory.fullMessageCount || preparedLive.beforeCount;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (!sourceMessages.length) {
|
|
732
|
+
progress.stop();
|
|
733
|
+
process.stderr.write(` ${c.gray('Nothing new to compact.')}\n`);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
progress.update('summarizing compacted history', 46);
|
|
738
|
+
const backendSummary = await summarizeResumeTranscript({
|
|
739
|
+
auth: ctx.auth,
|
|
740
|
+
toolExecutor: ctx.toolExecutor,
|
|
741
|
+
sessionId: session.id,
|
|
742
|
+
projectPath,
|
|
743
|
+
messages: sourceMessages,
|
|
744
|
+
});
|
|
745
|
+
let summarySource = backendSummary?.summary ? (backendSummary.source || 'backend') : 'local fallback';
|
|
746
|
+
let deltaSummary = backendSummary?.summary || '';
|
|
747
|
+
if (!deltaSummary) {
|
|
748
|
+
summaryWarning = backendSummary?.reason || 'backend summary unavailable';
|
|
749
|
+
deltaSummary = localCompactSummary(sourceMessages);
|
|
750
|
+
}
|
|
751
|
+
const summary = combineResumeSummaries(priorSummary, deltaSummary);
|
|
752
|
+
|
|
753
|
+
progress.update('rewriting live context', 74);
|
|
754
|
+
const applied = applyCompactSummary({
|
|
755
|
+
prepared: { ...preparedLive, sourceMessages },
|
|
756
|
+
summary,
|
|
757
|
+
sessionId: session.id,
|
|
758
|
+
cwd: projectPath,
|
|
759
|
+
originalRequest: session.history.find(m => m.role === 'user')?.content || session.lastTask || '',
|
|
760
|
+
previousSourceMessageCount,
|
|
761
|
+
});
|
|
762
|
+
session.agentHistory = applied.agentHistory;
|
|
763
|
+
session.compactSummary = summary;
|
|
764
|
+
session.compactSourceMessageCount = applied.sourceMessageCount;
|
|
765
|
+
|
|
766
|
+
if (ctx.jsonlWriter) {
|
|
767
|
+
progress.update('writing summary checkpoint', 88);
|
|
768
|
+
ctx.jsonlWriter.writeKeplerEvent({
|
|
769
|
+
type: 'resume_summary',
|
|
770
|
+
data: {
|
|
771
|
+
session_id: session.id || null,
|
|
772
|
+
mode: 'compact',
|
|
773
|
+
mode_label: '/compact',
|
|
774
|
+
summary,
|
|
775
|
+
summary_source: summarySource,
|
|
776
|
+
summary_warning: summaryWarning || null,
|
|
777
|
+
source: sourceFrom,
|
|
778
|
+
source_message_count: applied.sourceMessageCount,
|
|
779
|
+
previous_source_message_count: previousSourceMessageCount,
|
|
780
|
+
full_message_count: fullMessageCount,
|
|
781
|
+
retained_tail_messages: applied.retainedCount,
|
|
782
|
+
live_before_messages: applied.beforeCount,
|
|
783
|
+
live_after_messages: applied.afterCount,
|
|
784
|
+
},
|
|
785
|
+
});
|
|
786
|
+
await ctx.jsonlWriter.flush?.();
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
progress.stop();
|
|
790
|
+
process.stderr.write(
|
|
791
|
+
` ${c.green('✓')} ${c.dim(`Compacted context: ${applied.beforeCount} → ${applied.afterCount} live messages · retained ${applied.retainedCount} · summary ${summarySource}`)}\n`
|
|
792
|
+
);
|
|
793
|
+
if (summaryWarning) {
|
|
794
|
+
process.stderr.write(` ${c.yellow('⚠')} ${c.dim(`backend summary unavailable — used local summary (${summaryWarning})`)}\n`);
|
|
795
|
+
}
|
|
796
|
+
} catch (err) {
|
|
797
|
+
progress.stop();
|
|
798
|
+
process.stderr.write(` ${c.red(`Compact failed: ${err?.message || String(err)}`)}\n`);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
662
802
|
function resumeTailTurnCount(mode = '') {
|
|
663
803
|
const match = String(mode || '').match(/^tail-(\d+)$/);
|
|
664
804
|
if (!match) return null;
|
|
@@ -1224,10 +1364,12 @@ function updateStatusBar() {
|
|
|
1224
1364
|
// buffered head as a regular two-line shape first so the interleaving
|
|
1225
1365
|
// content lands below it.
|
|
1226
1366
|
let _pendingHead = null; // { callId, head, indent }
|
|
1367
|
+
let _lastRenderedBlock = null; // 'tool' | 'content' | null
|
|
1227
1368
|
|
|
1228
1369
|
function flushPendingHead() {
|
|
1229
1370
|
if (!_pendingHead) return;
|
|
1230
|
-
process.stderr.write(
|
|
1371
|
+
process.stderr.write(`${_pendingHead.head}\n`);
|
|
1372
|
+
_lastRenderedBlock = 'tool';
|
|
1231
1373
|
_pendingHead = null;
|
|
1232
1374
|
}
|
|
1233
1375
|
|
|
@@ -1312,7 +1454,8 @@ function renderToolResult(data, eventType = 'tool_result') {
|
|
|
1312
1454
|
const cols = process.stderr.columns || 120;
|
|
1313
1455
|
const combined = `${_pendingHead.head} ${outcome}`;
|
|
1314
1456
|
if (stripAnsi(combined).length <= cols) {
|
|
1315
|
-
process.stderr.write(
|
|
1457
|
+
process.stderr.write(`${combined}\n`);
|
|
1458
|
+
_lastRenderedBlock = 'tool';
|
|
1316
1459
|
_pendingHead = null;
|
|
1317
1460
|
return;
|
|
1318
1461
|
}
|
|
@@ -1326,6 +1469,7 @@ function renderToolResult(data, eventType = 'tool_result') {
|
|
|
1326
1469
|
|
|
1327
1470
|
// Two-line shape: gutter under the (already-printed or just-flushed) head.
|
|
1328
1471
|
process.stderr.write(`${gutter}${outcome}\n`);
|
|
1472
|
+
_lastRenderedBlock = 'tool';
|
|
1329
1473
|
|
|
1330
1474
|
// Lint warnings stay visible alongside writes.
|
|
1331
1475
|
if (hasLint) {
|
|
@@ -1448,6 +1592,7 @@ function startContentStream() {
|
|
|
1448
1592
|
_streamedPartialText = '';
|
|
1449
1593
|
_renderedToolResults.clear();
|
|
1450
1594
|
_renderedContentThisTurn = false;
|
|
1595
|
+
_lastRenderedBlock = null;
|
|
1451
1596
|
stopSpinner();
|
|
1452
1597
|
}
|
|
1453
1598
|
|
|
@@ -1472,12 +1617,14 @@ function flushContent() {
|
|
|
1472
1617
|
// Any buffered tool head needs to land BEFORE this content so the order
|
|
1473
1618
|
// is preserved on screen.
|
|
1474
1619
|
flushPendingHead();
|
|
1620
|
+
if (_lastRenderedBlock === 'tool') process.stderr.write('\n');
|
|
1475
1621
|
const rendered = renderMarkdown(_streamBuffer);
|
|
1476
1622
|
for (const line of rendered.split('\n')) {
|
|
1477
1623
|
process.stdout.write(` ${line}\n`);
|
|
1478
1624
|
}
|
|
1479
1625
|
_streamBuffer = '';
|
|
1480
1626
|
_renderedContentThisTurn = true;
|
|
1627
|
+
_lastRenderedBlock = 'content';
|
|
1481
1628
|
}
|
|
1482
1629
|
|
|
1483
1630
|
// ── Event Renderer ──
|
|
@@ -1529,11 +1676,13 @@ function renderEvent(event) {
|
|
|
1529
1676
|
}
|
|
1530
1677
|
}
|
|
1531
1678
|
if (text) {
|
|
1679
|
+
if (_lastRenderedBlock === 'tool') process.stderr.write('\n');
|
|
1532
1680
|
const rendered = renderMarkdown(text);
|
|
1533
1681
|
for (const line of rendered.split('\n')) {
|
|
1534
1682
|
process.stdout.write(` ${line}\n`);
|
|
1535
1683
|
}
|
|
1536
1684
|
_renderedContentThisTurn = true;
|
|
1685
|
+
_lastRenderedBlock = 'content';
|
|
1537
1686
|
}
|
|
1538
1687
|
break;
|
|
1539
1688
|
}
|
|
@@ -1760,9 +1909,15 @@ function renderEvent(event) {
|
|
|
1760
1909
|
case 'error':
|
|
1761
1910
|
stopSpinner();
|
|
1762
1911
|
flushContent();
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
process.stderr.write(
|
|
1912
|
+
{
|
|
1913
|
+
const guidance = formatAgentErrorGuidance(data || {});
|
|
1914
|
+
process.stderr.write(`\n ${c.red('✗')} ${guidance.title}\n`);
|
|
1915
|
+
for (const line of guidance.lines) {
|
|
1916
|
+
process.stderr.write(` ${c.dim(line)}\n`);
|
|
1917
|
+
}
|
|
1918
|
+
if (guidance.meta.length) {
|
|
1919
|
+
process.stderr.write(` ${c.dim(guidance.meta.join(' · '))}\n`);
|
|
1920
|
+
}
|
|
1766
1921
|
}
|
|
1767
1922
|
break;
|
|
1768
1923
|
|
|
@@ -2576,10 +2731,7 @@ async function handleCommand(input, ctx) {
|
|
|
2576
2731
|
}
|
|
2577
2732
|
|
|
2578
2733
|
case '/compact': {
|
|
2579
|
-
|
|
2580
|
-
if (before <= 4) { process.stderr.write(` ${c.gray('Nothing to compact.')}\n`); return; }
|
|
2581
|
-
session.agentHistory.splice(2, session.agentHistory.length - 6);
|
|
2582
|
-
process.stderr.write(` ${c.gray(`Compacted agent context: ${before} → ${session.agentHistory.length} messages`)}\n`);
|
|
2734
|
+
await compactCurrentSession(ctx, rest);
|
|
2583
2735
|
return;
|
|
2584
2736
|
}
|
|
2585
2737
|
|
package/src/ui/formatter.mjs
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { toolDisplayLabel, toolDisplaySummary } from '../terminal/tool-display.mjs';
|
|
12
12
|
import { formatMessageWindow } from '../core/rate-limit-display.mjs';
|
|
13
|
+
import { formatAgentErrorGuidance } from '../core/error-guidance.mjs';
|
|
13
14
|
|
|
14
15
|
const RESET = '\x1b[0m';
|
|
15
16
|
const BOLD = '\x1b[1m';
|
|
@@ -309,16 +310,13 @@ export class EventFormatter {
|
|
|
309
310
|
}
|
|
310
311
|
|
|
311
312
|
_error(data) {
|
|
312
|
-
const
|
|
313
|
-
process.stderr.write(`\n ${RED}✗ ${
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
process.stderr.write(` ${DIM}Run /config to set up your provider${RESET}\n`);
|
|
320
|
-
} else if (msg.includes('Backend') || msg.includes('Network')) {
|
|
321
|
-
process.stderr.write(` ${DIM}Check if the backend is running at ${this.sessionInfo?.backend || 'localhost:8150'}${RESET}\n`);
|
|
313
|
+
const guidance = formatAgentErrorGuidance(data || {});
|
|
314
|
+
process.stderr.write(`\n ${RED}✗ ${guidance.title}${RESET}\n`);
|
|
315
|
+
for (const line of guidance.lines) {
|
|
316
|
+
process.stderr.write(` ${DIM}${line}${RESET}\n`);
|
|
317
|
+
}
|
|
318
|
+
if (guidance.meta.length) {
|
|
319
|
+
process.stderr.write(` ${DIM}${guidance.meta.join(' · ')}${RESET}\n`);
|
|
322
320
|
}
|
|
323
321
|
}
|
|
324
322
|
|