@wangzhizhi/remi 0.1.225 → 0.1.244
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/README.md +206 -2
- package/dist/help.js +1 -1
- package/dist/initPrompt.js +1 -1
- package/dist/setup.js +5 -4
- package/dist/statusCard.js +1 -1
- package/dist/statusline.js +1 -1
- package/dist/tui/RemiApp.js +55 -7
- package/dist/tui/hooksPanel.js +2 -2
- package/dist/tui/renderers/MessageList.js +10 -7
- package/dist/version.js +1 -1
- package/node_modules/@remi/compact/dist/index.js +13 -5
- package/node_modules/@remi/compact/package.json +1 -1
- package/node_modules/@remi/config/dist/index.js +816 -38
- package/node_modules/@remi/config/package.json +1 -1
- package/node_modules/@remi/core/dist/cacheBenchmark.js +217 -0
- package/node_modules/@remi/core/dist/cacheDiagnostics.js +300 -0
- package/node_modules/@remi/core/dist/contextBuilder.js +13 -11
- package/node_modules/@remi/core/dist/index.js +630 -33
- package/node_modules/@remi/core/dist/stableHash.js +30 -0
- package/node_modules/@remi/core/package.json +1 -1
- package/node_modules/@remi/hooks/package.json +1 -1
- package/node_modules/@remi/llm/package.json +1 -1
- package/node_modules/@remi/mcp/dist/index.js +32 -18
- package/node_modules/@remi/mcp/package.json +1 -1
- package/node_modules/@remi/memory/package.json +1 -1
- package/node_modules/@remi/permissions/package.json +1 -1
- package/node_modules/@remi/sessions/dist/index.js +38 -0
- package/node_modules/@remi/sessions/package.json +1 -1
- package/node_modules/@remi/skills/dist/index.js +2 -2
- package/node_modules/@remi/skills/package.json +1 -1
- package/node_modules/@remi/terminal-markdown/dist/index.js +159 -22
- package/node_modules/@remi/terminal-markdown/package.json +1 -1
- package/node_modules/@remi/tools/dist/index.js +1 -1
- package/node_modules/@remi/tools/package.json +1 -1
- package/package.json +13 -13
- package/prompt/tool-use-system.md +19 -3
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
import { loadRemiConfig, normalizePermissionProfile, resolveToolPermissionMode, } from '@remi/config';
|
|
2
2
|
import { buildBaseSystemPrompt, buildCurrentTurnSystemPrompt, defaultResponseStyleId, resolveResponseStyle, } from './responseStyles.js';
|
|
3
3
|
import { buildProviderContext } from './contextBuilder.js';
|
|
4
|
+
import { buildCacheDiagnostic, latestCacheDiagnostic } from './cacheDiagnostics.js';
|
|
4
5
|
import { buildDirectoryOverviewPrelude } from './directoryOverview.js';
|
|
5
6
|
import { buildProjectInstructionsContext } from './projectInstructions.js';
|
|
6
7
|
import { readPromptFile } from './promptFiles.js';
|
|
8
|
+
import { stableJson } from './stableHash.js';
|
|
7
9
|
import { executeConfiguredHooks, hasHookForEvent, } from '@remi/hooks';
|
|
8
10
|
import { emptyMemoryRecall, recallRelevantMemories, recordMemoryUse } from '@remi/memory';
|
|
9
|
-
import { compactSession, estimateSessionTokens, estimateTextTokens, eventsAfterLatestCompact, latestCompactSummary, maybeUpdateSessionMemory, readSessionMemory } from '@remi/compact';
|
|
11
|
+
import { compactSession, estimateSessionTokens, estimateTextTokens, eventsAfterLatestCompact, latestCompactSummary, maybeUpdateSessionMemory, readSessionMemory, } from '@remi/compact';
|
|
10
12
|
import { formatSkillIndexContext, loadSkillContent, loadSkillIndex } from '@remi/skills';
|
|
11
13
|
export { buildBaseSystemPrompt, buildCurrentTurnSystemPrompt, buildResponseLanguageSystemPrompt, buildResponseStyleSystemPrompt, defaultResponseStyleId, isResponseStyleId, normalizeResponseStyleId, resolveResponseStyle, responseStylePresets, } from './responseStyles.js';
|
|
12
14
|
export { buildProviderContext, } from './contextBuilder.js';
|
|
13
15
|
export { buildProjectInstructionsContext } from './projectInstructions.js';
|
|
14
16
|
export { promptFileCandidates, readPromptFile } from './promptFiles.js';
|
|
17
|
+
export { buildCacheDiagnosticReport, formatCacheDiagnosticStatus, } from './cacheDiagnostics.js';
|
|
18
|
+
export { buildCacheShapeBenchmark, buildRemiCacheShapeBenchmarkFixture, buildRemiCacheShapeBenchmarkRequest, } from './cacheBenchmark.js';
|
|
15
19
|
import { createModelRouter, createProviderClient, createTokenEstimatorRegistry, } from '@remi/llm';
|
|
16
|
-
import { existsSync } from 'node:fs';
|
|
20
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
17
21
|
import { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep } from 'node:path';
|
|
18
22
|
import { createSessionStore, readSessionEvents, writeToolResultArtifact } from '@remi/sessions';
|
|
19
23
|
import { createMcpAwareToolRegistry, createMcpRuntime } from '@remi/mcp';
|
|
@@ -162,6 +166,11 @@ const maxAutoCompactFailures = 3;
|
|
|
162
166
|
const maxFilesystemContextEvents = 120;
|
|
163
167
|
const maxFilesystemContextRoots = 8;
|
|
164
168
|
const maxFilesystemContextPaths = 12;
|
|
169
|
+
const maxRecentContinuationEvents = 160;
|
|
170
|
+
const maxRecentLoadedSkills = 3;
|
|
171
|
+
const maxRecentToolFacts = 8;
|
|
172
|
+
const maxRecentFileExcerpts = 2;
|
|
173
|
+
const maxRecentFileExcerptChars = 2_000;
|
|
165
174
|
const maxFailedToolContextTurns = 3;
|
|
166
175
|
const maxFailedToolContextTextChars = 240;
|
|
167
176
|
const toolResultArtifactThresholdBytes = 16 * 1024;
|
|
@@ -191,7 +200,7 @@ export function createAgentReadOnlyToolExecutor() {
|
|
|
191
200
|
export function createAgentLocalToolExecutor() {
|
|
192
201
|
return createRemiLocalToolExecutor(createAgentToolRegistry());
|
|
193
202
|
}
|
|
194
|
-
function createContextBudget(model, config, runtimeConfig) {
|
|
203
|
+
export function createContextBudget(model, config, runtimeConfig) {
|
|
195
204
|
const contextWindowTokens = Math.max(1, model.contextWindow);
|
|
196
205
|
const configuredOutputReserve = config.maxOutputReserveTokens ?? runtimeConfig?.maxOutputReserveTokens;
|
|
197
206
|
const modelOutputReserve = Math.min(model.maxOutputTokens ?? defaultMaxOutputReserveTokens, defaultMaxOutputReserveTokens);
|
|
@@ -251,18 +260,17 @@ function shouldAutoCompactSession(options) {
|
|
|
251
260
|
maxSummaryChars: tokenBudgetToChars(options.budget.tokenBudgets.compact ?? 6_000),
|
|
252
261
|
};
|
|
253
262
|
}
|
|
254
|
-
function performAutoCompactSession(options) {
|
|
263
|
+
async function performAutoCompactSession(options) {
|
|
255
264
|
try {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
catch {
|
|
260
|
-
// Auto compact can still proceed from transcript if session memory update fails.
|
|
261
|
-
}
|
|
262
|
-
const result = compactSession(options.cwd, options.sessionId, {
|
|
265
|
+
const result = await compactCurrentSession({
|
|
266
|
+
cwd: options.cwd,
|
|
267
|
+
sessionId: options.sessionId,
|
|
263
268
|
maxSummaryChars: options.maxSummaryChars,
|
|
264
269
|
trigger: 'auto',
|
|
265
|
-
strategy:
|
|
270
|
+
strategy: options.strategy,
|
|
271
|
+
env: options.env,
|
|
272
|
+
...(options.provider ? { provider: options.provider } : {}),
|
|
273
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
266
274
|
});
|
|
267
275
|
autoCompactFailuresBySession.delete(options.failureKey);
|
|
268
276
|
return {
|
|
@@ -271,7 +279,7 @@ function performAutoCompactSession(options) {
|
|
|
271
279
|
estimatedTokens: result.estimatedTokens,
|
|
272
280
|
sourceEventCount: result.sourceEventCount,
|
|
273
281
|
trigger: 'auto',
|
|
274
|
-
strategy:
|
|
282
|
+
strategy: result.strategy,
|
|
275
283
|
events: readSessionEvents(options.cwd, options.sessionId),
|
|
276
284
|
};
|
|
277
285
|
}
|
|
@@ -280,6 +288,161 @@ function performAutoCompactSession(options) {
|
|
|
280
288
|
return { compacted: false };
|
|
281
289
|
}
|
|
282
290
|
}
|
|
291
|
+
export async function compactCurrentSession(options) {
|
|
292
|
+
const env = options.env ?? process.env;
|
|
293
|
+
const loaded = loadRemiConfig({ cwd: options.cwd });
|
|
294
|
+
const strategy = options.strategy ?? loaded.config.agent?.compactStrategy ?? 'deterministic';
|
|
295
|
+
const trigger = options.trigger ?? 'manual';
|
|
296
|
+
const maxSummaryChars = options.maxSummaryChars;
|
|
297
|
+
if (strategy === 'model') {
|
|
298
|
+
const summary = await tryBuildModelCompactSummary({
|
|
299
|
+
cwd: options.cwd,
|
|
300
|
+
sessionId: options.sessionId,
|
|
301
|
+
env,
|
|
302
|
+
loadedConfig: loaded.config,
|
|
303
|
+
...(options.provider ? { provider: options.provider } : {}),
|
|
304
|
+
...(options.modelOverrides ? { modelOverrides: options.modelOverrides } : {}),
|
|
305
|
+
...(options.responseStyle ? { responseStyle: options.responseStyle } : {}),
|
|
306
|
+
...(maxSummaryChars !== undefined ? { maxSummaryChars } : {}),
|
|
307
|
+
...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
|
|
308
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
309
|
+
});
|
|
310
|
+
if (summary && summary.trim().length > 0) {
|
|
311
|
+
return compactSession(options.cwd, options.sessionId, {
|
|
312
|
+
trigger,
|
|
313
|
+
strategy: 'model',
|
|
314
|
+
updateSessionMemory: false,
|
|
315
|
+
...(maxSummaryChars !== undefined ? { maxSummaryChars } : {}),
|
|
316
|
+
summaryOverride: summary,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return compactSession(options.cwd, options.sessionId, {
|
|
321
|
+
trigger,
|
|
322
|
+
strategy: 'deterministic',
|
|
323
|
+
updateSessionMemory: false,
|
|
324
|
+
...(maxSummaryChars !== undefined ? { maxSummaryChars } : {}),
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
async function tryBuildModelCompactSummary(options) {
|
|
328
|
+
try {
|
|
329
|
+
const router = createModelRouter(options.loadedConfig, options.env);
|
|
330
|
+
for (const [role, modelAlias] of Object.entries(options.modelOverrides ?? {})) {
|
|
331
|
+
if (modelAlias) {
|
|
332
|
+
router.switchRoleModel(role, modelAlias);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const resolved = router.resolve('compact');
|
|
336
|
+
if (resolved.apiKeyStatus === 'missing') {
|
|
337
|
+
return undefined;
|
|
338
|
+
}
|
|
339
|
+
const providerConfig = options.loadedConfig.providers?.[resolved.providerAlias];
|
|
340
|
+
if (!providerConfig && !options.provider) {
|
|
341
|
+
return undefined;
|
|
342
|
+
}
|
|
343
|
+
const provider = options.provider ?? createProviderClient(providerConfig, options.env);
|
|
344
|
+
const events = readSessionEvents(options.cwd, options.sessionId);
|
|
345
|
+
const activeEvents = eventsAfterLatestCompact(events);
|
|
346
|
+
if (activeEvents.length === 0) {
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
349
|
+
const sessionMemorySummary = readSessionMemory(options.cwd, options.sessionId)?.content;
|
|
350
|
+
const budget = createContextBudget(resolved, {}, options.loadedConfig.agent);
|
|
351
|
+
const sourceBudgetTokens = Math.max(2_048, Math.floor(budget.effectiveInputBudgetTokens * 0.65));
|
|
352
|
+
const sourceText = buildModelCompactSource({
|
|
353
|
+
events,
|
|
354
|
+
maxChars: tokenBudgetToChars(sourceBudgetTokens),
|
|
355
|
+
...(sessionMemorySummary ? { sessionMemorySummary } : {}),
|
|
356
|
+
});
|
|
357
|
+
const responseStyle = resolveResponseStyle(options.responseStyle ?? options.loadedConfig.responseStyle ?? defaultResponseStyleId);
|
|
358
|
+
const instruction = readPromptFile('compact-summary.md');
|
|
359
|
+
const messages = [
|
|
360
|
+
{ role: 'system', content: buildBaseSystemPrompt(responseStyle.id, undefined) },
|
|
361
|
+
{ role: 'user', content: sourceText },
|
|
362
|
+
{ role: 'user', content: instruction },
|
|
363
|
+
];
|
|
364
|
+
let summary = '';
|
|
365
|
+
for await (const chunk of provider.streamChat(messages, resolved, {
|
|
366
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
367
|
+
timeoutMs: options.timeoutMs ?? defaultTimeoutMs,
|
|
368
|
+
})) {
|
|
369
|
+
if (typeof chunk === 'string') {
|
|
370
|
+
summary += chunk;
|
|
371
|
+
}
|
|
372
|
+
else if (chunk?.type === 'tool_call' || chunk?.type === 'tool_call_delta' || chunk?.type === 'tool_calls_done') {
|
|
373
|
+
return undefined;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return sanitizeModelCompactSummary(summary, options.maxSummaryChars);
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
return undefined;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
function buildModelCompactSource(options) {
|
|
383
|
+
const priorSummary = latestCompactSummary(options.events);
|
|
384
|
+
const activeEvents = eventsAfterLatestCompact(options.events);
|
|
385
|
+
const sections = [
|
|
386
|
+
'# Session Segment To Compact',
|
|
387
|
+
'',
|
|
388
|
+
...(priorSummary ? ['## Prior Compact Summary', '', truncateText(priorSummary, 2_000), ''] : []),
|
|
389
|
+
...(options.sessionMemorySummary ? ['## Session Memory', '', truncateText(options.sessionMemorySummary, 2_000), ''] : []),
|
|
390
|
+
'## Active Transcript Since Latest Compact',
|
|
391
|
+
'',
|
|
392
|
+
...activeEvents.map(formatCompactSourceEvent),
|
|
393
|
+
];
|
|
394
|
+
return truncateText(sections.join('\n'), options.maxChars);
|
|
395
|
+
}
|
|
396
|
+
function formatCompactSourceEvent(event) {
|
|
397
|
+
if (event.type === 'user') {
|
|
398
|
+
return `- user: ${compactText(event.content, 600)}`;
|
|
399
|
+
}
|
|
400
|
+
if (event.type === 'assistant') {
|
|
401
|
+
return `- assistant: ${compactText(event.content, 600)}`;
|
|
402
|
+
}
|
|
403
|
+
if (event.type === 'system') {
|
|
404
|
+
return `- system ${event.level}: ${compactText(event.content, 300)}`;
|
|
405
|
+
}
|
|
406
|
+
if (event.type === 'skill_use') {
|
|
407
|
+
return `- skill loaded ${event.name}: ${event.filePath}`;
|
|
408
|
+
}
|
|
409
|
+
if (event.type === 'pending_action') {
|
|
410
|
+
const command = event.completionCommand ?? event.command ?? 'external action';
|
|
411
|
+
return `- pending action from ${event.sourceToolName}: ${compactText(command, 300)}`;
|
|
412
|
+
}
|
|
413
|
+
if (event.type === 'tool_call') {
|
|
414
|
+
return `- tool call ${event.toolName}: ${compactText(JSON.stringify(event.input), 300)}`;
|
|
415
|
+
}
|
|
416
|
+
if (event.type === 'tool_result') {
|
|
417
|
+
const status = event.ok ? 'ok' : 'failed';
|
|
418
|
+
const artifact = event.artifact ? ` artifact=${event.artifact.path} (${event.artifact.bytes} bytes)` : '';
|
|
419
|
+
return `- tool result ${event.toolName} ${status}: ${compactText(event.summary, 500)}${artifact}`;
|
|
420
|
+
}
|
|
421
|
+
if (event.type === 'compact') {
|
|
422
|
+
return `- compact boundary ${event.trigger ?? 'manual'} ${event.strategy ?? 'deterministic'}: ${compactText(event.summary, 400)}`;
|
|
423
|
+
}
|
|
424
|
+
if (event.type === 'cache_diagnostic') {
|
|
425
|
+
return `- cache diagnostic: hit=${Math.round(event.usage.cacheHitRate * 100)}% miss=${event.inference.missReason} changed=${event.inference.changedLayers.join(',') || 'none'}`;
|
|
426
|
+
}
|
|
427
|
+
return '- unknown event';
|
|
428
|
+
}
|
|
429
|
+
function sanitizeModelCompactSummary(summary, maxSummaryChars) {
|
|
430
|
+
const trimmed = summary.trim();
|
|
431
|
+
if (trimmed.length === 0) {
|
|
432
|
+
return undefined;
|
|
433
|
+
}
|
|
434
|
+
const sanitized = trimmed
|
|
435
|
+
.replace(/```(?:SEARCH\/REPLACE|search\/replace|diff)[\s\S]*?```/g, '[omitted edit block]')
|
|
436
|
+
.replace(/<tool_call[\s\S]*?<\/tool_call>/g, '[omitted tool call]')
|
|
437
|
+
.trim();
|
|
438
|
+
return truncateText(sanitized, maxSummaryChars ?? 6_000);
|
|
439
|
+
}
|
|
440
|
+
function truncateText(text, maxChars) {
|
|
441
|
+
if (text.length <= maxChars) {
|
|
442
|
+
return text;
|
|
443
|
+
}
|
|
444
|
+
return `${text.slice(0, Math.max(0, maxChars - 24)).trimEnd()}\n... truncated ...`;
|
|
445
|
+
}
|
|
283
446
|
function isAutoCompactEnabled(config, runtimeConfig, env) {
|
|
284
447
|
if (env['REMI_DISABLE_AUTO_COMPACT'] === '1' || env['DISABLE_COMPACT'] === '1') {
|
|
285
448
|
return false;
|
|
@@ -424,6 +587,7 @@ export async function* runChatTurn(input, config = {}) {
|
|
|
424
587
|
yield { type: 'error', sessionId: sessionStore.sessionId, message };
|
|
425
588
|
return;
|
|
426
589
|
}
|
|
590
|
+
const provider = config.provider ?? createProviderClient(providerConfig, env);
|
|
427
591
|
const contextBudget = createContextBudget(resolved, config, loaded.config.agent);
|
|
428
592
|
const tokenEstimatorResolution = tokenEstimatorRegistry.resolve(resolved);
|
|
429
593
|
let sessionEvents = config.sessionStore ? [] : readSessionEvents(cwd, sessionStore.sessionId);
|
|
@@ -445,20 +609,25 @@ export async function* runChatTurn(input, config = {}) {
|
|
|
445
609
|
sessionId: sessionStore.sessionId,
|
|
446
610
|
failureKey: autoCompactDecision.failureKey,
|
|
447
611
|
maxSummaryChars: autoCompactDecision.maxSummaryChars,
|
|
612
|
+
strategy: config.compactStrategy ?? loaded.config.agent?.compactStrategy ?? 'deterministic',
|
|
613
|
+
env,
|
|
614
|
+
provider: config.compactProvider ?? provider,
|
|
615
|
+
...(config.signal ? { signal: config.signal } : {}),
|
|
448
616
|
});
|
|
449
|
-
|
|
617
|
+
const autoCompactResult = await autoCompact;
|
|
618
|
+
if (autoCompactResult.compacted) {
|
|
450
619
|
yield {
|
|
451
620
|
type: 'compact',
|
|
452
|
-
summary:
|
|
453
|
-
estimatedTokens:
|
|
454
|
-
sourceEventCount:
|
|
621
|
+
summary: autoCompactResult.summary,
|
|
622
|
+
estimatedTokens: autoCompactResult.estimatedTokens,
|
|
623
|
+
sourceEventCount: autoCompactResult.sourceEventCount,
|
|
455
624
|
automatic: true,
|
|
456
|
-
trigger:
|
|
457
|
-
strategy:
|
|
625
|
+
trigger: autoCompactResult.trigger,
|
|
626
|
+
strategy: autoCompactResult.strategy,
|
|
458
627
|
};
|
|
459
|
-
sessionEvents =
|
|
628
|
+
sessionEvents = autoCompactResult.events;
|
|
460
629
|
}
|
|
461
|
-
yield { type: 'compact_end', automatic: true, trigger: 'auto', ok:
|
|
630
|
+
yield { type: 'compact_end', automatic: true, trigger: 'auto', ok: autoCompactResult.compacted };
|
|
462
631
|
}
|
|
463
632
|
}
|
|
464
633
|
const standaloneCurrentTask = isStandaloneRepositoryInitPrompt(input);
|
|
@@ -518,11 +687,13 @@ export async function* runChatTurn(input, config = {}) {
|
|
|
518
687
|
: undefined;
|
|
519
688
|
const recentFilesystemContext = toolDefinitions.length > 0 && !standaloneCurrentTask ? buildRecentFilesystemContext(sessionEventsForContext, cwd, env) : undefined;
|
|
520
689
|
const recentFailedToolContext = toolDefinitions.length > 0 && !standaloneCurrentTask ? buildRecentFailedToolContext(sessionEventsForContext) : undefined;
|
|
690
|
+
const recentContinuationContext = toolDefinitions.length > 0 && !standaloneCurrentTask ? buildRecentContinuationContext(sessionEventsForContext, cwd, env) : undefined;
|
|
521
691
|
const projectInstructionsContext = buildProjectInstructionsContext(cwd);
|
|
692
|
+
const recentLoadedSkills = toolDefinitions.length > 0 && !standaloneCurrentTask ? loadRecentSessionSkills(sessionEventsForContext, skillLoadOptions) : [];
|
|
522
693
|
const loadedSkills = [];
|
|
523
|
-
const loadedSkillNames = new Set();
|
|
524
|
-
const skillContext = formatSkillIndexContext(skillIndex);
|
|
525
|
-
const projectInstructions = mergeProjectInstructionSections(projectInstructionsContext.content,
|
|
694
|
+
const loadedSkillNames = new Set(recentLoadedSkills.map(skill => normalizeSkillNameForRuntime(skill.name)));
|
|
695
|
+
const skillContext = formatSkillIndexContext(skillIndex, recentLoadedSkills);
|
|
696
|
+
const projectInstructions = mergeProjectInstructionSections(projectInstructionsContext.content, skillContext);
|
|
526
697
|
const memoryRecall = config.disableMemory || !shouldOfferMemoryForInput(input)
|
|
527
698
|
? emptyMemoryRecall(input, config.maxMemoryItems ?? defaultMaxMemoryItems)
|
|
528
699
|
: recallRelevantMemories(cwd, input, {
|
|
@@ -530,11 +701,13 @@ export async function* runChatTurn(input, config = {}) {
|
|
|
530
701
|
maxBodyChars: config.maxMemoryChars ?? defaultMaxMemoryChars,
|
|
531
702
|
});
|
|
532
703
|
const toolUsePrompts = toolDefinitions.length > 0 ? buildToolUseSystemPrompts(cwd, env, planPromptMode(config.planMode, planFirstRequirement), responseLanguage) : undefined;
|
|
704
|
+
const runtimePrompt = mergeProjectInstructionSections(toolUsePrompts?.runtimePrompt, recentFilesystemContext, recentContinuationContext, recentFailedToolContext);
|
|
533
705
|
const providerContext = buildProviderContext({
|
|
534
706
|
systemPrompt: buildBaseSystemPrompt(responseStyle.id, responseLanguage),
|
|
535
|
-
...(toolUsePrompts ? { toolPrompt: toolUsePrompts.toolPrompt
|
|
707
|
+
...(toolUsePrompts ? { toolPrompt: toolUsePrompts.toolPrompt } : {}),
|
|
708
|
+
...(runtimePrompt ? { runtimePrompt } : {}),
|
|
536
709
|
...(projectInstructions ? { projectInstructions } : {}),
|
|
537
|
-
projectInstructionsSource: projectInstructionSource(projectInstructionsContext
|
|
710
|
+
projectInstructionsSource: projectInstructionSource(projectInstructionsContext),
|
|
538
711
|
memories: memoryRecall.memories,
|
|
539
712
|
...(sessionMemorySummary ? { sessionMemorySummary } : {}),
|
|
540
713
|
...(compactSummary ? { compactSummary } : {}),
|
|
@@ -554,6 +727,7 @@ export async function* runChatTurn(input, config = {}) {
|
|
|
554
727
|
...(tokenEstimatorResolution.reason ? { tokenEstimatorFallbackReason: tokenEstimatorResolution.reason } : {}),
|
|
555
728
|
});
|
|
556
729
|
const messages = providerContext.messages;
|
|
730
|
+
const previousCacheDiagnostic = latestCacheDiagnostic(sessionEvents);
|
|
557
731
|
if (planExecutionApproval) {
|
|
558
732
|
messages.push({ role: 'user', content: buildPlanExecutionApprovalPrompt(input, planExecutionApproval, responseLanguage) });
|
|
559
733
|
}
|
|
@@ -589,7 +763,6 @@ export async function* runChatTurn(input, config = {}) {
|
|
|
589
763
|
yield detail ? { ...event, detail } : event;
|
|
590
764
|
}
|
|
591
765
|
}
|
|
592
|
-
const provider = config.provider ?? createProviderClient(providerConfig, env);
|
|
593
766
|
const pendingHookRuntimeEvents = [];
|
|
594
767
|
let hookCommandSequence = 0;
|
|
595
768
|
const requestToolPermissionWithHooks = async (request, decision) => {
|
|
@@ -753,6 +926,10 @@ export async function* runChatTurn(input, config = {}) {
|
|
|
753
926
|
fullToolRegistry,
|
|
754
927
|
toolDefinitions,
|
|
755
928
|
fullToolDefinitions,
|
|
929
|
+
contextTrace: providerContext.trace,
|
|
930
|
+
tokenEstimator: tokenEstimatorResolution.estimator,
|
|
931
|
+
tokenEstimateProfile: tokenEstimatorResolution.profile,
|
|
932
|
+
...(previousCacheDiagnostic ? { previousCacheDiagnostic } : {}),
|
|
756
933
|
skillLoadOptions,
|
|
757
934
|
loadedSkillNames,
|
|
758
935
|
planFirstRequirement,
|
|
@@ -773,12 +950,13 @@ export async function* runChatTurn(input, config = {}) {
|
|
|
773
950
|
}
|
|
774
951
|
}
|
|
775
952
|
export async function* runAgentLoop(scope) {
|
|
776
|
-
const { input, config, cwd, responseLanguage, provider, resolved, messages, sessionStore, modelEvent, responseStyleEvent, maxTurns, maxToolCalls, toolExecutor, fullToolRegistry, fullToolDefinitions, skillLoadOptions, loadedSkillNames, planFirstRequirement, planExecutionApproval, requiredMutationTools, shouldUseSkillDecisionGuard, hasStopHooks, recalledMemoryRefs, createToolExecutionContext, runGenericHooks, runStopHooks, runStopFailureHooks, flushPendingHookRuntimeEvents, } = scope;
|
|
953
|
+
const { input, config, cwd, responseLanguage, provider, resolved, messages, sessionStore, modelEvent, responseStyleEvent, maxTurns, maxToolCalls, toolExecutor, fullToolRegistry, fullToolDefinitions, contextTrace, tokenEstimator, tokenEstimateProfile, skillLoadOptions, loadedSkillNames, planFirstRequirement, planExecutionApproval, requiredMutationTools, shouldUseSkillDecisionGuard, hasStopHooks, recalledMemoryRefs, createToolExecutionContext, runGenericHooks, runStopHooks, runStopFailureHooks, flushPendingHookRuntimeEvents, } = scope;
|
|
777
954
|
let activeToolRegistry = scope.activeToolRegistry;
|
|
778
955
|
let toolDefinitions = scope.toolDefinitions;
|
|
779
956
|
let assistantText = '';
|
|
780
957
|
let usage;
|
|
781
958
|
let publishedUsage;
|
|
959
|
+
let previousCacheDiagnostic = scope.previousCacheDiagnostic;
|
|
782
960
|
let modelTurns = 0;
|
|
783
961
|
let toolCallsUsed = 0;
|
|
784
962
|
let mutationGuardRetries = 0;
|
|
@@ -790,6 +968,7 @@ export async function* runAgentLoop(scope) {
|
|
|
790
968
|
let postActionApprovalGuardRetries = 0;
|
|
791
969
|
let toolArgumentRetries = 0;
|
|
792
970
|
let skillDecisionGuardPrompted = false;
|
|
971
|
+
let activeDraftPublished = false;
|
|
793
972
|
const planFirstState = createPlanFirstGuardState(planFirstRequirement);
|
|
794
973
|
const mutationState = createMutationGuardState(requiredMutationTools, input);
|
|
795
974
|
const verificationState = createVerificationGuardState();
|
|
@@ -802,6 +981,7 @@ export async function* runAgentLoop(scope) {
|
|
|
802
981
|
modelTurns += 1;
|
|
803
982
|
let turnText = '';
|
|
804
983
|
let turnReasoningText = '';
|
|
984
|
+
let requestUsage;
|
|
805
985
|
const requestedToolCalls = [];
|
|
806
986
|
const bufferTurnText = shouldBufferMutationText(mutationState) ||
|
|
807
987
|
shouldGuardMutationClaims(mutationState) ||
|
|
@@ -810,6 +990,24 @@ export async function* runAgentLoop(scope) {
|
|
|
810
990
|
shouldBufferSkillDecisionGuard(shouldUseSkillDecisionGuard, loadedSkillNames, skillDecisionGuardPrompted) ||
|
|
811
991
|
hasCurrentTurnActionEvidence(mutationState, verificationState) ||
|
|
812
992
|
hasStopHooks;
|
|
993
|
+
const previewBufferedDeltas = Boolean(config.previewBufferedDeltas && bufferTurnText);
|
|
994
|
+
let turnDraftPublished = false;
|
|
995
|
+
const discardTurnDraft = function* () {
|
|
996
|
+
if (!turnDraftPublished) {
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
turnDraftPublished = false;
|
|
1000
|
+
activeDraftPublished = false;
|
|
1001
|
+
yield { type: 'draft_reset' };
|
|
1002
|
+
};
|
|
1003
|
+
const commitTurnDraft = function* () {
|
|
1004
|
+
if (!turnDraftPublished) {
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
turnDraftPublished = false;
|
|
1008
|
+
activeDraftPublished = false;
|
|
1009
|
+
yield { type: 'draft_commit' };
|
|
1010
|
+
};
|
|
813
1011
|
for await (const chunk of provider.streamChat(messages, resolved, {
|
|
814
1012
|
...(config.signal ? { signal: config.signal } : {}),
|
|
815
1013
|
timeoutMs: config.timeoutMs ?? defaultTimeoutMs,
|
|
@@ -817,6 +1015,7 @@ export async function* runAgentLoop(scope) {
|
|
|
817
1015
|
})) {
|
|
818
1016
|
if (typeof chunk !== 'string') {
|
|
819
1017
|
if (chunk?.type === 'usage') {
|
|
1018
|
+
requestUsage = mergeTokenUsage(requestUsage, chunk.usage);
|
|
820
1019
|
const nextUsage = mergeTokenUsage(usage, chunk.usage);
|
|
821
1020
|
const usageDelta = tokenUsageDelta(publishedUsage, nextUsage);
|
|
822
1021
|
usage = nextUsage;
|
|
@@ -840,27 +1039,55 @@ export async function* runAgentLoop(scope) {
|
|
|
840
1039
|
if (!bufferTurnText) {
|
|
841
1040
|
yield { type: 'delta', text: chunk };
|
|
842
1041
|
}
|
|
1042
|
+
else if (previewBufferedDeltas) {
|
|
1043
|
+
turnDraftPublished = true;
|
|
1044
|
+
activeDraftPublished = true;
|
|
1045
|
+
yield { type: 'draft_delta', text: chunk };
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
if (requestUsage) {
|
|
1049
|
+
const cacheDiagnostic = buildCacheDiagnostic({
|
|
1050
|
+
messages,
|
|
1051
|
+
toolDefinitions,
|
|
1052
|
+
contextTrace,
|
|
1053
|
+
model: resolved,
|
|
1054
|
+
turn: modelTurns,
|
|
1055
|
+
usage: requestUsage,
|
|
1056
|
+
...(previousCacheDiagnostic ? { previous: previousCacheDiagnostic } : {}),
|
|
1057
|
+
tokenEstimator,
|
|
1058
|
+
tokenEstimateProfile,
|
|
1059
|
+
});
|
|
1060
|
+
sessionStore.append(cacheDiagnostic);
|
|
1061
|
+
previousCacheDiagnostic = {
|
|
1062
|
+
...cacheDiagnostic,
|
|
1063
|
+
sessionId: sessionStore.sessionId,
|
|
1064
|
+
timestamp: new Date().toISOString(),
|
|
1065
|
+
};
|
|
843
1066
|
}
|
|
844
1067
|
if (requestedToolCalls.length === 0) {
|
|
845
1068
|
if (shouldRetryMissingPlanFirst(planFirstState, planFirstGuardRetries, modelTurns, maxTurns)) {
|
|
846
1069
|
planFirstGuardRetries += 1;
|
|
1070
|
+
yield* discardTurnDraft();
|
|
847
1071
|
messages.push({ role: 'user', content: buildPlanFirstGuardPrompt(input, planFirstState, responseLanguage) });
|
|
848
1072
|
continue;
|
|
849
1073
|
}
|
|
850
1074
|
if (shouldFailMissingPlanFirst(planFirstState)) {
|
|
851
1075
|
const message = planFirstGuardFailureMessage(planFirstState);
|
|
852
1076
|
sessionStore.append({ type: 'system', level: 'error', content: message });
|
|
1077
|
+
yield* discardTurnDraft();
|
|
853
1078
|
yield { type: 'error', sessionId: sessionStore.sessionId, message };
|
|
854
1079
|
return;
|
|
855
1080
|
}
|
|
856
1081
|
if (shouldRetryMissingMutationTool(mutationState, mutationGuardRetries, modelTurns, maxTurns)) {
|
|
857
1082
|
mutationGuardRetries += 1;
|
|
1083
|
+
yield* discardTurnDraft();
|
|
858
1084
|
messages.push({ role: 'user', content: buildMutationGuardPrompt(input, mutationState, responseLanguage) });
|
|
859
1085
|
continue;
|
|
860
1086
|
}
|
|
861
1087
|
if (shouldFailMissingMutationTool(mutationState)) {
|
|
862
1088
|
const message = mutationGuardFailureMessage(mutationState);
|
|
863
1089
|
sessionStore.append({ type: 'system', level: 'error', content: message });
|
|
1090
|
+
yield* discardTurnDraft();
|
|
864
1091
|
yield { type: 'error', sessionId: sessionStore.sessionId, message };
|
|
865
1092
|
return;
|
|
866
1093
|
}
|
|
@@ -868,11 +1095,13 @@ export async function* runAgentLoop(scope) {
|
|
|
868
1095
|
if (mutationClaimIssue) {
|
|
869
1096
|
if (mutationClaimGuardRetries < defaultMutationGuardRetries && modelTurns < maxTurns) {
|
|
870
1097
|
mutationClaimGuardRetries += 1;
|
|
1098
|
+
yield* discardTurnDraft();
|
|
871
1099
|
messages.push({ role: 'user', content: buildMutationClaimGuardPrompt(input, turnText, mutationState, mutationClaimIssue, responseLanguage) });
|
|
872
1100
|
continue;
|
|
873
1101
|
}
|
|
874
1102
|
const message = mutationClaimGuardFailureMessage(mutationClaimIssue);
|
|
875
1103
|
sessionStore.append({ type: 'system', level: 'error', content: message });
|
|
1104
|
+
yield* discardTurnDraft();
|
|
876
1105
|
yield { type: 'error', sessionId: sessionStore.sessionId, message };
|
|
877
1106
|
return;
|
|
878
1107
|
}
|
|
@@ -880,11 +1109,13 @@ export async function* runAgentLoop(scope) {
|
|
|
880
1109
|
if (verificationClaimIssue) {
|
|
881
1110
|
if (verificationClaimGuardRetries < defaultVerificationClaimGuardRetries && modelTurns < maxTurns) {
|
|
882
1111
|
verificationClaimGuardRetries += 1;
|
|
1112
|
+
yield* discardTurnDraft();
|
|
883
1113
|
messages.push({ role: 'user', content: buildVerificationClaimGuardPrompt(input, turnText, verificationState, responseLanguage) });
|
|
884
1114
|
continue;
|
|
885
1115
|
}
|
|
886
1116
|
const message = verificationClaimGuardFailureMessage(verificationState);
|
|
887
1117
|
sessionStore.append({ type: 'system', level: 'error', content: message });
|
|
1118
|
+
yield* discardTurnDraft();
|
|
888
1119
|
yield { type: 'error', sessionId: sessionStore.sessionId, message };
|
|
889
1120
|
return;
|
|
890
1121
|
}
|
|
@@ -892,21 +1123,25 @@ export async function* runAgentLoop(scope) {
|
|
|
892
1123
|
if (planFirstClaimIssue) {
|
|
893
1124
|
if (planFirstClaimGuardRetries < defaultPlanFirstGuardRetries && modelTurns < maxTurns) {
|
|
894
1125
|
planFirstClaimGuardRetries += 1;
|
|
1126
|
+
yield* discardTurnDraft();
|
|
895
1127
|
messages.push({ role: 'user', content: buildPlanFirstClaimGuardPrompt(input, turnText, planFirstState, planFirstClaimIssue, responseLanguage) });
|
|
896
1128
|
continue;
|
|
897
1129
|
}
|
|
898
1130
|
const message = planFirstClaimGuardFailureMessage(planFirstClaimIssue);
|
|
899
1131
|
sessionStore.append({ type: 'system', level: 'error', content: message });
|
|
1132
|
+
yield* discardTurnDraft();
|
|
900
1133
|
yield { type: 'error', sessionId: sessionStore.sessionId, message };
|
|
901
1134
|
return;
|
|
902
1135
|
}
|
|
903
1136
|
if (shouldRetryPlanExecutionApproval(planExecutionApproval, turnText, planExecutionApprovalRetries, modelTurns, maxTurns)) {
|
|
904
1137
|
planExecutionApprovalRetries += 1;
|
|
1138
|
+
yield* discardTurnDraft();
|
|
905
1139
|
messages.push({ role: 'user', content: buildPlanExecutionApprovalPrompt(input, planExecutionApproval, responseLanguage, true) });
|
|
906
1140
|
continue;
|
|
907
1141
|
}
|
|
908
1142
|
if (shouldRetryPostActionApprovalDraft(turnText, mutationState, verificationState, postActionApprovalGuardRetries, modelTurns, maxTurns)) {
|
|
909
1143
|
postActionApprovalGuardRetries += 1;
|
|
1144
|
+
yield* discardTurnDraft();
|
|
910
1145
|
messages.push({ role: 'user', content: buildPostActionApprovalGuardPrompt(input, turnText, mutationState, verificationState, responseLanguage) });
|
|
911
1146
|
continue;
|
|
912
1147
|
}
|
|
@@ -918,19 +1153,26 @@ export async function* runAgentLoop(scope) {
|
|
|
918
1153
|
if (stopHookResult.blockingErrors.length > 0) {
|
|
919
1154
|
if (stopHookBlockCount < defaultStopHookBlockRetries && modelTurns < maxTurns) {
|
|
920
1155
|
stopHookBlockCount += 1;
|
|
1156
|
+
yield* discardTurnDraft();
|
|
921
1157
|
messages.push({ role: 'assistant', content: turnText });
|
|
922
1158
|
messages.push({ role: 'user', content: buildStopHookContinuationPrompt(stopHookResult, responseLanguage) });
|
|
923
1159
|
continue;
|
|
924
1160
|
}
|
|
925
1161
|
const message = stopHookFailureMessage(stopHookResult);
|
|
926
1162
|
sessionStore.append({ type: 'system', level: 'error', content: message });
|
|
1163
|
+
yield* discardTurnDraft();
|
|
927
1164
|
yield { type: 'error', sessionId: sessionStore.sessionId, message };
|
|
928
1165
|
return;
|
|
929
1166
|
}
|
|
930
1167
|
}
|
|
931
1168
|
assistantText += turnText;
|
|
932
1169
|
if (bufferTurnText && turnText.length > 0) {
|
|
933
|
-
|
|
1170
|
+
if (turnDraftPublished) {
|
|
1171
|
+
yield* commitTurnDraft();
|
|
1172
|
+
}
|
|
1173
|
+
else {
|
|
1174
|
+
yield { type: 'delta', text: turnText };
|
|
1175
|
+
}
|
|
934
1176
|
}
|
|
935
1177
|
sessionStore.append({
|
|
936
1178
|
type: 'assistant',
|
|
@@ -952,9 +1194,11 @@ export async function* runAgentLoop(scope) {
|
|
|
952
1194
|
}
|
|
953
1195
|
if (shouldRetrySkillDecisionGuard(shouldUseSkillDecisionGuard, loadedSkillNames, skillDecisionGuardPrompted, requestedToolCalls, modelTurns, maxTurns)) {
|
|
954
1196
|
skillDecisionGuardPrompted = true;
|
|
1197
|
+
yield* discardTurnDraft();
|
|
955
1198
|
messages.push({ role: 'user', content: buildSkillDecisionGuardPrompt(input, requestedToolCalls, responseLanguage) });
|
|
956
1199
|
continue;
|
|
957
1200
|
}
|
|
1201
|
+
yield* commitTurnDraft();
|
|
958
1202
|
messages.push({
|
|
959
1203
|
role: 'assistant',
|
|
960
1204
|
content: turnText,
|
|
@@ -1011,6 +1255,10 @@ export async function* runAgentLoop(scope) {
|
|
|
1011
1255
|
const transcriptArtifacts = hiddenPlanFirstToolCall
|
|
1012
1256
|
? new Map()
|
|
1013
1257
|
: appendToolTranscriptEvents(cwd, sessionStore, outcome.transcriptEvents);
|
|
1258
|
+
const pendingAction = hiddenPlanFirstToolCall ? undefined : pendingActionFromToolResult(call, outcome.result);
|
|
1259
|
+
if (pendingAction) {
|
|
1260
|
+
sessionStore.append(pendingAction);
|
|
1261
|
+
}
|
|
1014
1262
|
if (!hiddenPlanFirstToolCall) {
|
|
1015
1263
|
recordMutationToolOutcome(mutationState, outcome.toolName, outcome.result, call);
|
|
1016
1264
|
recordVerificationToolOutcome(verificationState, outcome.toolName, outcome.result);
|
|
@@ -1086,6 +1334,13 @@ export async function* runAgentLoop(scope) {
|
|
|
1086
1334
|
const skillKey = normalizeSkillNameForRuntime(outcome.skill.name);
|
|
1087
1335
|
if (!loadedSkillNames.has(skillKey)) {
|
|
1088
1336
|
loadedSkillNames.add(skillKey);
|
|
1337
|
+
sessionStore.append({
|
|
1338
|
+
type: 'skill_use',
|
|
1339
|
+
name: outcome.skill.name,
|
|
1340
|
+
displayName: outcome.skill.displayName,
|
|
1341
|
+
source: outcome.skill.source,
|
|
1342
|
+
filePath: outcome.skill.filePath,
|
|
1343
|
+
});
|
|
1089
1344
|
yield {
|
|
1090
1345
|
type: 'skill_use',
|
|
1091
1346
|
name: outcome.skill.name,
|
|
@@ -1219,6 +1474,10 @@ export async function* runAgentLoop(scope) {
|
|
|
1219
1474
|
}
|
|
1220
1475
|
catch (error) {
|
|
1221
1476
|
const message = safeErrorMessage(error);
|
|
1477
|
+
if (activeDraftPublished) {
|
|
1478
|
+
activeDraftPublished = false;
|
|
1479
|
+
yield { type: 'draft_reset' };
|
|
1480
|
+
}
|
|
1222
1481
|
if (isLlmRequestTimeoutMessage(message) && hasSuccessfulToolResult) {
|
|
1223
1482
|
const fallbackText = llmTimeoutAfterSuccessfulToolMessage(responseLanguage);
|
|
1224
1483
|
assistantText = assistantText ? `${assistantText}\n\n${fallbackText}` : fallbackText;
|
|
@@ -1549,8 +1808,8 @@ function mergeProjectInstructionSections(...sectionsToMerge) {
|
|
|
1549
1808
|
const sections = sectionsToMerge.filter((section) => Boolean(section && section.trim().length > 0));
|
|
1550
1809
|
return sections.length > 0 ? sections.join('\n\n') : undefined;
|
|
1551
1810
|
}
|
|
1552
|
-
function projectInstructionSource(projectInstructions
|
|
1553
|
-
return
|
|
1811
|
+
function projectInstructionSource(projectInstructions) {
|
|
1812
|
+
return projectInstructions.source;
|
|
1554
1813
|
}
|
|
1555
1814
|
function buildRecentFilesystemContext(events, cwd, env) {
|
|
1556
1815
|
const recentEvents = events.slice(-maxFilesystemContextEvents);
|
|
@@ -1608,6 +1867,162 @@ function buildRecentFilesystemContext(events, cwd, env) {
|
|
|
1608
1867
|
'For commands that must run inside one of these projects, set the tool cwd to the mapped absolute project root.',
|
|
1609
1868
|
].join('\n');
|
|
1610
1869
|
}
|
|
1870
|
+
function loadRecentSessionSkills(events, options) {
|
|
1871
|
+
const loadedSkills = [];
|
|
1872
|
+
const seen = new Set();
|
|
1873
|
+
for (let index = events.length - 1; index >= 0 && loadedSkills.length < maxRecentLoadedSkills; index -= 1) {
|
|
1874
|
+
const event = events[index];
|
|
1875
|
+
if (!event || event.type !== 'skill_use') {
|
|
1876
|
+
continue;
|
|
1877
|
+
}
|
|
1878
|
+
const key = normalizeSkillNameForRuntime(event.name);
|
|
1879
|
+
if (seen.has(key)) {
|
|
1880
|
+
continue;
|
|
1881
|
+
}
|
|
1882
|
+
seen.add(key);
|
|
1883
|
+
const skill = loadSkillContent({
|
|
1884
|
+
...options,
|
|
1885
|
+
name: event.name,
|
|
1886
|
+
maxContentChars: maxLoadedSkillContentChars,
|
|
1887
|
+
});
|
|
1888
|
+
if (skill) {
|
|
1889
|
+
loadedSkills.unshift(skill);
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
return loadedSkills;
|
|
1893
|
+
}
|
|
1894
|
+
function buildRecentContinuationContext(events, cwd, env) {
|
|
1895
|
+
const recentEvents = events.slice(-maxRecentContinuationEvents);
|
|
1896
|
+
const toolCalls = new Map();
|
|
1897
|
+
const commandFacts = [];
|
|
1898
|
+
const fileExcerpts = [];
|
|
1899
|
+
const pendingActions = [];
|
|
1900
|
+
const home = typeof env.HOME === 'string' && env.HOME.trim().length > 0 ? normalize(resolve(env.HOME)) : undefined;
|
|
1901
|
+
for (const event of recentEvents) {
|
|
1902
|
+
if (event.type === 'tool_call') {
|
|
1903
|
+
toolCalls.set(event.callId, event);
|
|
1904
|
+
continue;
|
|
1905
|
+
}
|
|
1906
|
+
if (event.type === 'tool_result' && event.ok) {
|
|
1907
|
+
const call = toolCalls.get(event.callId);
|
|
1908
|
+
if (!call) {
|
|
1909
|
+
continue;
|
|
1910
|
+
}
|
|
1911
|
+
const commandFact = formatRecentCommandFact(call, event);
|
|
1912
|
+
if (commandFact) {
|
|
1913
|
+
commandFacts.push(commandFact);
|
|
1914
|
+
}
|
|
1915
|
+
const fileExcerpt = formatRecentFileExcerpt(call, event, cwd, home);
|
|
1916
|
+
if (fileExcerpt) {
|
|
1917
|
+
fileExcerpts.push(fileExcerpt);
|
|
1918
|
+
}
|
|
1919
|
+
continue;
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
for (const event of recentEvents) {
|
|
1923
|
+
if (event.type !== 'pending_action') {
|
|
1924
|
+
continue;
|
|
1925
|
+
}
|
|
1926
|
+
const pendingAction = formatPendingActionFact(event);
|
|
1927
|
+
if (pendingAction) {
|
|
1928
|
+
pendingActions.push(pendingAction);
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
const recentCommandFacts = uniqueMostRecent(commandFacts, maxRecentToolFacts);
|
|
1932
|
+
const recentFileExcerpts = uniqueMostRecent(fileExcerpts, maxRecentFileExcerpts);
|
|
1933
|
+
const recentPendingActions = uniqueMostRecent(pendingActions, 2);
|
|
1934
|
+
if (recentCommandFacts.length === 0 && recentFileExcerpts.length === 0 && recentPendingActions.length === 0) {
|
|
1935
|
+
return undefined;
|
|
1936
|
+
}
|
|
1937
|
+
return [
|
|
1938
|
+
'## Recent Tool Continuation Facts',
|
|
1939
|
+
'These records come from recent Remi tool activity in this session. They may be stale if the external state changed after the recorded tool result.',
|
|
1940
|
+
...(recentPendingActions.length > 0
|
|
1941
|
+
? [
|
|
1942
|
+
'',
|
|
1943
|
+
'Pending external actions:',
|
|
1944
|
+
...recentPendingActions,
|
|
1945
|
+
]
|
|
1946
|
+
: []),
|
|
1947
|
+
...(recentCommandFacts.length > 0 ? ['', 'Recent successful commands:', ...recentCommandFacts] : []),
|
|
1948
|
+
...(recentFileExcerpts.length > 0 ? ['', 'Recent file excerpts:', ...recentFileExcerpts] : []),
|
|
1949
|
+
].join('\n');
|
|
1950
|
+
}
|
|
1951
|
+
function formatRecentCommandFact(call, result) {
|
|
1952
|
+
if (call.toolName !== 'run_command' && call.toolName !== 'execute_shell') {
|
|
1953
|
+
return undefined;
|
|
1954
|
+
}
|
|
1955
|
+
const command = typeof call.input['command'] === 'string' ? call.input['command'].trim() : '';
|
|
1956
|
+
if (!command) {
|
|
1957
|
+
return undefined;
|
|
1958
|
+
}
|
|
1959
|
+
return `- ${call.toolName}: ${compactText(command, 220)} => ${compactText(result.summary, 220)}`;
|
|
1960
|
+
}
|
|
1961
|
+
function formatRecentFileExcerpt(call, result, cwd, home) {
|
|
1962
|
+
if (call.toolName !== 'read_file' || !result.artifact) {
|
|
1963
|
+
return undefined;
|
|
1964
|
+
}
|
|
1965
|
+
const output = readToolResultArtifactContent(result.artifact);
|
|
1966
|
+
if (!output) {
|
|
1967
|
+
return undefined;
|
|
1968
|
+
}
|
|
1969
|
+
const parsed = parseToolOutput(output);
|
|
1970
|
+
if (!parsed || typeof parsed['content'] !== 'string') {
|
|
1971
|
+
return undefined;
|
|
1972
|
+
}
|
|
1973
|
+
const rawPath = typeof parsed['path'] === 'string' ? parsed['path'] : typeof call.input['path'] === 'string' ? call.input['path'] : '';
|
|
1974
|
+
if (!rawPath) {
|
|
1975
|
+
return undefined;
|
|
1976
|
+
}
|
|
1977
|
+
const normalizedPath = normalizeFilesystemCandidate(rawPath, home) ?? normalize(resolve(cwd, rawPath));
|
|
1978
|
+
if (isSensitiveFilesystemCandidate(normalizedPath)) {
|
|
1979
|
+
return undefined;
|
|
1980
|
+
}
|
|
1981
|
+
const content = truncateText(parsed['content'], maxRecentFileExcerptChars);
|
|
1982
|
+
return [`- ${normalizedPath}`, ' ```', indentContinuationContextBlock(content), ' ```'].join('\n');
|
|
1983
|
+
}
|
|
1984
|
+
function readToolResultArtifactContent(artifact) {
|
|
1985
|
+
try {
|
|
1986
|
+
const parsed = JSON.parse(readFileSync(artifact.path, 'utf8'));
|
|
1987
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
1988
|
+
return undefined;
|
|
1989
|
+
}
|
|
1990
|
+
const record = parsed;
|
|
1991
|
+
return typeof record['content'] === 'string' ? record['content'] : undefined;
|
|
1992
|
+
}
|
|
1993
|
+
catch {
|
|
1994
|
+
return undefined;
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
function indentContinuationContextBlock(text) {
|
|
1998
|
+
return text
|
|
1999
|
+
.replace(/\r\n/g, '\n')
|
|
2000
|
+
.split('\n')
|
|
2001
|
+
.map(line => ` ${line}`)
|
|
2002
|
+
.join('\n');
|
|
2003
|
+
}
|
|
2004
|
+
function formatPendingActionFact(event) {
|
|
2005
|
+
const lines = [`- ${event.sourceToolName} call ${event.callId}`];
|
|
2006
|
+
if (event.command) {
|
|
2007
|
+
lines.push(` Original command: ${compactText(event.command, 260)}`);
|
|
2008
|
+
}
|
|
2009
|
+
if (event.completionCommand) {
|
|
2010
|
+
lines.push(` Completion command: ${compactText(event.completionCommand, 360)}`);
|
|
2011
|
+
}
|
|
2012
|
+
else if (event.tokenName && event.token) {
|
|
2013
|
+
lines.push(` ${event.tokenName}: ${event.token}`);
|
|
2014
|
+
}
|
|
2015
|
+
if (event.url) {
|
|
2016
|
+
lines.push(` URL: ${event.url}`);
|
|
2017
|
+
}
|
|
2018
|
+
if (event.qrImagePath) {
|
|
2019
|
+
lines.push(` QR image: ${event.qrImagePath}`);
|
|
2020
|
+
}
|
|
2021
|
+
if (event.message) {
|
|
2022
|
+
lines.push(` Note: ${compactText(event.message, 220)}`);
|
|
2023
|
+
}
|
|
2024
|
+
return lines.length > 1 ? lines.join('\n') : undefined;
|
|
2025
|
+
}
|
|
1611
2026
|
function buildRecentFailedToolContext(events) {
|
|
1612
2027
|
const failedTurns = [];
|
|
1613
2028
|
let currentTurn;
|
|
@@ -2693,7 +3108,15 @@ function toolDefinitionsForProvider(registry, extraTools = []) {
|
|
|
2693
3108
|
description: tool.description,
|
|
2694
3109
|
parameters: tool.inputSchema,
|
|
2695
3110
|
})),
|
|
2696
|
-
]
|
|
3111
|
+
]
|
|
3112
|
+
.map(tool => ({
|
|
3113
|
+
...tool,
|
|
3114
|
+
parameters: stableJson(tool.parameters),
|
|
3115
|
+
}))
|
|
3116
|
+
.sort(compareToolDefinitions);
|
|
3117
|
+
}
|
|
3118
|
+
function compareToolDefinitions(left, right) {
|
|
3119
|
+
return left.name.localeCompare(right.name) || left.description.localeCompare(right.description);
|
|
2697
3120
|
}
|
|
2698
3121
|
function coreToolDefinitionsForProvider(index, config) {
|
|
2699
3122
|
const definitions = [];
|
|
@@ -3442,6 +3865,180 @@ function summarizeToolDetail(result) {
|
|
|
3442
3865
|
}
|
|
3443
3866
|
return undefined;
|
|
3444
3867
|
}
|
|
3868
|
+
function pendingActionFromToolResult(call, result) {
|
|
3869
|
+
if (!result.ok || (result.toolName !== 'run_command' && result.toolName !== 'execute_shell')) {
|
|
3870
|
+
return undefined;
|
|
3871
|
+
}
|
|
3872
|
+
const parsed = parseToolOutput(result.output);
|
|
3873
|
+
if (!parsed || typeof parsed['command'] !== 'string') {
|
|
3874
|
+
return undefined;
|
|
3875
|
+
}
|
|
3876
|
+
const command = parsed['command'].trim();
|
|
3877
|
+
const stdout = typeof parsed['stdout'] === 'string' ? parsed['stdout'] : '';
|
|
3878
|
+
const stderr = typeof parsed['stderr'] === 'string' ? parsed['stderr'] : '';
|
|
3879
|
+
const combinedOutput = joinCommandOutput(stdout, stderr);
|
|
3880
|
+
const structuredRecords = structuredObjectsFromText(combinedOutput);
|
|
3881
|
+
const token = firstStringFromRecords(structuredRecords, pendingActionTokenKeys()) ?? tokenFromText(combinedOutput);
|
|
3882
|
+
const tokenName = token ? tokenNameFromRecords(structuredRecords, pendingActionTokenKeys()) ?? tokenNameFromText(combinedOutput) ?? 'token' : undefined;
|
|
3883
|
+
const url = firstStringFromRecords(structuredRecords, pendingActionUrlKeys()) ?? urlFromText(combinedOutput);
|
|
3884
|
+
const qrImagePath = firstStringFromRecords(structuredRecords, ['qr_image_path', 'qrImagePath']) ?? qrImagePathFromRecords(structuredRecords);
|
|
3885
|
+
const message = firstStringFromRecords(structuredRecords, ['message', 'title', 'hint']);
|
|
3886
|
+
const completionCommand = completionCommandFromText(combinedOutput) ?? deriveCompletionCommand(command, token);
|
|
3887
|
+
if (!completionCommand && !token && !pendingActionSignal(command, combinedOutput, url)) {
|
|
3888
|
+
return undefined;
|
|
3889
|
+
}
|
|
3890
|
+
const action = {
|
|
3891
|
+
type: 'pending_action',
|
|
3892
|
+
callId: call.id,
|
|
3893
|
+
sourceToolName: result.toolName,
|
|
3894
|
+
command,
|
|
3895
|
+
...(completionCommand ? { completionCommand } : {}),
|
|
3896
|
+
...(tokenName ? { tokenName } : {}),
|
|
3897
|
+
...(token ? { token } : {}),
|
|
3898
|
+
...(url ? { url } : {}),
|
|
3899
|
+
...(qrImagePath ? { qrImagePath } : {}),
|
|
3900
|
+
...(message ? { message: compactText(message, 300) } : {}),
|
|
3901
|
+
};
|
|
3902
|
+
return action;
|
|
3903
|
+
}
|
|
3904
|
+
function pendingActionTokenKeys() {
|
|
3905
|
+
return ['complete_token', 'resume_token', 'continuation_token', 'poll_token', 'device_code', 'user_code'];
|
|
3906
|
+
}
|
|
3907
|
+
function pendingActionUrlKeys() {
|
|
3908
|
+
return ['verification_uri_complete', 'verification_url', 'authorization_url', 'authorize_url', 'login_url', 'url', 'open_url'];
|
|
3909
|
+
}
|
|
3910
|
+
function structuredObjectsFromText(text) {
|
|
3911
|
+
const records = [];
|
|
3912
|
+
for (const line of text.split(/\r?\n/)) {
|
|
3913
|
+
const trimmed = line.trim();
|
|
3914
|
+
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) {
|
|
3915
|
+
continue;
|
|
3916
|
+
}
|
|
3917
|
+
try {
|
|
3918
|
+
const parsed = JSON.parse(trimmed);
|
|
3919
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
3920
|
+
records.push(parsed);
|
|
3921
|
+
}
|
|
3922
|
+
}
|
|
3923
|
+
catch {
|
|
3924
|
+
// Non-JSON output is handled by regex fallback extractors below.
|
|
3925
|
+
}
|
|
3926
|
+
}
|
|
3927
|
+
return records;
|
|
3928
|
+
}
|
|
3929
|
+
function firstStringFromRecords(records, keys) {
|
|
3930
|
+
for (const record of records) {
|
|
3931
|
+
const value = findNestedString(record, keys);
|
|
3932
|
+
if (value) {
|
|
3933
|
+
return value;
|
|
3934
|
+
}
|
|
3935
|
+
}
|
|
3936
|
+
return undefined;
|
|
3937
|
+
}
|
|
3938
|
+
function tokenNameFromRecords(records, keys) {
|
|
3939
|
+
for (const record of records) {
|
|
3940
|
+
const key = findNestedStringKey(record, keys);
|
|
3941
|
+
if (key) {
|
|
3942
|
+
return key;
|
|
3943
|
+
}
|
|
3944
|
+
}
|
|
3945
|
+
return undefined;
|
|
3946
|
+
}
|
|
3947
|
+
function findNestedString(value, keys, depth = 0) {
|
|
3948
|
+
if (!value || typeof value !== 'object' || depth > 4) {
|
|
3949
|
+
return undefined;
|
|
3950
|
+
}
|
|
3951
|
+
const record = value;
|
|
3952
|
+
for (const key of keys) {
|
|
3953
|
+
const candidate = record[key];
|
|
3954
|
+
if (typeof candidate === 'string' && candidate.trim().length > 0) {
|
|
3955
|
+
return candidate.trim();
|
|
3956
|
+
}
|
|
3957
|
+
}
|
|
3958
|
+
for (const nested of Object.values(record)) {
|
|
3959
|
+
const candidate = findNestedString(nested, keys, depth + 1);
|
|
3960
|
+
if (candidate) {
|
|
3961
|
+
return candidate;
|
|
3962
|
+
}
|
|
3963
|
+
}
|
|
3964
|
+
return undefined;
|
|
3965
|
+
}
|
|
3966
|
+
function findNestedStringKey(value, keys, depth = 0) {
|
|
3967
|
+
if (!value || typeof value !== 'object' || depth > 4) {
|
|
3968
|
+
return undefined;
|
|
3969
|
+
}
|
|
3970
|
+
const record = value;
|
|
3971
|
+
for (const key of keys) {
|
|
3972
|
+
const candidate = record[key];
|
|
3973
|
+
if (typeof candidate === 'string' && candidate.trim().length > 0) {
|
|
3974
|
+
return key;
|
|
3975
|
+
}
|
|
3976
|
+
}
|
|
3977
|
+
for (const nested of Object.values(record)) {
|
|
3978
|
+
const candidate = findNestedStringKey(nested, keys, depth + 1);
|
|
3979
|
+
if (candidate) {
|
|
3980
|
+
return candidate;
|
|
3981
|
+
}
|
|
3982
|
+
}
|
|
3983
|
+
return undefined;
|
|
3984
|
+
}
|
|
3985
|
+
function tokenFromText(text) {
|
|
3986
|
+
const match = text.match(/\b(?:complete[_-]?token|resume[_-]?token|continuation[_-]?token|poll[_-]?token|device[_-]?code|user[_-]?code)\b["':=\s]+([A-Za-z0-9._:-]{6,})/i);
|
|
3987
|
+
return match?.[1];
|
|
3988
|
+
}
|
|
3989
|
+
function tokenNameFromText(text) {
|
|
3990
|
+
const match = text.match(/\b(complete[_-]?token|resume[_-]?token|continuation[_-]?token|poll[_-]?token|device[_-]?code|user[_-]?code)\b/i);
|
|
3991
|
+
return match?.[1]?.replace(/-/g, '_');
|
|
3992
|
+
}
|
|
3993
|
+
function urlFromText(text) {
|
|
3994
|
+
const match = text.match(/https?:\/\/[^\s"'<>]+/i);
|
|
3995
|
+
return match ? trimUrlTrailer(match[0]) : undefined;
|
|
3996
|
+
}
|
|
3997
|
+
function trimUrlTrailer(url) {
|
|
3998
|
+
return url.replace(/[),.;]+$/g, '');
|
|
3999
|
+
}
|
|
4000
|
+
function qrImagePathFromRecords(records) {
|
|
4001
|
+
for (const record of records) {
|
|
4002
|
+
const path = findNestedString(record, ['path']);
|
|
4003
|
+
if (path && /(?:qr|auth|login).*\.(?:png|jpg|jpeg)$/i.test(path)) {
|
|
4004
|
+
return path;
|
|
4005
|
+
}
|
|
4006
|
+
}
|
|
4007
|
+
return undefined;
|
|
4008
|
+
}
|
|
4009
|
+
function completionCommandFromText(text) {
|
|
4010
|
+
const match = text.match(/([^\r\n`]*\b(?:auth|oauth|sso)?\s*login\b[^\r\n`]*--complete\s+\S+[^\r\n`]*)/i);
|
|
4011
|
+
const command = match?.[1]?.trim();
|
|
4012
|
+
return command && command.length < 1_000 ? command : undefined;
|
|
4013
|
+
}
|
|
4014
|
+
function deriveCompletionCommand(command, token) {
|
|
4015
|
+
if (!token) {
|
|
4016
|
+
return undefined;
|
|
4017
|
+
}
|
|
4018
|
+
const trimmed = command.trim();
|
|
4019
|
+
if (!trimmed) {
|
|
4020
|
+
return undefined;
|
|
4021
|
+
}
|
|
4022
|
+
if (/\s--complete(?:\s|$)/.test(trimmed)) {
|
|
4023
|
+
return trimmed;
|
|
4024
|
+
}
|
|
4025
|
+
const safeToken = shellWordForContext(token);
|
|
4026
|
+
if (/\s--begin(?:\s|$)/.test(trimmed)) {
|
|
4027
|
+
return trimmed.replace(/\s--begin(?=\s|$)/, ` --complete ${safeToken}`);
|
|
4028
|
+
}
|
|
4029
|
+
if (/\b(?:auth|oauth|sso)?\s*login\b/i.test(trimmed)) {
|
|
4030
|
+
return `${trimmed} --complete ${safeToken}`;
|
|
4031
|
+
}
|
|
4032
|
+
return undefined;
|
|
4033
|
+
}
|
|
4034
|
+
function shellWordForContext(value) {
|
|
4035
|
+
return /^[A-Za-z0-9._:/=-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
|
|
4036
|
+
}
|
|
4037
|
+
function pendingActionSignal(command, output, url) {
|
|
4038
|
+
return Boolean(url &&
|
|
4039
|
+
(/\b(?:auth|oauth|sso|login|mfa|device|verify|verification)\b/i.test(command) ||
|
|
4040
|
+
/\b(?:action_required|device_code|login_status|pending|verification_uri|authenticate|confirm|scan)\b/i.test(output)));
|
|
4041
|
+
}
|
|
3445
4042
|
function isRecoverableToolProtocolError(result) {
|
|
3446
4043
|
if (result.ok) {
|
|
3447
4044
|
return false;
|
|
@@ -3534,7 +4131,7 @@ function pathIsUrlRecoveryGuidance(result) {
|
|
|
3534
4131
|
return [
|
|
3535
4132
|
'Do not call filesystem tools for URLs.',
|
|
3536
4133
|
'Use web_fetch for public http(s) URLs.',
|
|
3537
|
-
'For authenticated internal
|
|
4134
|
+
'For authenticated internal document URLs, load a relevant internal-platform skill when one is available.',
|
|
3538
4135
|
].join(' ');
|
|
3539
4136
|
}
|
|
3540
4137
|
function runCommandRecoveryGuidance(result, call) {
|