@wangzhizhi/remi 0.0.1-alpha
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 +9 -0
- package/dist/doctor.js +108 -0
- package/dist/git.js +41 -0
- package/dist/help.js +27 -0
- package/dist/i18n.js +422 -0
- package/dist/index.js +97 -0
- package/dist/initPrompt.js +17 -0
- package/dist/model.js +116 -0
- package/dist/modelSelection.js +34 -0
- package/dist/permissionDisplay.js +46 -0
- package/dist/permissions.js +206 -0
- package/dist/repl.js +346 -0
- package/dist/resume.js +3 -0
- package/dist/setup.js +62 -0
- package/dist/statusline.js +59 -0
- package/dist/style.js +48 -0
- package/dist/syntaxTheme.js +39 -0
- package/dist/tui/RemiApp.js +1756 -0
- package/dist/tui/commands.js +427 -0
- package/dist/tui/index.js +42 -0
- package/dist/tui/renderers/Header.js +28 -0
- package/dist/tui/renderers/MessageList.js +1176 -0
- package/dist/tui/renderers/PromptBox.js +118 -0
- package/dist/tui/renderers/StatusLine.js +124 -0
- package/dist/tui/renderers/WorkingIndicator.js +70 -0
- package/dist/tui/slashCommandHighlight.js +8 -0
- package/dist/tui/theme.js +13 -0
- package/dist/tui/types.js +1 -0
- package/dist/usage.js +66 -0
- package/dist/version.js +5 -0
- package/node_modules/@remi/compact/dist/index.js +389 -0
- package/node_modules/@remi/compact/package.json +8 -0
- package/node_modules/@remi/config/dist/index.js +426 -0
- package/node_modules/@remi/config/package.json +8 -0
- package/node_modules/@remi/core/dist/contextBuilder.js +344 -0
- package/node_modules/@remi/core/dist/directoryOverview.js +359 -0
- package/node_modules/@remi/core/dist/index.js +2843 -0
- package/node_modules/@remi/core/dist/projectInstructions.js +123 -0
- package/node_modules/@remi/core/dist/responseStyles.js +98 -0
- package/node_modules/@remi/core/package.json +8 -0
- package/node_modules/@remi/llm/dist/index.js +804 -0
- package/node_modules/@remi/llm/package.json +8 -0
- package/node_modules/@remi/memory/dist/index.js +312 -0
- package/node_modules/@remi/memory/package.json +8 -0
- package/node_modules/@remi/permissions/dist/index.js +90 -0
- package/node_modules/@remi/permissions/package.json +8 -0
- package/node_modules/@remi/sessions/dist/index.js +370 -0
- package/node_modules/@remi/sessions/package.json +8 -0
- package/node_modules/@remi/skills/dist/index.js +273 -0
- package/node_modules/@remi/skills/package.json +8 -0
- package/node_modules/@remi/terminal-markdown/dist/index.js +1412 -0
- package/node_modules/@remi/terminal-markdown/package.json +8 -0
- package/node_modules/@remi/tools/dist/index.js +3875 -0
- package/node_modules/@remi/tools/package.json +8 -0
- package/package.json +48 -0
|
@@ -0,0 +1,2843 @@
|
|
|
1
|
+
import { loadRemiConfig, normalizePermissionProfile, resolveToolPermissionMode, } from '@remi/config';
|
|
2
|
+
import { buildBaseSystemPrompt, defaultResponseStyleId, resolveResponseStyle, } from './responseStyles.js';
|
|
3
|
+
import { buildProviderContext } from './contextBuilder.js';
|
|
4
|
+
import { buildDirectoryOverviewPrelude } from './directoryOverview.js';
|
|
5
|
+
import { buildProjectInstructionsContext } from './projectInstructions.js';
|
|
6
|
+
import { emptyMemoryRecall, recallRelevantMemories, recordMemoryUse } from '@remi/memory';
|
|
7
|
+
import { compactSession, estimateSessionTokens, estimateTextTokens, eventsAfterLatestCompact, latestCompactSummary, maybeUpdateSessionMemory, readSessionMemory } from '@remi/compact';
|
|
8
|
+
import { formatSkillIndexContext, loadSkillContent, loadSkillIndex } from '@remi/skills';
|
|
9
|
+
export { buildBaseSystemPrompt, buildResponseLanguageSystemPrompt, buildResponseStyleSystemPrompt, defaultResponseStyleId, isResponseStyleId, resolveResponseStyle, responseStylePresets, } from './responseStyles.js';
|
|
10
|
+
export { buildProviderContext, } from './contextBuilder.js';
|
|
11
|
+
export { buildProjectInstructionsContext } from './projectInstructions.js';
|
|
12
|
+
import { createModelRouter, createProviderClient, createTokenEstimatorRegistry, } from '@remi/llm';
|
|
13
|
+
import { readFileSync } from 'node:fs';
|
|
14
|
+
import { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep } from 'node:path';
|
|
15
|
+
import { createSessionStore, readSessionEvents, writeToolResultArtifact } from '@remi/sessions';
|
|
16
|
+
import { createReadOnlyDryRunExecutor as createRemiReadOnlyDryRunExecutor, createLocalToolExecutor as createRemiLocalToolExecutor, createBuiltInToolRegistry as createRemiBuiltInToolRegistry, createToolRegistry as createRemiToolRegistry, createReadOnlyFileSystemExecutor as createRemiReadOnlyFileSystemExecutor, readFileTool, listFilesTool, searchTextTool, globTool, todoWriteTool, runCommandTool, } from '@remi/tools';
|
|
17
|
+
export { createReadOnlyDryRunExecutor, createReadOnlyFileSystemExecutor, createReadOnlyToolRegistry, createToolPermissionRequest, createToolRegistry, toolInputSchemaToJsonSchema, toolOutputSchemaToJsonSchema, readOnlyTools, readFileTool, listFilesTool, searchTextTool, globTool, todoWriteTool, createDirectoryTool, editFileTool, writeFileTool, deleteFileTool, runCommandTool, executeShellTool, builtInTools, createBuiltInToolRegistry, createLocalToolExecutor, } from '@remi/tools';
|
|
18
|
+
export const corePackageName = '@remi/core';
|
|
19
|
+
const loadSkillToolName = 'load_skill';
|
|
20
|
+
const maxLoadedSkillContentChars = 24_000;
|
|
21
|
+
const loadSkillTool = {
|
|
22
|
+
name: loadSkillToolName,
|
|
23
|
+
description: 'Load the full instructions for one Remi skill from the available skill index. If a listed skill semantically matches the user request, call this before reading files, editing files, running commands, or answering about the task. Do not use it when the match is ambiguous.',
|
|
24
|
+
inputSchema: {
|
|
25
|
+
type: 'object',
|
|
26
|
+
description: 'Load full Remi skill instructions by skill name.',
|
|
27
|
+
properties: {
|
|
28
|
+
name: {
|
|
29
|
+
type: 'string',
|
|
30
|
+
description: 'Skill name from the Remi Skills index, for example code-review.',
|
|
31
|
+
},
|
|
32
|
+
reason: {
|
|
33
|
+
type: 'string',
|
|
34
|
+
description: 'Optional brief reason why this skill matches the current user request.',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
required: ['name'],
|
|
38
|
+
additionalProperties: false,
|
|
39
|
+
},
|
|
40
|
+
outputSchema: {
|
|
41
|
+
type: 'object',
|
|
42
|
+
properties: {
|
|
43
|
+
ok: { type: 'boolean', description: 'Whether the skill was loaded.' },
|
|
44
|
+
skill: {
|
|
45
|
+
type: 'object',
|
|
46
|
+
description: 'Loaded skill metadata and instructions.',
|
|
47
|
+
properties: {
|
|
48
|
+
name: { type: 'string', description: 'Skill name.' },
|
|
49
|
+
displayName: { type: 'string', description: 'User-facing skill name.' },
|
|
50
|
+
source: { type: 'string', description: 'Skill source.' },
|
|
51
|
+
filePath: { type: 'string', description: 'Absolute SKILL.md path.' },
|
|
52
|
+
instructions: { type: 'string', description: 'Full skill instructions.' },
|
|
53
|
+
},
|
|
54
|
+
required: ['name', 'displayName', 'source', 'filePath', 'instructions'],
|
|
55
|
+
additionalProperties: false,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
required: ['ok'],
|
|
59
|
+
additionalProperties: false,
|
|
60
|
+
},
|
|
61
|
+
riskLevel: 'read',
|
|
62
|
+
permissionPolicy: {
|
|
63
|
+
mode: 'allow',
|
|
64
|
+
requirements: ['filesystem-read', 'agent-state'],
|
|
65
|
+
reason: 'Loads a discovered local skill instruction file into this agent turn.',
|
|
66
|
+
},
|
|
67
|
+
outputLimit: { maxBytes: 128 * 1024, truncate: 'tail' },
|
|
68
|
+
};
|
|
69
|
+
const defaultTimeoutMs = 60_000;
|
|
70
|
+
export const defaultMaxTurns = 96;
|
|
71
|
+
export const defaultMaxToolCalls = 128;
|
|
72
|
+
const defaultMaxHistoryMessages = 24;
|
|
73
|
+
const defaultMaxHistoryChars = 24_000;
|
|
74
|
+
const defaultMaxHistoryMessageChars = 8_000;
|
|
75
|
+
const defaultFilesystemContextChars = 4_000;
|
|
76
|
+
const defaultMaxMemoryItems = 5;
|
|
77
|
+
const defaultMaxMemoryChars = 6_000;
|
|
78
|
+
const defaultMutationGuardRetries = 2;
|
|
79
|
+
const defaultVerificationClaimGuardRetries = 2;
|
|
80
|
+
const defaultPlanFirstGuardRetries = 2;
|
|
81
|
+
const defaultToolArgumentRetries = 2;
|
|
82
|
+
const maxIdenticalToolCallsBeforeSuppression = 2;
|
|
83
|
+
const defaultContextSafetyBufferTokens = 2_000;
|
|
84
|
+
const defaultMaxOutputReserveTokens = 8_000;
|
|
85
|
+
const defaultAutoCompactThresholdPercent = 85;
|
|
86
|
+
const maxAutoCompactFailures = 3;
|
|
87
|
+
const maxFilesystemContextEvents = 120;
|
|
88
|
+
const maxFilesystemContextRoots = 8;
|
|
89
|
+
const maxFilesystemContextPaths = 12;
|
|
90
|
+
const toolResultArtifactThresholdBytes = 16 * 1024;
|
|
91
|
+
const recoverableWriteToolProtocolErrors = new Set([
|
|
92
|
+
'FILE_EXISTS',
|
|
93
|
+
'FILE_NOT_READ',
|
|
94
|
+
'FILE_PARTIALLY_READ',
|
|
95
|
+
'FILE_CHANGED_SINCE_READ',
|
|
96
|
+
'STRING_NOT_FOUND',
|
|
97
|
+
]);
|
|
98
|
+
const recoverableExplorationToolProtocolErrors = new Set(['PATH_NOT_FOUND']);
|
|
99
|
+
const autoCompactFailuresBySession = new Map();
|
|
100
|
+
const tokenEstimatorRegistry = createTokenEstimatorRegistry();
|
|
101
|
+
export function createAgentToolRegistry() {
|
|
102
|
+
return createRemiBuiltInToolRegistry();
|
|
103
|
+
}
|
|
104
|
+
export function createAgentPlanModeToolRegistry() {
|
|
105
|
+
return createRemiToolRegistry([readFileTool, listFilesTool, searchTextTool, globTool, todoWriteTool, runCommandTool]);
|
|
106
|
+
}
|
|
107
|
+
export function createAgentDryRunToolExecutor() {
|
|
108
|
+
return createRemiReadOnlyDryRunExecutor(createAgentToolRegistry());
|
|
109
|
+
}
|
|
110
|
+
export function createAgentReadOnlyToolExecutor() {
|
|
111
|
+
return createRemiReadOnlyFileSystemExecutor(createAgentToolRegistry());
|
|
112
|
+
}
|
|
113
|
+
export function createAgentLocalToolExecutor() {
|
|
114
|
+
return createRemiLocalToolExecutor(createAgentToolRegistry());
|
|
115
|
+
}
|
|
116
|
+
function createContextBudget(model, config, runtimeConfig) {
|
|
117
|
+
const contextWindowTokens = Math.max(1, model.contextWindow);
|
|
118
|
+
const configuredOutputReserve = config.maxOutputReserveTokens ?? runtimeConfig?.maxOutputReserveTokens;
|
|
119
|
+
const modelOutputReserve = Math.min(model.maxOutputTokens ?? defaultMaxOutputReserveTokens, defaultMaxOutputReserveTokens);
|
|
120
|
+
const outputReserveTokens = clampInteger(configuredOutputReserve ?? modelOutputReserve, 1_024, Math.max(1_024, Math.floor(contextWindowTokens * 0.4)));
|
|
121
|
+
const safetyBufferTokens = clampInteger(config.contextSafetyBufferTokens ?? runtimeConfig?.contextSafetyBufferTokens ?? defaultContextSafetyBufferTokens, 0, Math.max(0, Math.floor(contextWindowTokens * 0.2)));
|
|
122
|
+
const effectiveInputBudgetTokens = Math.max(1_024, contextWindowTokens - outputReserveTokens - safetyBufferTokens);
|
|
123
|
+
const thresholdPercent = clampInteger(config.autoCompactThresholdPercent ?? runtimeConfig?.autoCompactThresholdPercent ?? defaultAutoCompactThresholdPercent, 50, 98);
|
|
124
|
+
const autoCompactThresholdTokens = Math.max(512, Math.floor(effectiveInputBudgetTokens * (thresholdPercent / 100)));
|
|
125
|
+
const system = Math.min(16_000, Math.max(1_024, Math.floor(effectiveInputBudgetTokens * 0.08)));
|
|
126
|
+
const tool = Math.min(24_000, Math.max(1_024, Math.floor(effectiveInputBudgetTokens * 0.12)));
|
|
127
|
+
const project = Math.min(charsToTokenBudget(defaultFilesystemContextChars), Math.max(512, Math.floor(effectiveInputBudgetTokens * 0.04)));
|
|
128
|
+
const memory = Math.min(charsToTokenBudget(config.maxMemoryChars ?? defaultMaxMemoryChars), Math.max(512, Math.floor(effectiveInputBudgetTokens * 0.08)));
|
|
129
|
+
const session = Math.min(charsToTokenBudget(config.maxSessionMemoryChars ?? 8_000), Math.max(512, Math.floor(effectiveInputBudgetTokens * 0.10)));
|
|
130
|
+
const compact = Math.min(24_000, Math.max(1_024, Math.floor(effectiveInputBudgetTokens * 0.14)));
|
|
131
|
+
const current = Math.min(64_000, Math.max(1_024, Math.floor(effectiveInputBudgetTokens * 0.20)));
|
|
132
|
+
const reserved = system + tool + project + memory + session + compact + current;
|
|
133
|
+
const recent = Math.max(1_024, effectiveInputBudgetTokens - reserved);
|
|
134
|
+
const tokenBudgets = {
|
|
135
|
+
system,
|
|
136
|
+
tool,
|
|
137
|
+
project,
|
|
138
|
+
memory,
|
|
139
|
+
session,
|
|
140
|
+
compact,
|
|
141
|
+
recent,
|
|
142
|
+
current,
|
|
143
|
+
};
|
|
144
|
+
return {
|
|
145
|
+
contextWindowTokens,
|
|
146
|
+
effectiveInputBudgetTokens,
|
|
147
|
+
autoCompactThresholdTokens,
|
|
148
|
+
tokenBudgets,
|
|
149
|
+
charBudgets: Object.fromEntries(Object.entries(tokenBudgets).map(([layer, tokens]) => [layer, tokenBudgetToChars(tokens ?? 0)])),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function shouldAutoCompactSession(options) {
|
|
153
|
+
if (!isAutoCompactEnabled(options.config, options.runtimeConfig, options.env)) {
|
|
154
|
+
return { shouldCompact: false };
|
|
155
|
+
}
|
|
156
|
+
const activeEvents = eventsAfterLatestCompact(options.events);
|
|
157
|
+
if (activeEvents.length < 4) {
|
|
158
|
+
return { shouldCompact: false };
|
|
159
|
+
}
|
|
160
|
+
const failureKey = `${options.cwd}\0${options.sessionId}`;
|
|
161
|
+
if ((autoCompactFailuresBySession.get(failureKey) ?? 0) >= maxAutoCompactFailures) {
|
|
162
|
+
return { shouldCompact: false };
|
|
163
|
+
}
|
|
164
|
+
const estimatedTokens = estimateSessionTokens(activeEvents) + estimateTextTokens(options.input);
|
|
165
|
+
if (estimatedTokens < options.budget.autoCompactThresholdTokens) {
|
|
166
|
+
return { shouldCompact: false };
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
shouldCompact: true,
|
|
170
|
+
failureKey,
|
|
171
|
+
maxSummaryChars: tokenBudgetToChars(options.budget.tokenBudgets.compact ?? 6_000),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function performAutoCompactSession(options) {
|
|
175
|
+
try {
|
|
176
|
+
try {
|
|
177
|
+
maybeUpdateSessionMemory(options.cwd, options.sessionId, { force: true, maxSummaryChars: Math.min(options.maxSummaryChars, 8_000) });
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// Auto compact can still proceed from transcript if session memory update fails.
|
|
181
|
+
}
|
|
182
|
+
const result = compactSession(options.cwd, options.sessionId, {
|
|
183
|
+
maxSummaryChars: options.maxSummaryChars,
|
|
184
|
+
trigger: 'auto',
|
|
185
|
+
strategy: 'deterministic',
|
|
186
|
+
});
|
|
187
|
+
autoCompactFailuresBySession.delete(options.failureKey);
|
|
188
|
+
return {
|
|
189
|
+
compacted: true,
|
|
190
|
+
summary: result.summary,
|
|
191
|
+
estimatedTokens: result.estimatedTokens,
|
|
192
|
+
sourceEventCount: result.sourceEventCount,
|
|
193
|
+
trigger: 'auto',
|
|
194
|
+
strategy: 'deterministic',
|
|
195
|
+
events: readSessionEvents(options.cwd, options.sessionId),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
autoCompactFailuresBySession.set(options.failureKey, (autoCompactFailuresBySession.get(options.failureKey) ?? 0) + 1);
|
|
200
|
+
return { compacted: false };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function isAutoCompactEnabled(config, runtimeConfig, env) {
|
|
204
|
+
if (env['REMI_DISABLE_AUTO_COMPACT'] === '1' || env['DISABLE_COMPACT'] === '1') {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
return config.autoCompact ?? runtimeConfig?.autoCompact ?? true;
|
|
208
|
+
}
|
|
209
|
+
function defaultMaxHistoryMessagesForBudget(budget) {
|
|
210
|
+
if (budget.effectiveInputBudgetTokens >= 500_000) {
|
|
211
|
+
return 120;
|
|
212
|
+
}
|
|
213
|
+
if (budget.effectiveInputBudgetTokens >= 120_000) {
|
|
214
|
+
return 80;
|
|
215
|
+
}
|
|
216
|
+
return defaultMaxHistoryMessages;
|
|
217
|
+
}
|
|
218
|
+
function charsToTokenBudget(chars) {
|
|
219
|
+
return Math.max(1, Math.ceil(chars / 3));
|
|
220
|
+
}
|
|
221
|
+
function tokenBudgetToChars(tokens) {
|
|
222
|
+
return Math.max(256, Math.floor(tokens * 3.5));
|
|
223
|
+
}
|
|
224
|
+
function clampInteger(value, min, max) {
|
|
225
|
+
if (!Number.isFinite(value)) {
|
|
226
|
+
return min;
|
|
227
|
+
}
|
|
228
|
+
return Math.max(min, Math.min(max, Math.floor(value)));
|
|
229
|
+
}
|
|
230
|
+
export function estimateSessionContextTrace(config = {}) {
|
|
231
|
+
const cwd = config.cwd ?? process.cwd();
|
|
232
|
+
const env = config.env ?? process.env;
|
|
233
|
+
const loaded = loadRemiConfig({ cwd });
|
|
234
|
+
const router = createModelRouter(loaded.config, env);
|
|
235
|
+
for (const [role, modelAlias] of Object.entries(config.modelOverrides ?? {})) {
|
|
236
|
+
if (modelAlias) {
|
|
237
|
+
router.switchRoleModel(role, modelAlias);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const resolved = router.resolve('main');
|
|
241
|
+
const contextBudget = createContextBudget(resolved, config, loaded.config.agent);
|
|
242
|
+
const tokenEstimatorResolution = tokenEstimatorRegistry.resolve(resolved);
|
|
243
|
+
const sessionEvents = config.sessionId ? readSessionEvents(cwd, config.sessionId) : [];
|
|
244
|
+
const sessionEventsForContext = eventsAfterLatestCompact(sessionEvents);
|
|
245
|
+
const compactSummary = latestCompactSummary(sessionEvents);
|
|
246
|
+
const sessionMemorySummary = config.disableSessionMemory || !config.sessionId ? undefined : readSessionMemory(cwd, config.sessionId)?.content;
|
|
247
|
+
const priorMessages = sessionEventsToChatMessages(sessionEventsForContext, {
|
|
248
|
+
maxMessages: config.maxHistoryMessages ?? defaultMaxHistoryMessagesForBudget(contextBudget),
|
|
249
|
+
maxChars: config.maxHistoryChars ?? tokenBudgetToChars(contextBudget.tokenBudgets.recent ?? 0),
|
|
250
|
+
});
|
|
251
|
+
const responseStyle = resolveResponseStyle(config.responseStyle ?? loaded.config.responseStyle ?? defaultResponseStyleId);
|
|
252
|
+
const currentUserMessage = config.currentUserMessage ?? '';
|
|
253
|
+
const responseLanguage = resolveResponseLanguage(currentUserMessage);
|
|
254
|
+
const projectInstructionsContext = buildProjectInstructionsContext(cwd);
|
|
255
|
+
const providerContext = buildProviderContext({
|
|
256
|
+
systemPrompt: buildBaseSystemPrompt(responseStyle.id, responseLanguage),
|
|
257
|
+
...(projectInstructionsContext.content ? { projectInstructions: projectInstructionsContext.content } : {}),
|
|
258
|
+
projectInstructionsSource: projectInstructionsContext.source,
|
|
259
|
+
...(sessionMemorySummary ? { sessionMemorySummary } : {}),
|
|
260
|
+
...(compactSummary ? { compactSummary } : {}),
|
|
261
|
+
recentMessages: priorMessages,
|
|
262
|
+
currentUserMessage,
|
|
263
|
+
budgets: contextBudget.charBudgets,
|
|
264
|
+
tokenBudgets: contextBudget.tokenBudgets,
|
|
265
|
+
contextWindowTokens: contextBudget.contextWindowTokens,
|
|
266
|
+
effectiveInputBudgetTokens: contextBudget.effectiveInputBudgetTokens,
|
|
267
|
+
autoCompactThresholdTokens: contextBudget.autoCompactThresholdTokens,
|
|
268
|
+
tokenEstimator: tokenEstimatorResolution.estimator,
|
|
269
|
+
tokenEstimatorKind: tokenEstimatorResolution.actualKind,
|
|
270
|
+
tokenEstimatorRequestedKind: tokenEstimatorResolution.requestedKind,
|
|
271
|
+
tokenEstimateProfile: tokenEstimatorResolution.profile,
|
|
272
|
+
tokenEstimatorFallback: tokenEstimatorResolution.fallback,
|
|
273
|
+
...(tokenEstimatorResolution.reason ? { tokenEstimatorFallbackReason: tokenEstimatorResolution.reason } : {}),
|
|
274
|
+
});
|
|
275
|
+
return providerContext.trace;
|
|
276
|
+
}
|
|
277
|
+
export async function* runChatTurn(input, config = {}) {
|
|
278
|
+
const cwd = config.cwd ?? process.cwd();
|
|
279
|
+
const env = config.env ?? process.env;
|
|
280
|
+
const sessionStore = config.sessionStore ?? createSessionStore(cwd, config.sessionId);
|
|
281
|
+
const loaded = loadRemiConfig({ cwd });
|
|
282
|
+
const maxTurns = normalizeMaxTurns(config.maxTurns ?? loaded.config.agent?.maxTurns);
|
|
283
|
+
const maxToolCalls = normalizeToolCallBudget(config.maxToolCalls ?? loaded.config.agent?.maxToolCalls, defaultMaxToolCalls);
|
|
284
|
+
const router = createModelRouter(loaded.config, env);
|
|
285
|
+
for (const [role, modelAlias] of Object.entries(config.modelOverrides ?? {})) {
|
|
286
|
+
if (modelAlias) {
|
|
287
|
+
router.switchRoleModel(role, modelAlias);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
let resolved;
|
|
291
|
+
try {
|
|
292
|
+
resolved = router.resolve('main');
|
|
293
|
+
}
|
|
294
|
+
catch (error) {
|
|
295
|
+
const message = safeErrorMessage(error);
|
|
296
|
+
sessionStore.append({ type: 'user', content: input });
|
|
297
|
+
sessionStore.append({ type: 'system', level: 'error', content: message });
|
|
298
|
+
yield { type: 'error', sessionId: sessionStore.sessionId, message };
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const providerConfig = loaded.config.providers?.[resolved.providerAlias];
|
|
302
|
+
if (!providerConfig) {
|
|
303
|
+
const message = `Unknown provider alias: ${resolved.providerAlias}`;
|
|
304
|
+
sessionStore.append({ type: 'user', content: input });
|
|
305
|
+
sessionStore.append({ type: 'system', level: 'error', content: message });
|
|
306
|
+
yield { type: 'error', sessionId: sessionStore.sessionId, message };
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const modelEvent = {
|
|
310
|
+
profile: resolved.profile,
|
|
311
|
+
role: resolved.role,
|
|
312
|
+
alias: resolved.alias,
|
|
313
|
+
displayName: resolved.displayName ?? resolved.alias,
|
|
314
|
+
provider: resolved.providerAlias,
|
|
315
|
+
model: resolved.model,
|
|
316
|
+
...(resolved.effort !== undefined ? { effort: resolved.effort } : {}),
|
|
317
|
+
};
|
|
318
|
+
const responseStyle = resolveResponseStyle(config.responseStyle ?? loaded.config.responseStyle ?? defaultResponseStyleId);
|
|
319
|
+
const responseLanguage = resolveResponseLanguage(input);
|
|
320
|
+
const permissionProfile = config.permissionProfile ?? normalizePermissionProfile(loaded.config.permissions);
|
|
321
|
+
const toolPermissionMode = config.toolPermissionMode ?? resolveToolPermissionMode(loaded.config.permissions);
|
|
322
|
+
const responseStyleEvent = {
|
|
323
|
+
id: responseStyle.id,
|
|
324
|
+
label: responseStyle.label,
|
|
325
|
+
};
|
|
326
|
+
if (maxTurns < 1) {
|
|
327
|
+
const message = `Maximum model turns reached before starting request (maxTurns=${maxTurns}).`;
|
|
328
|
+
sessionStore.append({ type: 'user', content: input });
|
|
329
|
+
sessionStore.append({ type: 'system', level: 'error', content: message });
|
|
330
|
+
yield { type: 'error', sessionId: sessionStore.sessionId, message };
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
if (resolved.apiKeyStatus === 'missing') {
|
|
334
|
+
const keyHint = providerConfig.apiKeyEnv ? ` Set ${providerConfig.apiKeyEnv} or run model doctor.` : ' Run model doctor.';
|
|
335
|
+
const message = `Missing API key for model ${resolved.displayName}.${keyHint}`;
|
|
336
|
+
sessionStore.append({ type: 'user', content: input });
|
|
337
|
+
sessionStore.append({ type: 'system', level: 'error', content: message });
|
|
338
|
+
yield { type: 'error', sessionId: sessionStore.sessionId, message };
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const contextBudget = createContextBudget(resolved, config, loaded.config.agent);
|
|
342
|
+
const tokenEstimatorResolution = tokenEstimatorRegistry.resolve(resolved);
|
|
343
|
+
let sessionEvents = config.sessionStore ? [] : readSessionEvents(cwd, sessionStore.sessionId);
|
|
344
|
+
if (!config.sessionStore) {
|
|
345
|
+
const autoCompactDecision = shouldAutoCompactSession({
|
|
346
|
+
cwd,
|
|
347
|
+
sessionId: sessionStore.sessionId,
|
|
348
|
+
input,
|
|
349
|
+
events: sessionEvents,
|
|
350
|
+
budget: contextBudget,
|
|
351
|
+
config,
|
|
352
|
+
env,
|
|
353
|
+
...(loaded.config.agent ? { runtimeConfig: loaded.config.agent } : {}),
|
|
354
|
+
});
|
|
355
|
+
if (autoCompactDecision.shouldCompact) {
|
|
356
|
+
yield { type: 'compact_start', automatic: true, trigger: 'auto' };
|
|
357
|
+
const autoCompact = performAutoCompactSession({
|
|
358
|
+
cwd,
|
|
359
|
+
sessionId: sessionStore.sessionId,
|
|
360
|
+
failureKey: autoCompactDecision.failureKey,
|
|
361
|
+
maxSummaryChars: autoCompactDecision.maxSummaryChars,
|
|
362
|
+
});
|
|
363
|
+
if (autoCompact.compacted) {
|
|
364
|
+
yield {
|
|
365
|
+
type: 'compact',
|
|
366
|
+
summary: autoCompact.summary,
|
|
367
|
+
estimatedTokens: autoCompact.estimatedTokens,
|
|
368
|
+
sourceEventCount: autoCompact.sourceEventCount,
|
|
369
|
+
automatic: true,
|
|
370
|
+
trigger: autoCompact.trigger,
|
|
371
|
+
strategy: autoCompact.strategy,
|
|
372
|
+
};
|
|
373
|
+
sessionEvents = autoCompact.events;
|
|
374
|
+
}
|
|
375
|
+
yield { type: 'compact_end', automatic: true, trigger: 'auto', ok: autoCompact.compacted };
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const sessionEventsForContext = config.sessionStore ? [] : eventsAfterLatestCompact(sessionEvents);
|
|
379
|
+
const compactSummary = latestCompactSummary(sessionEvents);
|
|
380
|
+
const sessionMemorySummary = config.sessionStore || config.disableSessionMemory ? undefined : readSessionMemory(cwd, sessionStore.sessionId)?.content;
|
|
381
|
+
const priorMessages = config.sessionStore
|
|
382
|
+
? []
|
|
383
|
+
: sessionEventsToChatMessages(sessionEventsForContext, {
|
|
384
|
+
maxMessages: config.maxHistoryMessages ?? defaultMaxHistoryMessagesForBudget(contextBudget),
|
|
385
|
+
maxChars: config.maxHistoryChars ?? tokenBudgetToChars(contextBudget.tokenBudgets.recent ?? 0),
|
|
386
|
+
});
|
|
387
|
+
sessionStore.append({ type: 'user', content: input });
|
|
388
|
+
const planFirstRequirement = config.planMode || config.planFirst === 'off' ? undefined : planFirstRequirementForInput(input, sessionEventsForContext);
|
|
389
|
+
const planExecutionApproval = config.planMode ? undefined : planExecutionApprovalForInput(input, sessionEventsForContext);
|
|
390
|
+
const planModeToolRegistry = createAgentPlanModeToolRegistry();
|
|
391
|
+
const fullToolRegistry = createAgentToolRegistry();
|
|
392
|
+
const effectivePlanMode = Boolean(config.planMode || planFirstRequirement);
|
|
393
|
+
const toolExecutor = config.toolExecutor ?? createRemiReadOnlyFileSystemExecutor(fullToolRegistry);
|
|
394
|
+
const readFileState = new Map();
|
|
395
|
+
const shouldOfferTools = resolved.supportsTools && shouldOfferToolsForInput(input);
|
|
396
|
+
const disabledSkills = loaded.config.skills?.disabled;
|
|
397
|
+
const skillHomeDir = typeof env.HOME === 'string' && env.HOME.trim().length > 0 ? env.HOME : config.env ? cwd : undefined;
|
|
398
|
+
const skillLoadOptions = {
|
|
399
|
+
cwd,
|
|
400
|
+
...(skillHomeDir ? { homeDir: skillHomeDir } : {}),
|
|
401
|
+
...(disabledSkills ? { disabled: disabledSkills } : {}),
|
|
402
|
+
};
|
|
403
|
+
const skillIndex = loadSkillIndex(skillLoadOptions);
|
|
404
|
+
const hasEnabledSkillIndex = skillIndex.skills.some(skill => skill.enabled);
|
|
405
|
+
const shouldUseSkillDecisionGuard = hasEnabledSkillIndex && !isExplicitSkillInvocationPrompt(input);
|
|
406
|
+
const fullToolDefinitions = shouldOfferTools ? toolDefinitionsForProvider(fullToolRegistry, skillToolDefinitionsForProvider(skillIndex)) : [];
|
|
407
|
+
const requiredMutationTools = config.planMode || planFirstRequirement?.executeAfterPlan === false ? [] : requiredMutationToolsForInput(input, fullToolDefinitions);
|
|
408
|
+
const adaptiveToolRegistry = shouldUseReadOnlyToolSurface(input, planExecutionApproval, requiredMutationTools) ? planModeToolRegistry : fullToolRegistry;
|
|
409
|
+
let activeToolRegistry = effectivePlanMode ? planModeToolRegistry : adaptiveToolRegistry;
|
|
410
|
+
let toolDefinitions = shouldOfferTools ? toolDefinitionsForProvider(activeToolRegistry, skillToolDefinitionsForProvider(skillIndex)) : [];
|
|
411
|
+
const directoryOverviewPrelude = toolDefinitions.length > 0
|
|
412
|
+
? await buildDirectoryOverviewPrelude({
|
|
413
|
+
input,
|
|
414
|
+
cwd,
|
|
415
|
+
env,
|
|
416
|
+
sessionId: sessionStore.sessionId,
|
|
417
|
+
executor: toolExecutor,
|
|
418
|
+
...(config.signal ? { signal: config.signal } : {}),
|
|
419
|
+
})
|
|
420
|
+
: undefined;
|
|
421
|
+
const recentFilesystemContext = toolDefinitions.length > 0 ? buildRecentFilesystemContext(sessionEventsForContext, cwd, env) : undefined;
|
|
422
|
+
const projectInstructionsContext = buildProjectInstructionsContext(cwd);
|
|
423
|
+
const loadedSkills = [];
|
|
424
|
+
const loadedSkillNames = new Set();
|
|
425
|
+
const skillContext = formatSkillIndexContext(skillIndex);
|
|
426
|
+
const projectInstructions = mergeProjectInstructionSections(projectInstructionsContext.content, recentFilesystemContext, skillContext);
|
|
427
|
+
const memoryRecall = config.disableMemory || !shouldOfferMemoryForInput(input)
|
|
428
|
+
? emptyMemoryRecall(input, config.maxMemoryItems ?? defaultMaxMemoryItems)
|
|
429
|
+
: recallRelevantMemories(cwd, input, {
|
|
430
|
+
limit: config.maxMemoryItems ?? defaultMaxMemoryItems,
|
|
431
|
+
maxBodyChars: config.maxMemoryChars ?? defaultMaxMemoryChars,
|
|
432
|
+
});
|
|
433
|
+
const providerContext = buildProviderContext({
|
|
434
|
+
systemPrompt: buildBaseSystemPrompt(responseStyle.id, responseLanguage),
|
|
435
|
+
...(toolDefinitions.length > 0 ? { toolPrompt: buildToolUseSystemPrompt(cwd, env, planPromptMode(config.planMode, planFirstRequirement), responseLanguage) } : {}),
|
|
436
|
+
...(projectInstructions ? { projectInstructions } : {}),
|
|
437
|
+
projectInstructionsSource: projectInstructionSource(projectInstructionsContext, Boolean(recentFilesystemContext)),
|
|
438
|
+
memories: memoryRecall.memories,
|
|
439
|
+
...(sessionMemorySummary ? { sessionMemorySummary } : {}),
|
|
440
|
+
...(compactSummary ? { compactSummary } : {}),
|
|
441
|
+
recentMessages: [...priorMessages, ...(directoryOverviewPrelude?.messages ?? [])],
|
|
442
|
+
currentUserMessage: input,
|
|
443
|
+
budgets: contextBudget.charBudgets,
|
|
444
|
+
tokenBudgets: contextBudget.tokenBudgets,
|
|
445
|
+
contextWindowTokens: contextBudget.contextWindowTokens,
|
|
446
|
+
effectiveInputBudgetTokens: contextBudget.effectiveInputBudgetTokens,
|
|
447
|
+
autoCompactThresholdTokens: contextBudget.autoCompactThresholdTokens,
|
|
448
|
+
tokenEstimator: tokenEstimatorResolution.estimator,
|
|
449
|
+
tokenEstimatorKind: tokenEstimatorResolution.actualKind,
|
|
450
|
+
tokenEstimatorRequestedKind: tokenEstimatorResolution.requestedKind,
|
|
451
|
+
tokenEstimateProfile: tokenEstimatorResolution.profile,
|
|
452
|
+
tokenEstimatorFallback: tokenEstimatorResolution.fallback,
|
|
453
|
+
...(tokenEstimatorResolution.reason ? { tokenEstimatorFallbackReason: tokenEstimatorResolution.reason } : {}),
|
|
454
|
+
});
|
|
455
|
+
const messages = providerContext.messages;
|
|
456
|
+
if (planExecutionApproval) {
|
|
457
|
+
messages.push({ role: 'user', content: buildPlanExecutionApprovalPrompt(input, planExecutionApproval, responseLanguage) });
|
|
458
|
+
}
|
|
459
|
+
const recalledMemoryIds = memoryRecall.memories.map(memory => memory.id);
|
|
460
|
+
yield {
|
|
461
|
+
type: 'start',
|
|
462
|
+
sessionId: sessionStore.sessionId,
|
|
463
|
+
model: modelEvent,
|
|
464
|
+
responseStyle: responseStyleEvent,
|
|
465
|
+
contextTrace: providerContext.trace,
|
|
466
|
+
memoryTrace: memoryRecall.trace,
|
|
467
|
+
};
|
|
468
|
+
for (const skill of loadedSkills) {
|
|
469
|
+
yield {
|
|
470
|
+
type: 'skill_use',
|
|
471
|
+
name: skill.name,
|
|
472
|
+
displayName: skill.displayName,
|
|
473
|
+
source: skill.source,
|
|
474
|
+
filePath: skill.filePath,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
if (directoryOverviewPrelude) {
|
|
478
|
+
for (const { call, result } of directoryOverviewPrelude.toolCalls) {
|
|
479
|
+
yield { type: 'tool_call', callId: call.id, toolName: call.toolName, summary: summarizeToolCall(call) };
|
|
480
|
+
const detail = summarizeToolDetail(result);
|
|
481
|
+
const event = {
|
|
482
|
+
type: 'tool_result',
|
|
483
|
+
callId: result.callId,
|
|
484
|
+
toolName: result.toolName,
|
|
485
|
+
ok: result.ok,
|
|
486
|
+
summary: summarizeToolResult(result),
|
|
487
|
+
};
|
|
488
|
+
yield detail ? { ...event, detail } : event;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
const provider = config.provider ?? createProviderClient(providerConfig, env);
|
|
492
|
+
let assistantText = '';
|
|
493
|
+
let usage;
|
|
494
|
+
let publishedUsage;
|
|
495
|
+
let modelTurns = 0;
|
|
496
|
+
let toolCallsUsed = 0;
|
|
497
|
+
let mutationGuardRetries = 0;
|
|
498
|
+
let mutationClaimGuardRetries = 0;
|
|
499
|
+
let verificationClaimGuardRetries = 0;
|
|
500
|
+
let planFirstGuardRetries = 0;
|
|
501
|
+
let planFirstClaimGuardRetries = 0;
|
|
502
|
+
let planExecutionApprovalRetries = 0;
|
|
503
|
+
let postActionApprovalGuardRetries = 0;
|
|
504
|
+
let toolArgumentRetries = 0;
|
|
505
|
+
let skillDecisionGuardPrompted = false;
|
|
506
|
+
const planFirstState = createPlanFirstGuardState(planFirstRequirement);
|
|
507
|
+
const mutationState = createMutationGuardState(requiredMutationTools, input);
|
|
508
|
+
const verificationState = createVerificationGuardState();
|
|
509
|
+
const repeatedToolCallState = createRepeatedToolCallState();
|
|
510
|
+
const guardMutationClaims = shouldGuardMutationClaimsForInput(input, requiredMutationTools);
|
|
511
|
+
const guardVerificationClaims = shouldGuardVerificationClaimsForInput(input, requiredMutationTools);
|
|
512
|
+
try {
|
|
513
|
+
while (modelTurns < maxTurns) {
|
|
514
|
+
modelTurns += 1;
|
|
515
|
+
let turnText = '';
|
|
516
|
+
let turnReasoningText = '';
|
|
517
|
+
const requestedToolCalls = [];
|
|
518
|
+
const bufferTurnText = shouldBufferMutationText(mutationState) ||
|
|
519
|
+
guardMutationClaims ||
|
|
520
|
+
guardVerificationClaims ||
|
|
521
|
+
shouldBufferPlanFirstText(planFirstState) ||
|
|
522
|
+
shouldBufferSkillDecisionGuard(shouldUseSkillDecisionGuard, loadedSkillNames, skillDecisionGuardPrompted) ||
|
|
523
|
+
hasCurrentTurnActionEvidence(mutationState, verificationState);
|
|
524
|
+
for await (const chunk of provider.streamChat(messages, resolved, {
|
|
525
|
+
...(config.signal ? { signal: config.signal } : {}),
|
|
526
|
+
timeoutMs: config.timeoutMs ?? defaultTimeoutMs,
|
|
527
|
+
...(toolDefinitions.length > 0 ? { tools: toolDefinitions } : {}),
|
|
528
|
+
})) {
|
|
529
|
+
if (typeof chunk !== 'string') {
|
|
530
|
+
if (chunk?.type === 'usage') {
|
|
531
|
+
const nextUsage = mergeTokenUsage(usage, chunk.usage);
|
|
532
|
+
const usageDelta = tokenUsageDelta(publishedUsage, nextUsage);
|
|
533
|
+
usage = nextUsage;
|
|
534
|
+
if (hasTokenUsage(usageDelta)) {
|
|
535
|
+
publishedUsage = nextUsage;
|
|
536
|
+
yield { type: 'usage', usage: usageDelta };
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
else if (chunk?.type === 'reasoning_delta') {
|
|
540
|
+
turnReasoningText += chunk.text;
|
|
541
|
+
}
|
|
542
|
+
else if (chunk?.type === 'tool_call') {
|
|
543
|
+
requestedToolCalls.push(chunk.toolCall);
|
|
544
|
+
}
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
if (chunk.length === 0) {
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
turnText += chunk;
|
|
551
|
+
if (!bufferTurnText) {
|
|
552
|
+
yield { type: 'delta', text: chunk };
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
if (requestedToolCalls.length === 0) {
|
|
556
|
+
if (shouldRetryMissingPlanFirst(planFirstState, planFirstGuardRetries, modelTurns, maxTurns)) {
|
|
557
|
+
planFirstGuardRetries += 1;
|
|
558
|
+
messages.push({ role: 'user', content: buildPlanFirstGuardPrompt(input, planFirstState, responseLanguage) });
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
if (shouldFailMissingPlanFirst(planFirstState)) {
|
|
562
|
+
const message = planFirstGuardFailureMessage(planFirstState);
|
|
563
|
+
sessionStore.append({ type: 'system', level: 'error', content: message });
|
|
564
|
+
yield { type: 'error', sessionId: sessionStore.sessionId, message };
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (shouldRetryMissingMutationTool(mutationState, mutationGuardRetries, modelTurns, maxTurns)) {
|
|
568
|
+
mutationGuardRetries += 1;
|
|
569
|
+
messages.push({ role: 'user', content: buildMutationGuardPrompt(input, mutationState, responseLanguage) });
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
if (shouldFailMissingMutationTool(mutationState)) {
|
|
573
|
+
const message = mutationGuardFailureMessage(mutationState);
|
|
574
|
+
sessionStore.append({ type: 'system', level: 'error', content: message });
|
|
575
|
+
yield { type: 'error', sessionId: sessionStore.sessionId, message };
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
const mutationClaimIssue = guardMutationClaims ? mutationCompletionClaimIssue(turnText, mutationState) : undefined;
|
|
579
|
+
if (mutationClaimIssue) {
|
|
580
|
+
if (mutationClaimGuardRetries < defaultMutationGuardRetries && modelTurns < maxTurns) {
|
|
581
|
+
mutationClaimGuardRetries += 1;
|
|
582
|
+
messages.push({ role: 'user', content: buildMutationClaimGuardPrompt(input, turnText, mutationState, mutationClaimIssue, responseLanguage) });
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
const message = mutationClaimGuardFailureMessage(mutationClaimIssue);
|
|
586
|
+
sessionStore.append({ type: 'system', level: 'error', content: message });
|
|
587
|
+
yield { type: 'error', sessionId: sessionStore.sessionId, message };
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
const verificationClaimIssue = guardVerificationClaims ? verificationClaimGuardIssue(turnText, verificationState) : undefined;
|
|
591
|
+
if (verificationClaimIssue) {
|
|
592
|
+
if (verificationClaimGuardRetries < defaultVerificationClaimGuardRetries && modelTurns < maxTurns) {
|
|
593
|
+
verificationClaimGuardRetries += 1;
|
|
594
|
+
messages.push({ role: 'user', content: buildVerificationClaimGuardPrompt(input, turnText, verificationState, responseLanguage) });
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
const message = verificationClaimGuardFailureMessage(verificationState);
|
|
598
|
+
sessionStore.append({ type: 'system', level: 'error', content: message });
|
|
599
|
+
yield { type: 'error', sessionId: sessionStore.sessionId, message };
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
const planFirstClaimIssue = planFirstCompletionClaimIssue(turnText, planFirstState);
|
|
603
|
+
if (planFirstClaimIssue) {
|
|
604
|
+
if (planFirstClaimGuardRetries < defaultPlanFirstGuardRetries && modelTurns < maxTurns) {
|
|
605
|
+
planFirstClaimGuardRetries += 1;
|
|
606
|
+
messages.push({ role: 'user', content: buildPlanFirstClaimGuardPrompt(input, turnText, planFirstState, planFirstClaimIssue, responseLanguage) });
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
const message = planFirstClaimGuardFailureMessage(planFirstClaimIssue);
|
|
610
|
+
sessionStore.append({ type: 'system', level: 'error', content: message });
|
|
611
|
+
yield { type: 'error', sessionId: sessionStore.sessionId, message };
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
if (shouldRetryPlanExecutionApproval(planExecutionApproval, turnText, planExecutionApprovalRetries, modelTurns, maxTurns)) {
|
|
615
|
+
planExecutionApprovalRetries += 1;
|
|
616
|
+
messages.push({ role: 'user', content: buildPlanExecutionApprovalPrompt(input, planExecutionApproval, responseLanguage, true) });
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
if (shouldRetryPostActionApprovalDraft(turnText, mutationState, verificationState, postActionApprovalGuardRetries, modelTurns, maxTurns)) {
|
|
620
|
+
postActionApprovalGuardRetries += 1;
|
|
621
|
+
messages.push({ role: 'user', content: buildPostActionApprovalGuardPrompt(input, turnText, mutationState, verificationState, responseLanguage) });
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
assistantText += turnText;
|
|
625
|
+
if (bufferTurnText && turnText.length > 0) {
|
|
626
|
+
yield { type: 'delta', text: turnText };
|
|
627
|
+
}
|
|
628
|
+
sessionStore.append({
|
|
629
|
+
type: 'assistant',
|
|
630
|
+
content: assistantText,
|
|
631
|
+
model: modelEvent,
|
|
632
|
+
style: responseStyleEvent,
|
|
633
|
+
...(usage ? { usage } : {}),
|
|
634
|
+
});
|
|
635
|
+
recordRecalledMemoryUse(cwd, recalledMemoryIds);
|
|
636
|
+
recordSessionMemoryIfNeeded(cwd, sessionStore.sessionId, config);
|
|
637
|
+
yield {
|
|
638
|
+
type: 'done',
|
|
639
|
+
text: assistantText,
|
|
640
|
+
sessionId: sessionStore.sessionId,
|
|
641
|
+
sessionPath: sessionStore.sessionPath,
|
|
642
|
+
...(usage ? { usage } : {}),
|
|
643
|
+
};
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
if (shouldRetrySkillDecisionGuard(shouldUseSkillDecisionGuard, loadedSkillNames, skillDecisionGuardPrompted, requestedToolCalls, modelTurns, maxTurns)) {
|
|
647
|
+
skillDecisionGuardPrompted = true;
|
|
648
|
+
messages.push({ role: 'user', content: buildSkillDecisionGuardPrompt(input, requestedToolCalls, responseLanguage) });
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
messages.push({
|
|
652
|
+
role: 'assistant',
|
|
653
|
+
content: turnText,
|
|
654
|
+
...(turnReasoningText.length > 0 ? { reasoningContent: turnReasoningText } : {}),
|
|
655
|
+
toolCalls: requestedToolCalls,
|
|
656
|
+
});
|
|
657
|
+
let pendingConcurrentToolExecutions = [];
|
|
658
|
+
const createToolExecutionContext = () => ({
|
|
659
|
+
cwd,
|
|
660
|
+
sessionId: sessionStore.sessionId,
|
|
661
|
+
maxOutputBytes: 128 * 1024,
|
|
662
|
+
...(config.signal ? { signal: config.signal } : {}),
|
|
663
|
+
permissionMode: toolPermissionMode,
|
|
664
|
+
permissionProfile,
|
|
665
|
+
allowedReadRoots: allowedReadRoots(cwd, env),
|
|
666
|
+
allowedWriteRoots: allowedWriteRoots(cwd, env),
|
|
667
|
+
permissionRules: latestPermissionRules(cwd, env, runtimePermissionRules(config.permissionRules)),
|
|
668
|
+
...(config.requestToolPermission ? { requestPermission: config.requestToolPermission } : {}),
|
|
669
|
+
readFileState,
|
|
670
|
+
});
|
|
671
|
+
const executePreparedToolCall = async (prepared) => {
|
|
672
|
+
const outcome = prepared.hiddenPlanFirstToolCall
|
|
673
|
+
? planFirstUnavailableToolOutcome(prepared.call)
|
|
674
|
+
: prepared.repeatedToolCallOutcome
|
|
675
|
+
? prepared.repeatedToolCallOutcome
|
|
676
|
+
: await toolExecutor.execute({
|
|
677
|
+
call: prepared.call,
|
|
678
|
+
context: createToolExecutionContext(),
|
|
679
|
+
});
|
|
680
|
+
return { ...prepared, outcome };
|
|
681
|
+
};
|
|
682
|
+
const publishCompletedToolCall = function* (completed) {
|
|
683
|
+
const { requestedToolCall, call, hiddenPlanFirstToolCall, outcome } = completed;
|
|
684
|
+
const transcriptArtifacts = hiddenPlanFirstToolCall
|
|
685
|
+
? new Map()
|
|
686
|
+
: appendToolTranscriptEvents(cwd, sessionStore, outcome.transcriptEvents);
|
|
687
|
+
if (!hiddenPlanFirstToolCall) {
|
|
688
|
+
recordMutationToolOutcome(mutationState, outcome.toolName, outcome.result, call);
|
|
689
|
+
recordVerificationToolOutcome(verificationState, outcome.toolName, outcome.result);
|
|
690
|
+
}
|
|
691
|
+
const detail = summarizeToolDetail(outcome.result);
|
|
692
|
+
const artifact = transcriptArtifacts.get(outcome.callId);
|
|
693
|
+
const event = {
|
|
694
|
+
type: 'tool_result',
|
|
695
|
+
callId: outcome.callId,
|
|
696
|
+
toolName: outcome.toolName,
|
|
697
|
+
ok: outcome.result.ok,
|
|
698
|
+
summary: summarizeToolResult(outcome.result),
|
|
699
|
+
...(isRecoverableToolProtocolError(outcome.result) || hiddenPlanFirstToolCall ? { recoverable: true } : {}),
|
|
700
|
+
...(artifact ? { artifact } : {}),
|
|
701
|
+
};
|
|
702
|
+
const planItems = planItemsFromToolResult(outcome.result);
|
|
703
|
+
if (planItems) {
|
|
704
|
+
const continueAfterPlan = recordPlanFirstToolOutcome(planFirstState);
|
|
705
|
+
yield { type: 'plan_update', callId: outcome.callId, items: planItems };
|
|
706
|
+
if (continueAfterPlan) {
|
|
707
|
+
activeToolRegistry = fullToolRegistry;
|
|
708
|
+
toolDefinitions = fullToolDefinitions;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
else if (hiddenPlanFirstToolCall) {
|
|
712
|
+
yield event;
|
|
713
|
+
}
|
|
714
|
+
else {
|
|
715
|
+
yield detail ? { ...event, detail } : event;
|
|
716
|
+
}
|
|
717
|
+
messages.push({
|
|
718
|
+
role: 'tool',
|
|
719
|
+
toolCallId: requestedToolCall.id,
|
|
720
|
+
content: toolResultMessageContent(outcome.result, call),
|
|
721
|
+
});
|
|
722
|
+
if (planItems && planFirstState.requirement?.executeAfterPlan) {
|
|
723
|
+
messages.push({ role: 'user', content: buildPlanFirstContinuationPrompt(input, responseLanguage) });
|
|
724
|
+
}
|
|
725
|
+
else if (planItems && planExecutionApproval) {
|
|
726
|
+
messages.push({ role: 'user', content: buildPlanExecutionApprovalPrompt(input, planExecutionApproval, responseLanguage, true) });
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
const flushConcurrentToolExecutions = async function* () {
|
|
730
|
+
if (pendingConcurrentToolExecutions.length === 0) {
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
const batch = pendingConcurrentToolExecutions;
|
|
734
|
+
pendingConcurrentToolExecutions = [];
|
|
735
|
+
for (const prepared of batch) {
|
|
736
|
+
yield { type: 'tool_call', callId: prepared.call.id, toolName: prepared.call.toolName, summary: summarizeToolCall(prepared.call) };
|
|
737
|
+
}
|
|
738
|
+
const completedBatch = await Promise.all(batch.map(executePreparedToolCall));
|
|
739
|
+
for (const completed of completedBatch) {
|
|
740
|
+
yield* publishCompletedToolCall(completed);
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
for (const requestedToolCall of requestedToolCalls) {
|
|
744
|
+
if (toolCallsUsed >= maxToolCalls) {
|
|
745
|
+
yield* flushConcurrentToolExecutions();
|
|
746
|
+
const message = toolBudgetExhaustedMessage(toolCallsUsed, maxToolCalls);
|
|
747
|
+
sessionStore.append({ type: 'system', level: 'error', content: message });
|
|
748
|
+
yield { type: 'error', sessionId: sessionStore.sessionId, message };
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
toolCallsUsed += 1;
|
|
752
|
+
if (requestedToolCall.name === loadSkillToolName) {
|
|
753
|
+
yield* flushConcurrentToolExecutions();
|
|
754
|
+
const outcome = loadSkillToolCallOutcome(requestedToolCall, skillLoadOptions);
|
|
755
|
+
if (outcome.skill) {
|
|
756
|
+
const skillKey = normalizeSkillNameForRuntime(outcome.skill.name);
|
|
757
|
+
if (!loadedSkillNames.has(skillKey)) {
|
|
758
|
+
loadedSkillNames.add(skillKey);
|
|
759
|
+
yield {
|
|
760
|
+
type: 'skill_use',
|
|
761
|
+
name: outcome.skill.name,
|
|
762
|
+
displayName: outcome.skill.displayName,
|
|
763
|
+
source: outcome.skill.source,
|
|
764
|
+
filePath: outcome.skill.filePath,
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
messages.push({
|
|
769
|
+
role: 'tool',
|
|
770
|
+
toolCallId: requestedToolCall.id,
|
|
771
|
+
content: toolResultMessageContent(outcome.result),
|
|
772
|
+
});
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
const parsedToolCall = toolCallFromLLM(requestedToolCall, activeToolRegistry);
|
|
776
|
+
if (!parsedToolCall.ok) {
|
|
777
|
+
yield* flushConcurrentToolExecutions();
|
|
778
|
+
if (toolArgumentRetries >= defaultToolArgumentRetries || modelTurns >= maxTurns) {
|
|
779
|
+
const message = toolArgumentFailureMessage(toolArgumentRetries, parsedToolCall.result);
|
|
780
|
+
sessionStore.append({ type: 'system', level: 'error', content: message });
|
|
781
|
+
yield { type: 'error', sessionId: sessionStore.sessionId, message };
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
toolArgumentRetries += 1;
|
|
785
|
+
const outcome = invalidToolArgumentsOutcome(requestedToolCall, parsedToolCall.result);
|
|
786
|
+
yield {
|
|
787
|
+
type: 'tool_call',
|
|
788
|
+
callId: outcome.callId,
|
|
789
|
+
toolName: outcome.toolName,
|
|
790
|
+
summary: summarizeInvalidToolArgumentsCall(requestedToolCall),
|
|
791
|
+
};
|
|
792
|
+
appendToolTranscriptEvents(cwd, sessionStore, outcome.transcriptEvents);
|
|
793
|
+
yield {
|
|
794
|
+
type: 'tool_result',
|
|
795
|
+
callId: outcome.callId,
|
|
796
|
+
toolName: outcome.toolName,
|
|
797
|
+
ok: false,
|
|
798
|
+
summary: summarizeToolResult(outcome.result),
|
|
799
|
+
recoverable: true,
|
|
800
|
+
};
|
|
801
|
+
messages.push({
|
|
802
|
+
role: 'tool',
|
|
803
|
+
toolCallId: requestedToolCall.id,
|
|
804
|
+
content: toolResultMessageContent(outcome.result),
|
|
805
|
+
});
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
const call = parsedToolCall.call;
|
|
809
|
+
const hiddenPlanFirstToolCall = isHiddenPlanFirstToolCall(call, planFirstState, activeToolRegistry);
|
|
810
|
+
const repeatedToolCallOutcome = hiddenPlanFirstToolCall
|
|
811
|
+
? undefined
|
|
812
|
+
: maybeRepeatedToolCallOutcome(call, activeToolRegistry, repeatedToolCallState);
|
|
813
|
+
const preparedToolCall = {
|
|
814
|
+
requestedToolCall,
|
|
815
|
+
call,
|
|
816
|
+
hiddenPlanFirstToolCall,
|
|
817
|
+
...(repeatedToolCallOutcome ? { repeatedToolCallOutcome } : {}),
|
|
818
|
+
};
|
|
819
|
+
if (isConcurrentReadOnlyToolCall(call, activeToolRegistry) && !hiddenPlanFirstToolCall && !repeatedToolCallOutcome) {
|
|
820
|
+
pendingConcurrentToolExecutions.push(preparedToolCall);
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
yield* flushConcurrentToolExecutions();
|
|
824
|
+
if (!hiddenPlanFirstToolCall) {
|
|
825
|
+
yield { type: 'tool_call', callId: call.id, toolName: call.toolName, summary: summarizeToolCall(call) };
|
|
826
|
+
}
|
|
827
|
+
yield* publishCompletedToolCall(await executePreparedToolCall(preparedToolCall));
|
|
828
|
+
}
|
|
829
|
+
yield* flushConcurrentToolExecutions();
|
|
830
|
+
}
|
|
831
|
+
const message = `Maximum model turns reached (maxTurns=${maxTurns}).`;
|
|
832
|
+
sessionStore.append({ type: 'system', level: 'error', content: message });
|
|
833
|
+
yield { type: 'error', sessionId: sessionStore.sessionId, message };
|
|
834
|
+
}
|
|
835
|
+
catch (error) {
|
|
836
|
+
const message = safeErrorMessage(error);
|
|
837
|
+
sessionStore.append({ type: 'system', level: 'error', content: message });
|
|
838
|
+
yield { type: 'error', sessionId: sessionStore.sessionId, message };
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
function normalizeMaxTurns(maxTurns) {
|
|
842
|
+
if (maxTurns === undefined) {
|
|
843
|
+
return defaultMaxTurns;
|
|
844
|
+
}
|
|
845
|
+
if (!Number.isFinite(maxTurns)) {
|
|
846
|
+
return 0;
|
|
847
|
+
}
|
|
848
|
+
return Math.max(0, Math.floor(maxTurns));
|
|
849
|
+
}
|
|
850
|
+
function normalizeToolCallBudget(value, fallback) {
|
|
851
|
+
if (value === undefined) {
|
|
852
|
+
return fallback;
|
|
853
|
+
}
|
|
854
|
+
if (!Number.isFinite(value)) {
|
|
855
|
+
return fallback;
|
|
856
|
+
}
|
|
857
|
+
return Math.max(0, Math.floor(value));
|
|
858
|
+
}
|
|
859
|
+
function toolBudgetExhaustedMessage(toolCallsUsed, maxToolCalls) {
|
|
860
|
+
return `Tool budget exhausted after ${toolCallsUsed} tool calls (maxToolCalls=${maxToolCalls}). Partial work is preserved; ask Remi to continue from this session.`;
|
|
861
|
+
}
|
|
862
|
+
function isConcurrentReadOnlyToolCall(call, registry) {
|
|
863
|
+
const tool = registry.get(call.toolName);
|
|
864
|
+
return (tool?.riskLevel === 'read' &&
|
|
865
|
+
tool.permissionPolicy.requirements.length === 1 &&
|
|
866
|
+
tool.permissionPolicy.requirements[0] === 'filesystem-read');
|
|
867
|
+
}
|
|
868
|
+
function sessionEventsToChatMessages(events, options = {}) {
|
|
869
|
+
const candidates = [];
|
|
870
|
+
let pendingUser;
|
|
871
|
+
for (const event of events) {
|
|
872
|
+
if (event.type === 'user') {
|
|
873
|
+
pendingUser = event.content;
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
if (event.type === 'assistant') {
|
|
877
|
+
if (pendingUser !== undefined) {
|
|
878
|
+
candidates.push({ role: 'user', content: pendingUser });
|
|
879
|
+
pendingUser = undefined;
|
|
880
|
+
}
|
|
881
|
+
candidates.push({ role: 'assistant', content: event.content });
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
if (event.type === 'system' && event.level === 'error') {
|
|
885
|
+
pendingUser = undefined;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
return selectRecentHistoryMessages(candidates, {
|
|
889
|
+
maxMessages: normalizePositiveLimit(options.maxMessages, defaultMaxHistoryMessages),
|
|
890
|
+
maxChars: normalizePositiveLimit(options.maxChars, defaultMaxHistoryChars),
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
function selectRecentHistoryMessages(messages, options) {
|
|
894
|
+
const selected = [];
|
|
895
|
+
let usedChars = 0;
|
|
896
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
897
|
+
if (selected.length >= options.maxMessages || usedChars >= options.maxChars) {
|
|
898
|
+
break;
|
|
899
|
+
}
|
|
900
|
+
const message = messages[index];
|
|
901
|
+
if (!message || message.role === 'system' || message.role === 'tool') {
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
const remainingChars = options.maxChars - usedChars;
|
|
905
|
+
const contentBudget = Math.min(defaultMaxHistoryMessageChars, remainingChars);
|
|
906
|
+
if (contentBudget <= 0) {
|
|
907
|
+
break;
|
|
908
|
+
}
|
|
909
|
+
const content = truncateHistoryContent(message.content, contentBudget);
|
|
910
|
+
usedChars += content.length;
|
|
911
|
+
selected.push({ role: message.role, content });
|
|
912
|
+
}
|
|
913
|
+
return selected.reverse();
|
|
914
|
+
}
|
|
915
|
+
function truncateHistoryContent(content, maxChars) {
|
|
916
|
+
if (content.length <= maxChars) {
|
|
917
|
+
return content;
|
|
918
|
+
}
|
|
919
|
+
if (maxChars <= 64) {
|
|
920
|
+
return content.slice(Math.max(0, content.length - maxChars));
|
|
921
|
+
}
|
|
922
|
+
const notice = '[Earlier message content omitted to stay within Remi context budget.]\n';
|
|
923
|
+
return `${notice}${content.slice(Math.max(0, content.length - (maxChars - notice.length)))}`;
|
|
924
|
+
}
|
|
925
|
+
function normalizePositiveLimit(value, fallback) {
|
|
926
|
+
if (value === undefined || !Number.isFinite(value)) {
|
|
927
|
+
return fallback;
|
|
928
|
+
}
|
|
929
|
+
return Math.max(0, Math.floor(value));
|
|
930
|
+
}
|
|
931
|
+
function mergeProjectInstructionSections(...sectionsToMerge) {
|
|
932
|
+
const sections = sectionsToMerge.filter((section) => Boolean(section && section.trim().length > 0));
|
|
933
|
+
return sections.length > 0 ? sections.join('\n\n') : undefined;
|
|
934
|
+
}
|
|
935
|
+
function projectInstructionSource(projectInstructions, hasRecentFilesystemContext) {
|
|
936
|
+
return hasRecentFilesystemContext ? `${projectInstructions.source}; recent filesystem facts` : projectInstructions.source;
|
|
937
|
+
}
|
|
938
|
+
function buildRecentFilesystemContext(events, cwd, env) {
|
|
939
|
+
const recentEvents = events.slice(-maxFilesystemContextEvents);
|
|
940
|
+
const successfulCallIds = new Set(recentEvents
|
|
941
|
+
.filter((event) => event.type === 'tool_result' && event.ok)
|
|
942
|
+
.map(event => event.callId));
|
|
943
|
+
const rawCandidates = [];
|
|
944
|
+
const pushCandidate = (value) => {
|
|
945
|
+
rawCandidates.push(value);
|
|
946
|
+
};
|
|
947
|
+
for (const event of recentEvents) {
|
|
948
|
+
if (event.type === 'tool_call' && successfulCallIds.has(event.callId)) {
|
|
949
|
+
collectPathCandidates(event.input, pushCandidate);
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
if (event.type === 'tool_result' && event.ok) {
|
|
953
|
+
collectPathCandidatesFromToolResult(event.summary, pushCandidate);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
const home = typeof env.HOME === 'string' && env.HOME.trim().length > 0 ? normalize(resolve(env.HOME)) : undefined;
|
|
957
|
+
const normalizedCwd = normalize(resolve(cwd));
|
|
958
|
+
const absolutePaths = uniqueMostRecent(rawCandidates
|
|
959
|
+
.map(candidate => normalizeFilesystemCandidate(candidate, home))
|
|
960
|
+
.filter((candidate) => Boolean(candidate))
|
|
961
|
+
.filter(candidate => !isSensitiveFilesystemCandidate(candidate))
|
|
962
|
+
.filter(candidate => !isPathInsideOrEqual(candidate, normalizedCwd)), maxFilesystemContextPaths);
|
|
963
|
+
if (absolutePaths.length === 0) {
|
|
964
|
+
return undefined;
|
|
965
|
+
}
|
|
966
|
+
const roots = uniqueMostRecent(absolutePaths
|
|
967
|
+
.map(path => inferFilesystemRoot(path, home))
|
|
968
|
+
.filter((root) => Boolean(root))
|
|
969
|
+
.filter(root => !isPathInsideOrEqual(root, normalizedCwd)), maxFilesystemContextRoots);
|
|
970
|
+
const aliasCounts = countAliases(roots);
|
|
971
|
+
const rootLines = roots.map(root => {
|
|
972
|
+
const alias = basename(root);
|
|
973
|
+
return aliasCounts.get(alias) === 1 ? `- ${alias} => ${root}` : `- ${root}`;
|
|
974
|
+
});
|
|
975
|
+
const pathLines = absolutePaths
|
|
976
|
+
.filter(path => !roots.some(root => path === root))
|
|
977
|
+
.slice(0, maxFilesystemContextPaths)
|
|
978
|
+
.map(path => `- ${path}`);
|
|
979
|
+
return [
|
|
980
|
+
'## Recent Filesystem Locations',
|
|
981
|
+
'These path facts come from recent successful Remi tool activity in this session.',
|
|
982
|
+
'Use them when the user refers again to a project or directory by name without restating its absolute path.',
|
|
983
|
+
...(rootLines.length > 0 ? ['', 'Known project roots:', ...rootLines] : []),
|
|
984
|
+
...(pathLines.length > 0 ? ['', 'Recent absolute paths:', ...pathLines] : []),
|
|
985
|
+
'',
|
|
986
|
+
`Current Remi runtime cwd: ${normalizedCwd}`,
|
|
987
|
+
'Do not create a new directory with the same historical project name under the runtime cwd unless the user explicitly asks for the Remi repository.',
|
|
988
|
+
'For commands that must run inside one of these projects, set the tool cwd to the mapped absolute project root.',
|
|
989
|
+
].join('\n');
|
|
990
|
+
}
|
|
991
|
+
function collectPathCandidates(value, push, key = '', depth = 0) {
|
|
992
|
+
if (depth > 4) {
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
if (typeof value === 'string') {
|
|
996
|
+
if (isPathLikeKey(key)) {
|
|
997
|
+
push(value);
|
|
998
|
+
}
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
if (Array.isArray(value)) {
|
|
1002
|
+
for (const item of value.slice(0, 50)) {
|
|
1003
|
+
collectPathCandidates(item, push, key, depth + 1);
|
|
1004
|
+
}
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
if (!value || typeof value !== 'object') {
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
for (const [childKey, childValue] of Object.entries(value)) {
|
|
1011
|
+
collectPathCandidates(childValue, push, childKey, depth + 1);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
function collectPathCandidatesFromToolResult(summary, push) {
|
|
1015
|
+
const parsed = parseJsonObject(summary);
|
|
1016
|
+
if (parsed !== undefined) {
|
|
1017
|
+
collectPathCandidates(parsed, push);
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
for (const match of summary.matchAll(/(?:~\/|\/Users\/|\/private\/|\/tmp\/|\/var\/)[^\s"'`<>|]+/g)) {
|
|
1021
|
+
if (match[0]) {
|
|
1022
|
+
push(match[0]);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
function parseJsonObject(text) {
|
|
1027
|
+
try {
|
|
1028
|
+
return JSON.parse(text);
|
|
1029
|
+
}
|
|
1030
|
+
catch {
|
|
1031
|
+
return undefined;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
function isPathLikeKey(key) {
|
|
1035
|
+
return key === 'path' || key === 'cwd' || key.endsWith('Path');
|
|
1036
|
+
}
|
|
1037
|
+
function normalizeFilesystemCandidate(raw, home) {
|
|
1038
|
+
const trimmed = raw.trim().replace(/^[("'`]+/g, '').replace(/[)"'`,.;:\]}]+$/g, '');
|
|
1039
|
+
if (trimmed.length === 0) {
|
|
1040
|
+
return undefined;
|
|
1041
|
+
}
|
|
1042
|
+
if (trimmed === '~') {
|
|
1043
|
+
return home;
|
|
1044
|
+
}
|
|
1045
|
+
if (trimmed.startsWith('~/')) {
|
|
1046
|
+
return home ? normalize(resolve(home, trimmed.slice(2))) : undefined;
|
|
1047
|
+
}
|
|
1048
|
+
return isAbsolute(trimmed) ? normalize(resolve(trimmed)) : undefined;
|
|
1049
|
+
}
|
|
1050
|
+
function inferFilesystemRoot(path, home) {
|
|
1051
|
+
if (home) {
|
|
1052
|
+
const desktop = normalize(join(home, 'Desktop'));
|
|
1053
|
+
if (isPathInsideOrEqual(path, desktop)) {
|
|
1054
|
+
const rel = relative(desktop, path);
|
|
1055
|
+
const firstSegment = rel.split(sep).filter(Boolean)[0];
|
|
1056
|
+
return firstSegment ? normalize(join(desktop, firstSegment)) : desktop;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
const name = basename(path);
|
|
1060
|
+
return name.includes('.') ? dirname(path) : path;
|
|
1061
|
+
}
|
|
1062
|
+
function countAliases(paths) {
|
|
1063
|
+
const counts = new Map();
|
|
1064
|
+
for (const path of paths) {
|
|
1065
|
+
const alias = basename(path);
|
|
1066
|
+
counts.set(alias, (counts.get(alias) ?? 0) + 1);
|
|
1067
|
+
}
|
|
1068
|
+
return counts;
|
|
1069
|
+
}
|
|
1070
|
+
function uniqueMostRecent(values, limit) {
|
|
1071
|
+
const selected = [];
|
|
1072
|
+
const seen = new Set();
|
|
1073
|
+
for (let index = values.length - 1; index >= 0; index -= 1) {
|
|
1074
|
+
const value = values[index];
|
|
1075
|
+
if (!value || seen.has(value)) {
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
seen.add(value);
|
|
1079
|
+
selected.unshift(value);
|
|
1080
|
+
if (selected.length >= limit) {
|
|
1081
|
+
break;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
return selected;
|
|
1085
|
+
}
|
|
1086
|
+
function isPathInsideOrEqual(target, root) {
|
|
1087
|
+
const rel = relative(root, target);
|
|
1088
|
+
return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
|
|
1089
|
+
}
|
|
1090
|
+
function isSensitiveFilesystemCandidate(path) {
|
|
1091
|
+
const lowerName = basename(path).toLowerCase();
|
|
1092
|
+
return (lowerName === '.env' ||
|
|
1093
|
+
lowerName.startsWith('.env.') ||
|
|
1094
|
+
lowerName === '.npmrc' ||
|
|
1095
|
+
lowerName === '.netrc' ||
|
|
1096
|
+
lowerName.includes('token') ||
|
|
1097
|
+
lowerName.includes('secret') ||
|
|
1098
|
+
lowerName.endsWith('.pem') ||
|
|
1099
|
+
lowerName.endsWith('.key'));
|
|
1100
|
+
}
|
|
1101
|
+
function shouldOfferToolsForInput(input) {
|
|
1102
|
+
const normalized = normalizeIntentText(input);
|
|
1103
|
+
if (normalized.length === 0) {
|
|
1104
|
+
return false;
|
|
1105
|
+
}
|
|
1106
|
+
if (isTextOnlyPlanRequest(normalized)) {
|
|
1107
|
+
return false;
|
|
1108
|
+
}
|
|
1109
|
+
return !/^(你好|您好|嗨|哈喽|hello|hi|hey|thanks|thank you|谢谢|多谢|测试|test)$/.test(normalized);
|
|
1110
|
+
}
|
|
1111
|
+
function shouldUseReadOnlyToolSurface(input, planExecutionApproval, requiredMutationTools) {
|
|
1112
|
+
const normalized = normalizeIntentText(input);
|
|
1113
|
+
if (!shouldOfferToolsForInput(input) || normalized.length === 0) {
|
|
1114
|
+
return false;
|
|
1115
|
+
}
|
|
1116
|
+
if (planExecutionApproval || requiredMutationTools.length > 0) {
|
|
1117
|
+
return false;
|
|
1118
|
+
}
|
|
1119
|
+
if (isAdvisoryOnlyRequest(normalized) || isTextOnlyPlanRequest(normalized)) {
|
|
1120
|
+
return true;
|
|
1121
|
+
}
|
|
1122
|
+
if (fullToolSurfaceIntentPattern().test(normalized)) {
|
|
1123
|
+
return false;
|
|
1124
|
+
}
|
|
1125
|
+
return readOnlyToolSurfaceIntentPattern().test(normalized);
|
|
1126
|
+
}
|
|
1127
|
+
function readOnlyToolSurfaceIntentPattern() {
|
|
1128
|
+
return /(看一下|看看|查看|读取|读一下|列出|打开|搜索|查找|解释|说明|总结|分析|审查|看看有什么|有什么东西)|\b(?:read|show|list|open|search|find|inspect|explain|summari[sz]e|analy[sz]e|review|what is|what are|how does|why does)\b/i;
|
|
1129
|
+
}
|
|
1130
|
+
function fullToolSurfaceIntentPattern() {
|
|
1131
|
+
return /(实现|修改|改成|改为|更新|写入|写一个|写个|创建|新建|生成|添加|新增|增加|删除|移除|删掉|修复|推进|执行|运行|验证|测试|编译|构建|提交)|\b(?:implement|edit|modify|update|write|create|generate|add|delete|remove|fix|repair|execute|run|verify|test|build|compile|commit)\b/i;
|
|
1132
|
+
}
|
|
1133
|
+
function shouldOfferMemoryForInput(input) {
|
|
1134
|
+
return shouldOfferToolsForInput(input);
|
|
1135
|
+
}
|
|
1136
|
+
function shouldBufferSkillDecisionGuard(hasEnabledSkills, loadedSkillNames, prompted) {
|
|
1137
|
+
return hasEnabledSkills && loadedSkillNames.size === 0 && !prompted;
|
|
1138
|
+
}
|
|
1139
|
+
function shouldRetrySkillDecisionGuard(hasEnabledSkills, loadedSkillNames, prompted, requestedToolCalls, modelTurns, maxTurns) {
|
|
1140
|
+
return (hasEnabledSkills &&
|
|
1141
|
+
loadedSkillNames.size === 0 &&
|
|
1142
|
+
!prompted &&
|
|
1143
|
+
modelTurns < maxTurns &&
|
|
1144
|
+
requestedToolCalls.length > 0 &&
|
|
1145
|
+
requestedToolCalls.every(toolCall => toolCall.name !== loadSkillToolName));
|
|
1146
|
+
}
|
|
1147
|
+
function buildSkillDecisionGuardPrompt(input, requestedToolCalls, language) {
|
|
1148
|
+
const toolNames = [...new Set(requestedToolCalls.map(toolCall => toolCall.name))].join(', ');
|
|
1149
|
+
return [
|
|
1150
|
+
'Runtime guard: before using task tools, decide whether one of the available Remi Skills applies.',
|
|
1151
|
+
runtimeLanguageRequirement(language),
|
|
1152
|
+
`Original user request: ${input}`,
|
|
1153
|
+
`You just requested tool(s): ${toolNames}. Those tool calls were not executed yet.`,
|
|
1154
|
+
'Review the Remi Skills index already provided in the system context.',
|
|
1155
|
+
'If a listed skill semantically matches the request or the next action, call load_skill with that skill name now, and do not call other tools in the same assistant turn.',
|
|
1156
|
+
'If no listed skill matches or the match is ambiguous, do not call load_skill; repeat the appropriate original tool call(s) and continue.',
|
|
1157
|
+
'The runtime will not choose a skill for you. Only claim a skill was used after load_skill succeeds.',
|
|
1158
|
+
].join('\n');
|
|
1159
|
+
}
|
|
1160
|
+
function isExplicitSkillInvocationPrompt(input) {
|
|
1161
|
+
return /^Use the Remi skill `[^`]+` for this turn\.\nSkill source: .+\n/.test(input) && input.includes('\nSkill instructions:\n');
|
|
1162
|
+
}
|
|
1163
|
+
function planPromptMode(planMode, requirement) {
|
|
1164
|
+
if (planMode) {
|
|
1165
|
+
return 'manual';
|
|
1166
|
+
}
|
|
1167
|
+
return requirement ? 'required' : undefined;
|
|
1168
|
+
}
|
|
1169
|
+
function createPlanFirstGuardState(requirement) {
|
|
1170
|
+
return {
|
|
1171
|
+
...(requirement ? { requirement } : {}),
|
|
1172
|
+
satisfied: false,
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
function recordPlanFirstToolOutcome(state) {
|
|
1176
|
+
if (state.requirement) {
|
|
1177
|
+
state.satisfied = true;
|
|
1178
|
+
return state.requirement.executeAfterPlan;
|
|
1179
|
+
}
|
|
1180
|
+
return false;
|
|
1181
|
+
}
|
|
1182
|
+
function shouldBufferPlanFirstText(state) {
|
|
1183
|
+
return Boolean(state.requirement && (!state.satisfied || !state.requirement.executeAfterPlan));
|
|
1184
|
+
}
|
|
1185
|
+
function shouldRetryMissingPlanFirst(state, retries, modelTurns, maxTurns) {
|
|
1186
|
+
return Boolean(state.requirement && !state.satisfied && retries < defaultPlanFirstGuardRetries && modelTurns < maxTurns);
|
|
1187
|
+
}
|
|
1188
|
+
function shouldFailMissingPlanFirst(state) {
|
|
1189
|
+
return Boolean(state.requirement && !state.satisfied);
|
|
1190
|
+
}
|
|
1191
|
+
function buildPlanFirstGuardPrompt(input, state, language) {
|
|
1192
|
+
return [
|
|
1193
|
+
'Runtime guard: this task requires a visible plan before implementation.',
|
|
1194
|
+
runtimeLanguageRequirement(language),
|
|
1195
|
+
`Original user request: ${input}`,
|
|
1196
|
+
`Reason: ${state.requirement?.reason ?? 'multi-step or risky implementation request'}.`,
|
|
1197
|
+
'Inspect with read-only tools only if needed, then call todo_write with the complete current checklist.',
|
|
1198
|
+
'Do not edit files, create/delete files, run build/test commands, or claim implementation work is complete in this turn.',
|
|
1199
|
+
state.requirement?.executeAfterPlan
|
|
1200
|
+
? 'After todo_write, continue implementing the original request with normal tools unless you are blocked or need clarification.'
|
|
1201
|
+
: 'After todo_write, briefly ask the user to approve or refine the plan.',
|
|
1202
|
+
].join('\n');
|
|
1203
|
+
}
|
|
1204
|
+
function planFirstGuardFailureMessage(state) {
|
|
1205
|
+
return `I couldn't start implementation because this task needs a visible plan first. Missing successful todo_write for: ${state.requirement?.reason ?? 'multi-step or risky implementation request'}.`;
|
|
1206
|
+
}
|
|
1207
|
+
function planFirstCompletionClaimIssue(text, state) {
|
|
1208
|
+
if (!state.requirement) {
|
|
1209
|
+
return undefined;
|
|
1210
|
+
}
|
|
1211
|
+
if (state.requirement.executeAfterPlan && state.satisfied) {
|
|
1212
|
+
return undefined;
|
|
1213
|
+
}
|
|
1214
|
+
if (mutationCompletionClaimKind(text)) {
|
|
1215
|
+
return 'implementation';
|
|
1216
|
+
}
|
|
1217
|
+
if (verificationSuccessClaimKind(text)) {
|
|
1218
|
+
return 'verification';
|
|
1219
|
+
}
|
|
1220
|
+
return undefined;
|
|
1221
|
+
}
|
|
1222
|
+
function buildPlanFirstClaimGuardPrompt(input, draft, state, issue, language) {
|
|
1223
|
+
const issueLabel = issue === 'implementation' ? 'implementation or filesystem changes' : 'verification, build, test, or execution success';
|
|
1224
|
+
return [
|
|
1225
|
+
'Runtime guard: this is a plan-first turn, so the final answer must not claim implementation evidence.',
|
|
1226
|
+
runtimeLanguageRequirement(language),
|
|
1227
|
+
`Original user request: ${input}`,
|
|
1228
|
+
`Plan-first reason: ${state.requirement?.reason ?? 'multi-step or risky implementation request'}.`,
|
|
1229
|
+
`Blocked draft: ${compactText(draft, 800)}`,
|
|
1230
|
+
`Unsupported claim: ${issueLabel}.`,
|
|
1231
|
+
state.requirement?.executeAfterPlan
|
|
1232
|
+
? 'Revise the answer so it does not claim implementation evidence before actual tool results. Continue implementation when normal tools become available.'
|
|
1233
|
+
: 'Revise the answer to say that the plan is ready and ask the user to approve or refine it.',
|
|
1234
|
+
].join('\n');
|
|
1235
|
+
}
|
|
1236
|
+
function buildPlanFirstContinuationPrompt(input, language) {
|
|
1237
|
+
return [
|
|
1238
|
+
'Runtime: the visible plan has been recorded.',
|
|
1239
|
+
runtimeLanguageRequirement(language),
|
|
1240
|
+
`Continue implementing the original user request now: ${input}`,
|
|
1241
|
+
'Normal implementation tools are now available.',
|
|
1242
|
+
'Do not ask for confirmation unless the plan revealed an ambiguity, destructive action, or permission blocker.',
|
|
1243
|
+
].join('\n');
|
|
1244
|
+
}
|
|
1245
|
+
function buildPlanExecutionApprovalPrompt(input, approval, language, retry = false) {
|
|
1246
|
+
const planLines = approval?.items.length
|
|
1247
|
+
? approval.items.map((item, index) => `${index + 1}. [${item.status}] ${item.content}`)
|
|
1248
|
+
: ['(No plan items were available in the recent transcript.)'];
|
|
1249
|
+
return [
|
|
1250
|
+
retry
|
|
1251
|
+
? 'Runtime guard: the user already approved the recent visible plan, but the last response did not start execution.'
|
|
1252
|
+
: 'Runtime: the user has approved the recent visible plan for execution.',
|
|
1253
|
+
runtimeLanguageRequirement(language),
|
|
1254
|
+
`Current user approval: ${input}`,
|
|
1255
|
+
'Execute the latest recorded plan now with normal tools.',
|
|
1256
|
+
'Do not recreate the same plan, do not ask for approval again, and do not stop after todo_write unless a real blocker or destructive ambiguity appears.',
|
|
1257
|
+
'Use todo_write only to update item status while executing.',
|
|
1258
|
+
'Latest visible plan:',
|
|
1259
|
+
...planLines,
|
|
1260
|
+
].join('\n');
|
|
1261
|
+
}
|
|
1262
|
+
function shouldRetryPlanExecutionApproval(approval, draft, retries, modelTurns, maxTurns) {
|
|
1263
|
+
if (!approval || retries >= defaultPlanFirstGuardRetries || modelTurns >= maxTurns) {
|
|
1264
|
+
return false;
|
|
1265
|
+
}
|
|
1266
|
+
return asksForPlanApprovalAgain(draft);
|
|
1267
|
+
}
|
|
1268
|
+
function shouldRetryPostActionApprovalDraft(draft, mutationState, verificationState, retries, modelTurns, maxTurns) {
|
|
1269
|
+
if (retries >= defaultPlanFirstGuardRetries || modelTurns >= maxTurns) {
|
|
1270
|
+
return false;
|
|
1271
|
+
}
|
|
1272
|
+
return hasCurrentTurnActionEvidence(mutationState, verificationState) && asksToStartAfterApproval(draft);
|
|
1273
|
+
}
|
|
1274
|
+
function buildPostActionApprovalGuardPrompt(input, draft, mutationState, verificationState, language) {
|
|
1275
|
+
return [
|
|
1276
|
+
'Runtime guard: the current turn already has successful execution evidence, but the draft asks the user to confirm before starting.',
|
|
1277
|
+
runtimeLanguageRequirement(language),
|
|
1278
|
+
`Original user request: ${input}`,
|
|
1279
|
+
`Blocked draft: ${compactText(draft, 800)}`,
|
|
1280
|
+
`Current evidence: ${formatCurrentTurnActionEvidence(mutationState, verificationState)}.`,
|
|
1281
|
+
'Revise the final answer as a completed-work summary grounded in current-turn tool results.',
|
|
1282
|
+
'Do not ask the user to confirm before starting work that already happened.',
|
|
1283
|
+
].join('\n');
|
|
1284
|
+
}
|
|
1285
|
+
function asksForPlanApprovalAgain(text) {
|
|
1286
|
+
const normalized = normalizeIntentText(text);
|
|
1287
|
+
if (normalized.length === 0) {
|
|
1288
|
+
return true;
|
|
1289
|
+
}
|
|
1290
|
+
return /(批准后|确认后|同意后|允许后|如果.*批准|请.*批准|请.*确认|是否.*执行|要我.*执行|再执行|我再执行|开始改|开始执行|approve|approval|confirm|shall i|should i)/i.test(normalized);
|
|
1291
|
+
}
|
|
1292
|
+
function asksToStartAfterApproval(text) {
|
|
1293
|
+
const normalized = normalizeIntentText(text);
|
|
1294
|
+
if (normalized.length === 0) {
|
|
1295
|
+
return false;
|
|
1296
|
+
}
|
|
1297
|
+
return /(确认后|批准后|同意后|允许后|等确认|如果.{0,12}确认|请.{0,12}确认).{0,32}(开始|执行|实现|修改|改|写|创建|删除|运行|验证|测试|做|弄|搞)|(开始|执行|实现|修改|改|写|创建|删除|运行|验证|测试|做|弄|搞).{0,32}(前|之前).{0,12}(确认|批准|同意)|\b(?:after|once|when)\b.{0,24}\b(?:confirm|approve|approval)\b.{0,32}\b(?:start|execute|implement|edit|modify|write|create|delete|run|verify|test)\b|\b(?:confirm|approve)\b.{0,32}\b(?:and|then|before)\b.{0,32}\b(?:start|execute|implement|edit|modify|write|create|delete|run|verify|test)\b/i.test(normalized);
|
|
1298
|
+
}
|
|
1299
|
+
function planFirstClaimGuardFailureMessage(issue) {
|
|
1300
|
+
const label = issue === 'implementation' ? 'implementation completion' : 'verification success';
|
|
1301
|
+
return `Assistant attempted to claim ${label} during a plan-first turn.`;
|
|
1302
|
+
}
|
|
1303
|
+
function isHiddenPlanFirstToolCall(call, state, activeToolRegistry) {
|
|
1304
|
+
return Boolean(state.requirement && !state.satisfied && !activeToolRegistry.get(call.toolName));
|
|
1305
|
+
}
|
|
1306
|
+
function planFirstUnavailableToolOutcome(call) {
|
|
1307
|
+
const reason = [
|
|
1308
|
+
`Tool ${call.toolName} is not available during this plan-first phase.`,
|
|
1309
|
+
'Use read-only inspection if needed, then call todo_write with the complete plan.',
|
|
1310
|
+
'Do not edit files, delete files, or run build/test commands until the plan has been recorded.',
|
|
1311
|
+
].join(' ');
|
|
1312
|
+
const decision = {
|
|
1313
|
+
status: 'deny',
|
|
1314
|
+
reason,
|
|
1315
|
+
requirements: [],
|
|
1316
|
+
};
|
|
1317
|
+
const result = {
|
|
1318
|
+
ok: false,
|
|
1319
|
+
callId: call.id,
|
|
1320
|
+
toolName: call.toolName,
|
|
1321
|
+
error: {
|
|
1322
|
+
code: 'PLAN_FIRST_TOOL_NOT_AVAILABLE',
|
|
1323
|
+
message: reason,
|
|
1324
|
+
},
|
|
1325
|
+
};
|
|
1326
|
+
return {
|
|
1327
|
+
dryRun: false,
|
|
1328
|
+
callId: call.id,
|
|
1329
|
+
toolName: call.toolName,
|
|
1330
|
+
permissionDecision: decision,
|
|
1331
|
+
transcriptEvents: [],
|
|
1332
|
+
result,
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
function createRepeatedToolCallState() {
|
|
1336
|
+
return { counts: new Map() };
|
|
1337
|
+
}
|
|
1338
|
+
function maybeRepeatedToolCallOutcome(call, registry, state) {
|
|
1339
|
+
if (isMutationLikeTool(call.toolName)) {
|
|
1340
|
+
state.counts.clear();
|
|
1341
|
+
return undefined;
|
|
1342
|
+
}
|
|
1343
|
+
const signature = stableToolCallSignature(call);
|
|
1344
|
+
const count = (state.counts.get(signature) ?? 0) + 1;
|
|
1345
|
+
state.counts.set(signature, count);
|
|
1346
|
+
if (count <= maxIdenticalToolCallsBeforeSuppression) {
|
|
1347
|
+
return undefined;
|
|
1348
|
+
}
|
|
1349
|
+
const reason = [
|
|
1350
|
+
`Repeated identical ${call.toolName} call suppressed after ${count - 1} previous attempt(s).`,
|
|
1351
|
+
'Use the previous tool result, or retry with a different path, query, glob, or command if new information is needed.',
|
|
1352
|
+
].join(' ');
|
|
1353
|
+
const tool = registry.get(call.toolName);
|
|
1354
|
+
const decision = {
|
|
1355
|
+
status: 'deny',
|
|
1356
|
+
reason,
|
|
1357
|
+
requirements: tool?.permissionPolicy.requirements ?? [],
|
|
1358
|
+
};
|
|
1359
|
+
const result = {
|
|
1360
|
+
ok: false,
|
|
1361
|
+
callId: call.id,
|
|
1362
|
+
toolName: call.toolName,
|
|
1363
|
+
error: {
|
|
1364
|
+
code: 'REPEATED_TOOL_CALL',
|
|
1365
|
+
message: reason,
|
|
1366
|
+
},
|
|
1367
|
+
};
|
|
1368
|
+
return {
|
|
1369
|
+
dryRun: false,
|
|
1370
|
+
callId: call.id,
|
|
1371
|
+
toolName: call.toolName,
|
|
1372
|
+
permissionDecision: decision,
|
|
1373
|
+
transcriptEvents: [
|
|
1374
|
+
...(tool
|
|
1375
|
+
? [
|
|
1376
|
+
{
|
|
1377
|
+
type: 'tool_call',
|
|
1378
|
+
callId: call.id,
|
|
1379
|
+
toolName: call.toolName,
|
|
1380
|
+
input: call.input,
|
|
1381
|
+
riskLevel: tool.riskLevel,
|
|
1382
|
+
permissionPolicy: tool.permissionPolicy,
|
|
1383
|
+
status: 'denied',
|
|
1384
|
+
},
|
|
1385
|
+
]
|
|
1386
|
+
: []),
|
|
1387
|
+
{
|
|
1388
|
+
type: 'tool_result',
|
|
1389
|
+
callId: call.id,
|
|
1390
|
+
toolName: call.toolName,
|
|
1391
|
+
ok: false,
|
|
1392
|
+
summary: reason,
|
|
1393
|
+
errorCode: 'REPEATED_TOOL_CALL',
|
|
1394
|
+
},
|
|
1395
|
+
],
|
|
1396
|
+
result,
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
function isMutationLikeTool(toolName) {
|
|
1400
|
+
return (toolName === 'create_directory' ||
|
|
1401
|
+
toolName === 'edit_file' ||
|
|
1402
|
+
toolName === 'write_file' ||
|
|
1403
|
+
toolName === 'delete_file' ||
|
|
1404
|
+
toolName === 'delete_directory' ||
|
|
1405
|
+
toolName === 'execute_shell');
|
|
1406
|
+
}
|
|
1407
|
+
function stableToolCallSignature(call) {
|
|
1408
|
+
return `${call.toolName}:${stableJsonStringify(call.input)}`;
|
|
1409
|
+
}
|
|
1410
|
+
function stableJsonStringify(value) {
|
|
1411
|
+
if (Array.isArray(value)) {
|
|
1412
|
+
return `[${value.map(item => stableJsonStringify(item)).join(',')}]`;
|
|
1413
|
+
}
|
|
1414
|
+
if (value && typeof value === 'object') {
|
|
1415
|
+
const record = value;
|
|
1416
|
+
return `{${Object.keys(record)
|
|
1417
|
+
.sort()
|
|
1418
|
+
.map(key => `${JSON.stringify(key)}:${stableJsonStringify(record[key])}`)
|
|
1419
|
+
.join(',')}}`;
|
|
1420
|
+
}
|
|
1421
|
+
return JSON.stringify(value);
|
|
1422
|
+
}
|
|
1423
|
+
function planFirstRequirementForInput(input, events) {
|
|
1424
|
+
const normalized = normalizeIntentText(input);
|
|
1425
|
+
if (!shouldOfferToolsForInput(input) || normalized.length === 0) {
|
|
1426
|
+
return undefined;
|
|
1427
|
+
}
|
|
1428
|
+
if (isPlanExecutionConsent(normalized) && hasRecentSuccessfulPlan(events)) {
|
|
1429
|
+
return undefined;
|
|
1430
|
+
}
|
|
1431
|
+
if (simpleLowRiskRequestPattern().test(normalized)) {
|
|
1432
|
+
return undefined;
|
|
1433
|
+
}
|
|
1434
|
+
if (isAdvisoryOnlyRequest(normalized)) {
|
|
1435
|
+
return undefined;
|
|
1436
|
+
}
|
|
1437
|
+
if (explicitPlanningRequestPattern().test(normalized)) {
|
|
1438
|
+
return { reason: 'the user asked for a plan or implementation approach', executeAfterPlan: false };
|
|
1439
|
+
}
|
|
1440
|
+
if (crossLanguageOrMigrationPattern().test(normalized)) {
|
|
1441
|
+
return { reason: 'cross-language migration, rewrite, or broad refactor', executeAfterPlan: true };
|
|
1442
|
+
}
|
|
1443
|
+
if (newServicePattern().test(normalized)) {
|
|
1444
|
+
return { reason: 'new service or API scaffolding spans multiple design choices', executeAfterPlan: true };
|
|
1445
|
+
}
|
|
1446
|
+
if (multiAlgorithmPattern().test(normalized)) {
|
|
1447
|
+
return { reason: 'multi-algorithm or capability expansion is likely multi-step', executeAfterPlan: true };
|
|
1448
|
+
}
|
|
1449
|
+
if (broadProjectChangePattern().test(normalized)) {
|
|
1450
|
+
return { reason: 'broad project-level implementation request', executeAfterPlan: true };
|
|
1451
|
+
}
|
|
1452
|
+
return undefined;
|
|
1453
|
+
}
|
|
1454
|
+
function planExecutionApprovalForInput(input, events) {
|
|
1455
|
+
const normalized = normalizeIntentText(input);
|
|
1456
|
+
if (!isPlanExecutionConsent(normalized)) {
|
|
1457
|
+
return undefined;
|
|
1458
|
+
}
|
|
1459
|
+
const items = latestSuccessfulPlanItems(events);
|
|
1460
|
+
return items ? { items } : undefined;
|
|
1461
|
+
}
|
|
1462
|
+
function normalizeIntentText(input) {
|
|
1463
|
+
return input.trim().replace(/\s+/g, ' ').replace(/[。!?!?.,,、\s]+$/g, '').toLowerCase();
|
|
1464
|
+
}
|
|
1465
|
+
function resolveResponseLanguage(input) {
|
|
1466
|
+
return /[\u3400-\u9fff]/.test(input) ? 'zh-Hans' : undefined;
|
|
1467
|
+
}
|
|
1468
|
+
function runtimeLanguageRequirement(language) {
|
|
1469
|
+
if (language === 'zh-Hans') {
|
|
1470
|
+
return 'Language requirement: user-facing prose, visible plan items, brief transition text, blockers, and final answer must be in Simplified Chinese. Do not use English filler or mixed-language lead-ins such as "Let me", "First", "I will", or "I need to"; write those clauses in Chinese instead. Keep code identifiers, file paths, commands, and tool JSON unchanged.';
|
|
1471
|
+
}
|
|
1472
|
+
if (language === 'en') {
|
|
1473
|
+
return 'Language requirement: user-facing prose, visible plan items, brief transition text, blockers, and final answer must be in English. Keep code identifiers, file paths, commands, and tool JSON unchanged.';
|
|
1474
|
+
}
|
|
1475
|
+
return 'Language requirement: match the latest user message language for user-facing prose, visible plan items, brief transition text, blockers, and final answer. If the latest user message is Chinese, use Simplified Chinese and do not use English filler or mixed-language lead-ins such as "Let me", "First", "I will", or "I need to"; write those clauses in Chinese instead. Keep code identifiers, file paths, commands, and tool JSON unchanged.';
|
|
1476
|
+
}
|
|
1477
|
+
function isPlanExecutionConsent(normalized) {
|
|
1478
|
+
return /(按计划|照计划|执行计划|开始执行|开始实现|继续执行|继续实现|可以执行|可以开始|执行吧|开始吧|实现吧|改吧|做吧|弄吧|搞吧|改|做|弄|搞|批准|同意|继续|可以|好|好的|行|开干|走起)|\b(?:ok|okay|yes|yep|proceed|continue|go ahead|approved|approve|start implementing|implement the plan|execute the plan)\b/i.test(normalized);
|
|
1479
|
+
}
|
|
1480
|
+
function hasRecentSuccessfulPlan(events) {
|
|
1481
|
+
return Boolean(latestSuccessfulPlanItems(events));
|
|
1482
|
+
}
|
|
1483
|
+
function latestSuccessfulPlanItems(events) {
|
|
1484
|
+
const successfulTodoCallIds = new Set(events
|
|
1485
|
+
.filter((event) => event.type === 'tool_result' && event.toolName === 'todo_write' && event.ok)
|
|
1486
|
+
.map(event => event.callId));
|
|
1487
|
+
if (successfulTodoCallIds.size === 0) {
|
|
1488
|
+
return undefined;
|
|
1489
|
+
}
|
|
1490
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
1491
|
+
const event = events[index];
|
|
1492
|
+
if (event?.type === 'user') {
|
|
1493
|
+
return undefined;
|
|
1494
|
+
}
|
|
1495
|
+
if (event?.type === 'system' && event.level === 'error' && isInterruptedSystemEvent(event)) {
|
|
1496
|
+
return undefined;
|
|
1497
|
+
}
|
|
1498
|
+
if (event?.type === 'tool_call' && event.toolName === 'todo_write' && successfulTodoCallIds.has(event.callId)) {
|
|
1499
|
+
return planItemsFromRawItems(event.input['items']);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
return undefined;
|
|
1503
|
+
}
|
|
1504
|
+
function planItemsFromRawItems(rawItems) {
|
|
1505
|
+
if (!Array.isArray(rawItems)) {
|
|
1506
|
+
return undefined;
|
|
1507
|
+
}
|
|
1508
|
+
const items = [];
|
|
1509
|
+
for (const rawItem of rawItems) {
|
|
1510
|
+
if (!rawItem || typeof rawItem !== 'object' || Array.isArray(rawItem)) {
|
|
1511
|
+
return undefined;
|
|
1512
|
+
}
|
|
1513
|
+
const record = rawItem;
|
|
1514
|
+
const content = record['content'];
|
|
1515
|
+
const status = record['status'];
|
|
1516
|
+
const id = record['id'];
|
|
1517
|
+
if (typeof content !== 'string' ||
|
|
1518
|
+
(status !== 'pending' && status !== 'in_progress' && status !== 'completed') ||
|
|
1519
|
+
(id !== undefined && typeof id !== 'string')) {
|
|
1520
|
+
return undefined;
|
|
1521
|
+
}
|
|
1522
|
+
items.push({
|
|
1523
|
+
...(typeof id === 'string' ? { id } : {}),
|
|
1524
|
+
content,
|
|
1525
|
+
status,
|
|
1526
|
+
});
|
|
1527
|
+
}
|
|
1528
|
+
return items;
|
|
1529
|
+
}
|
|
1530
|
+
function isInterruptedSystemEvent(event) {
|
|
1531
|
+
return event.level === 'error' && /(interrupted by user|abort(?:ed)?)/i.test(event.content);
|
|
1532
|
+
}
|
|
1533
|
+
function simpleLowRiskRequestPattern() {
|
|
1534
|
+
return /^(创建|新建|建立).{0,24}(空)?(目录|文件夹|folder|directory|dir)$|^(读|读取|查看|看看|列出|打开|搜索|查找).{0,80}$|\b(?:read|show|list|open|search|find)\b.{0,80}$/i;
|
|
1535
|
+
}
|
|
1536
|
+
function isAdvisoryOnlyRequest(normalized) {
|
|
1537
|
+
if (!advisoryRequestPattern().test(normalized)) {
|
|
1538
|
+
return false;
|
|
1539
|
+
}
|
|
1540
|
+
return !advisoryPlusActionPattern().test(normalized);
|
|
1541
|
+
}
|
|
1542
|
+
function isTextOnlyPlanRequest(normalized) {
|
|
1543
|
+
if (!explicitPlanningRequestPattern().test(normalized)) {
|
|
1544
|
+
return false;
|
|
1545
|
+
}
|
|
1546
|
+
if (textOnlyPlanActionPattern().test(normalized)) {
|
|
1547
|
+
return false;
|
|
1548
|
+
}
|
|
1549
|
+
return textOnlyPlanQualifierPattern().test(normalized);
|
|
1550
|
+
}
|
|
1551
|
+
function advisoryRequestPattern() {
|
|
1552
|
+
return /(推荐|建议|你觉得|应该|适合|优先|加什么|添加什么|新增什么|选什么|要不要|是否|可不可以|能不能)|\b(?:recommend|suggest|should|what should|which .* should|advice|what would .* add|which .* add)\b/i;
|
|
1553
|
+
}
|
|
1554
|
+
function advisoryPlusActionPattern() {
|
|
1555
|
+
return /(并|然后|顺便|直接|现在|立刻).{0,16}(添加|新增|增加|实现|修改|创建|写入|删除)|\b(?:and|then|also|directly|now)\b.{0,24}\b(?:add|implement|create|write|edit|modify|delete)\b/i;
|
|
1556
|
+
}
|
|
1557
|
+
function explicitPlanningRequestPattern() {
|
|
1558
|
+
return /(先)?(计划|规划|方案|设计方案|实施方案|执行方案|实现方案|技术方案)|\b(?:plan|planning|implementation plan|approach|design an approach)\b/i;
|
|
1559
|
+
}
|
|
1560
|
+
function textOnlyPlanQualifierPattern() {
|
|
1561
|
+
return /(模拟|示例|样例|例子|假设|随便|看看|看一下|列一个|列出|简单输出|输出.{0,12}文字|只.{0,8}输出|仅.{0,8}输出|不用执行|不要执行|不执行|别执行)|\b(?:mock|sample|example|hypothetical|just show|only output|text only|do not execute|don't execute|no execution)\b/i;
|
|
1562
|
+
}
|
|
1563
|
+
function textOnlyPlanActionPattern() {
|
|
1564
|
+
return /(然后|并|直接|现在|立刻).{0,24}(执行|实现|修改|改|写|创建|删除|运行|验证|测试)|\b(?:and|then|directly|now)\b.{0,32}\b(?:execute|implement|edit|modify|write|create|delete|run|verify|test)\b/i;
|
|
1565
|
+
}
|
|
1566
|
+
function crossLanguageOrMigrationPattern() {
|
|
1567
|
+
return /(改成|改为|换成|转换成|迁移到|移植到|重写成|改造成).{0,36}(c\+\+|cpp|go|golang|rust|python|typescript|ts|javascript|js|java|语言|实现)|\b(?:convert|port|migrate|rewrite|refactor)\b.{0,48}\b(?:c\+\+|cpp|go|golang|rust|python|typescript|javascript|java|implementation)\b/i;
|
|
1568
|
+
}
|
|
1569
|
+
function newServicePattern() {
|
|
1570
|
+
return /(创建|新建|新增|增加|实现|搭建).{0,36}(服务|后端|接口|api|微服务|service|backend|server|worker)|\b(?:create|build|implement|scaffold|add)\b.{0,48}\b(?:service|api|backend|server|worker)\b/i;
|
|
1571
|
+
}
|
|
1572
|
+
function multiAlgorithmPattern() {
|
|
1573
|
+
return /(添加|新增|增加|补充|加入|实现|加|更多).{0,36}(算法|排序|查找|搜索|search|sort|sorting|algorithm)|(算法|排序|查找|搜索).{0,24}(更多|多个|一批|若干|相关|家族)|\b(?:add|implement)\b.{0,48}\b(?:algorithms?|sorts?|sorting|search)\b/i;
|
|
1574
|
+
}
|
|
1575
|
+
function broadProjectChangePattern() {
|
|
1576
|
+
return /(重构|架构|大改|整体|整个|全量|端到端|系统性|拆分|模块化|改造).{0,60}(项目|工程|代码|实现|模块|目录|架构)?|\b(?:large-scale|project-wide|end-to-end|architecture|architectural|restructure|modularize)\b/i;
|
|
1577
|
+
}
|
|
1578
|
+
function requiredMutationToolsForInput(input, toolDefinitions) {
|
|
1579
|
+
if (!shouldOfferToolsForInput(input)) {
|
|
1580
|
+
return [];
|
|
1581
|
+
}
|
|
1582
|
+
if (isAdvisoryOnlyRequest(normalizeIntentText(input))) {
|
|
1583
|
+
return [];
|
|
1584
|
+
}
|
|
1585
|
+
const availableTools = new Set(toolDefinitions.map(tool => tool.name));
|
|
1586
|
+
if (availableTools.size === 0) {
|
|
1587
|
+
return [];
|
|
1588
|
+
}
|
|
1589
|
+
const normalized = input.trim().toLowerCase();
|
|
1590
|
+
const required = [];
|
|
1591
|
+
const hasDeleteTerm = /(删除|移除|删掉|去掉)/.test(normalized) || /\b(delete|remove)\b/.test(normalized);
|
|
1592
|
+
const hasContentRemovalContext = /(部分|片段|段落|段|代码|逻辑|函数|方法|内容|行|块|区块|章节|小节|注释|批注|备注|log|日志|section|part|snippet|block|code|logic|function|method|content|line|lines|comment|comments|logging)/.test(normalized);
|
|
1593
|
+
const hasWriteMutationVerb = /(新建|新增|增加|创建|建立|生成|写入|写一个|写个|写|修改|改成|改为|换成|转换|迁移|更新|补上|补充|加入|添加|加|覆盖|重写)/.test(normalized) ||
|
|
1594
|
+
/\b(create|write|edit|modify|update|add|append|insert|overwrite|replace|scaffold|generate|convert|port|migrate)\b/.test(normalized);
|
|
1595
|
+
const hasDirectoryTerm = /(目录|文件夹|folder|directory|\bdir\b)/.test(normalized);
|
|
1596
|
+
const hasWriteTerm = /(代码|逻辑|函数|方法|程序|项目|工程|文件(?!夹)|源码|实现|算法|排序|数据结构|包|模块|demo|示例|部分|片段|内容|行|块|log|日志|main\.|\.go|\.ts|\.tsx|\.js|\.jsx|\.py|\.md|c\+\+|cpp)/.test(normalized) ||
|
|
1597
|
+
/\b(code|logic|function|method|program|project|file|source|package|module|comment|comments|implementation|algorithm|algorithms|sort|sorts|sorting|data structure|data structures|demo|example|app|cpp|c\+\+)\b/.test(normalized);
|
|
1598
|
+
if (hasDeleteTerm && hasDirectoryTerm && !hasContentRemovalContext && availableTools.has('delete_directory')) {
|
|
1599
|
+
required.push('delete_directory');
|
|
1600
|
+
}
|
|
1601
|
+
else if (hasDeleteTerm && !hasContentRemovalContext && availableTools.has('delete_file')) {
|
|
1602
|
+
required.push('delete_file');
|
|
1603
|
+
}
|
|
1604
|
+
if (((hasWriteMutationVerb && hasWriteTerm) || (hasDeleteTerm && hasContentRemovalContext)) && (availableTools.has('edit_file') || availableTools.has('write_file'))) {
|
|
1605
|
+
required.push('file_mutation');
|
|
1606
|
+
}
|
|
1607
|
+
if (hasWriteMutationVerb && hasDirectoryTerm && availableTools.has('create_directory')) {
|
|
1608
|
+
required.push('create_directory');
|
|
1609
|
+
}
|
|
1610
|
+
return Array.from(new Set(required));
|
|
1611
|
+
}
|
|
1612
|
+
function createMutationGuardState(required, input) {
|
|
1613
|
+
return {
|
|
1614
|
+
required,
|
|
1615
|
+
succeeded: new Set(),
|
|
1616
|
+
noopSucceeded: new Set(),
|
|
1617
|
+
failed: new Map(),
|
|
1618
|
+
observedSucceeded: new Set(),
|
|
1619
|
+
allowNoopDeletionEvidence: deletionMutationRequestPattern().test(input),
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
function shouldBufferMutationText(state) {
|
|
1623
|
+
return state.required.length > 0 && !allRequiredMutationsSucceeded(state);
|
|
1624
|
+
}
|
|
1625
|
+
function shouldRetryMissingMutationTool(state, retries, modelTurns, maxTurns) {
|
|
1626
|
+
return state.required.length > 0 && !allRequiredMutationsSucceeded(state) && retries < defaultMutationGuardRetries && modelTurns < maxTurns;
|
|
1627
|
+
}
|
|
1628
|
+
function shouldFailMissingMutationTool(state) {
|
|
1629
|
+
return state.required.length > 0 && !allRequiredMutationsSucceeded(state);
|
|
1630
|
+
}
|
|
1631
|
+
function allRequiredMutationsSucceeded(state) {
|
|
1632
|
+
return state.required.every(requirement => state.succeeded.has(requirement) || state.noopSucceeded.has(requirement));
|
|
1633
|
+
}
|
|
1634
|
+
function recordMutationToolOutcome(state, toolName, result, call) {
|
|
1635
|
+
if (result.ok) {
|
|
1636
|
+
const observed = mutationRequirementForTool(toolName);
|
|
1637
|
+
if (observed) {
|
|
1638
|
+
state.observedSucceeded.add(observed);
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
for (const requirement of state.required) {
|
|
1642
|
+
if (!isMutationToolForRequirement(requirement, toolName)) {
|
|
1643
|
+
continue;
|
|
1644
|
+
}
|
|
1645
|
+
if (result.ok) {
|
|
1646
|
+
state.succeeded.add(requirement);
|
|
1647
|
+
state.failed.delete(requirement);
|
|
1648
|
+
}
|
|
1649
|
+
else {
|
|
1650
|
+
state.failed.set(requirement, `${result.error.code}: ${result.error.message}`);
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
recordShellRemovalMutationOutcome(state, toolName, result, call);
|
|
1654
|
+
recordNoopDeletionEvidence(state, toolName, result);
|
|
1655
|
+
}
|
|
1656
|
+
function mutationRequirementForTool(toolName) {
|
|
1657
|
+
if (toolName === 'edit_file' || toolName === 'write_file') {
|
|
1658
|
+
return 'file_mutation';
|
|
1659
|
+
}
|
|
1660
|
+
if (toolName === 'create_directory') {
|
|
1661
|
+
return 'create_directory';
|
|
1662
|
+
}
|
|
1663
|
+
if (toolName === 'delete_file') {
|
|
1664
|
+
return 'delete_file';
|
|
1665
|
+
}
|
|
1666
|
+
if (toolName === 'delete_directory') {
|
|
1667
|
+
return 'delete_directory';
|
|
1668
|
+
}
|
|
1669
|
+
return undefined;
|
|
1670
|
+
}
|
|
1671
|
+
function isMutationToolForRequirement(requiredMutationTool, toolName) {
|
|
1672
|
+
if (requiredMutationTool === 'file_mutation') {
|
|
1673
|
+
return toolName === 'edit_file' || toolName === 'write_file';
|
|
1674
|
+
}
|
|
1675
|
+
return toolName === requiredMutationTool;
|
|
1676
|
+
}
|
|
1677
|
+
function recordShellRemovalMutationOutcome(state, toolName, result, call) {
|
|
1678
|
+
if (toolName !== 'execute_shell' || !result.ok || !shellCommandResultSucceeded(result)) {
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
const command = typeof call?.input['command'] === 'string' ? call.input['command'] : '';
|
|
1682
|
+
const removalCommand = shellRemovalCommand(command);
|
|
1683
|
+
if (!removalCommand) {
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
if (state.required.includes('file_mutation')) {
|
|
1687
|
+
state.succeeded.add('file_mutation');
|
|
1688
|
+
state.observedSucceeded.add('file_mutation');
|
|
1689
|
+
state.failed.delete('file_mutation');
|
|
1690
|
+
}
|
|
1691
|
+
if (state.required.includes('delete_file') && removalCommand === 'rm') {
|
|
1692
|
+
state.succeeded.add('delete_file');
|
|
1693
|
+
state.observedSucceeded.add('delete_file');
|
|
1694
|
+
state.failed.delete('delete_file');
|
|
1695
|
+
}
|
|
1696
|
+
if (state.required.includes('delete_directory')) {
|
|
1697
|
+
state.succeeded.add('delete_directory');
|
|
1698
|
+
state.observedSucceeded.add('delete_directory');
|
|
1699
|
+
state.failed.delete('delete_directory');
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
function shellRemovalCommand(command) {
|
|
1703
|
+
const trimmed = command.trim();
|
|
1704
|
+
if (/^(?:command\s+)?rm(?:\s|$)/.test(trimmed) || /(?:^|[;&|]\s*)(?:command\s+)?rm(?:\s|$)/.test(trimmed)) {
|
|
1705
|
+
return 'rm';
|
|
1706
|
+
}
|
|
1707
|
+
if (/^(?:command\s+)?rmdir(?:\s|$)/.test(trimmed) || /(?:^|[;&|]\s*)(?:command\s+)?rmdir(?:\s|$)/.test(trimmed)) {
|
|
1708
|
+
return 'rmdir';
|
|
1709
|
+
}
|
|
1710
|
+
return undefined;
|
|
1711
|
+
}
|
|
1712
|
+
function recordNoopDeletionEvidence(state, toolName, result) {
|
|
1713
|
+
if (!state.allowNoopDeletionEvidence ||
|
|
1714
|
+
!state.required.includes('file_mutation') ||
|
|
1715
|
+
!result.ok ||
|
|
1716
|
+
(toolName !== 'glob' && toolName !== 'search_text')) {
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
const parsed = parseToolOutput(result.output);
|
|
1720
|
+
const matches = parsed?.['matches'];
|
|
1721
|
+
if (!Array.isArray(matches) || matches.length !== 0 || parsed?.['truncated'] === true) {
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
state.noopSucceeded.add('file_mutation');
|
|
1725
|
+
state.failed.delete('file_mutation');
|
|
1726
|
+
}
|
|
1727
|
+
function shellCommandResultSucceeded(result) {
|
|
1728
|
+
if (!result.ok) {
|
|
1729
|
+
return false;
|
|
1730
|
+
}
|
|
1731
|
+
try {
|
|
1732
|
+
const parsed = JSON.parse(result.output);
|
|
1733
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
1734
|
+
return false;
|
|
1735
|
+
}
|
|
1736
|
+
return parsed['exitCode'] === 0;
|
|
1737
|
+
}
|
|
1738
|
+
catch {
|
|
1739
|
+
return false;
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
function requiredMutationToolLabel(requiredMutationTool) {
|
|
1743
|
+
return requiredMutationTool === 'file_mutation' ? 'edit_file or write_file' : requiredMutationTool;
|
|
1744
|
+
}
|
|
1745
|
+
function buildMutationGuardPrompt(input, state, language) {
|
|
1746
|
+
const missing = state.required.filter(requirement => !state.succeeded.has(requirement));
|
|
1747
|
+
const toolInstructions = missing.map(requirement => {
|
|
1748
|
+
if (requirement === 'file_mutation') {
|
|
1749
|
+
return 'For file content changes, call read_file first, then edit_file for changes to an existing file. Use write_file for a new file or a full-file replacement only when that is the right operation.';
|
|
1750
|
+
}
|
|
1751
|
+
if (requirement === 'delete_file') {
|
|
1752
|
+
return 'For user-requested file deletion, prefer delete_file. Shell rm is allowed only through execute_shell approval and must complete with exit 0 before you claim deletion.';
|
|
1753
|
+
}
|
|
1754
|
+
if (requirement === 'delete_directory') {
|
|
1755
|
+
return 'For user-requested empty directory deletion, prefer delete_directory. Shell rmdir is allowed only through execute_shell approval and must complete with exit 0 before you claim deletion.';
|
|
1756
|
+
}
|
|
1757
|
+
return 'For requested directory creation, call create_directory.';
|
|
1758
|
+
});
|
|
1759
|
+
const failures = missing
|
|
1760
|
+
.map(requirement => {
|
|
1761
|
+
const failure = state.failed.get(requirement);
|
|
1762
|
+
return failure ? `- ${requiredMutationToolLabel(requirement)} failed: ${failure}` : undefined;
|
|
1763
|
+
})
|
|
1764
|
+
.filter((line) => Boolean(line));
|
|
1765
|
+
return [
|
|
1766
|
+
'Runtime guard: the user asked for local filesystem changes, but not all required mutation tools succeeded.',
|
|
1767
|
+
runtimeLanguageRequirement(language),
|
|
1768
|
+
`Original user request: ${input}`,
|
|
1769
|
+
`Required successful mutations still missing: ${missing.map(requiredMutationToolLabel).join(', ')}`,
|
|
1770
|
+
...toolInstructions,
|
|
1771
|
+
...(failures.length > 0 ? ['Observed failed mutation attempts:', ...failures] : []),
|
|
1772
|
+
'Do not claim completion until successful tool results for every required mutation are present in this turn.',
|
|
1773
|
+
'If the tool is denied or the target cannot be safely changed, use the tool result to explain the actual blocker instead of saying the work is done.',
|
|
1774
|
+
].join('\n');
|
|
1775
|
+
}
|
|
1776
|
+
function mutationGuardFailureMessage(state) {
|
|
1777
|
+
const missing = state.required.filter(requirement => !state.succeeded.has(requirement));
|
|
1778
|
+
const labels = missing.map(userFacingMutationRequirementLabel).join(', ');
|
|
1779
|
+
const failures = missing
|
|
1780
|
+
.map(requirement => state.failed.get(requirement))
|
|
1781
|
+
.filter((failure) => Boolean(failure));
|
|
1782
|
+
const detail = failures.length > 0
|
|
1783
|
+
? ` Last tool failure: ${compactText(failures.at(-1) ?? '', 300)}`
|
|
1784
|
+
: ' No successful filesystem change was recorded.';
|
|
1785
|
+
return `I couldn't complete the requested filesystem change. Missing successful operation: ${labels}.${detail}`;
|
|
1786
|
+
}
|
|
1787
|
+
function shouldGuardMutationClaimsForInput(input, requiredMutationTools) {
|
|
1788
|
+
if (requiredMutationTools.length > 0) {
|
|
1789
|
+
return true;
|
|
1790
|
+
}
|
|
1791
|
+
if (isAdvisoryOnlyRequest(normalizeIntentText(input))) {
|
|
1792
|
+
return false;
|
|
1793
|
+
}
|
|
1794
|
+
return mutationRequestPattern().test(input);
|
|
1795
|
+
}
|
|
1796
|
+
function mutationCompletionClaimIssue(text, state) {
|
|
1797
|
+
const claim = mutationCompletionClaimKind(text);
|
|
1798
|
+
if (!claim) {
|
|
1799
|
+
return undefined;
|
|
1800
|
+
}
|
|
1801
|
+
if (claim === 'delete_file') {
|
|
1802
|
+
return state.observedSucceeded.has('delete_file') || state.observedSucceeded.has('delete_directory') ? undefined : claim;
|
|
1803
|
+
}
|
|
1804
|
+
return state.observedSucceeded.has(claim) ? undefined : claim;
|
|
1805
|
+
}
|
|
1806
|
+
function mutationCompletionClaimKind(text) {
|
|
1807
|
+
const normalized = text.replace(/\s+/g, ' ').trim().toLowerCase();
|
|
1808
|
+
if (normalized.length === 0 || mutationClaimNegationPattern().test(normalized)) {
|
|
1809
|
+
return undefined;
|
|
1810
|
+
}
|
|
1811
|
+
if (contentRemovalCompletionClaimPattern().test(normalized)) {
|
|
1812
|
+
return 'file_mutation';
|
|
1813
|
+
}
|
|
1814
|
+
if (fileMutationCompletionClaimPattern().test(normalized)) {
|
|
1815
|
+
return 'file_mutation';
|
|
1816
|
+
}
|
|
1817
|
+
if (directoryCreationCompletionClaimPattern().test(normalized)) {
|
|
1818
|
+
return 'create_directory';
|
|
1819
|
+
}
|
|
1820
|
+
if (deleteCompletionClaimPattern().test(normalized)) {
|
|
1821
|
+
return 'delete_file';
|
|
1822
|
+
}
|
|
1823
|
+
return undefined;
|
|
1824
|
+
}
|
|
1825
|
+
function buildMutationClaimGuardPrompt(input, draft, state, claim, language) {
|
|
1826
|
+
const observed = state.observedSucceeded.size > 0 ? Array.from(state.observedSucceeded).map(requiredMutationToolLabel).join(', ') : 'none';
|
|
1827
|
+
return [
|
|
1828
|
+
'Runtime guard: your last draft claimed that a filesystem change was completed, but current-turn tool evidence does not support that claim.',
|
|
1829
|
+
runtimeLanguageRequirement(language),
|
|
1830
|
+
`Original user request: ${input}`,
|
|
1831
|
+
`Blocked draft: ${compactText(draft, 800)}`,
|
|
1832
|
+
`Claim requires current-turn evidence for: ${requiredMutationToolLabel(claim)}`,
|
|
1833
|
+
`Current successful mutation evidence: ${observed}`,
|
|
1834
|
+
'Use current filesystem tools as the source of truth. Prior assistant messages and prior tool results are not proof of the current filesystem state.',
|
|
1835
|
+
'If the user asked for a file change, read the current file and then call edit_file or write_file before claiming completion.',
|
|
1836
|
+
'If no change can be made, revise the answer to explain the actual blocker instead of saying the work is done.',
|
|
1837
|
+
].join('\n');
|
|
1838
|
+
}
|
|
1839
|
+
function mutationClaimGuardFailureMessage(claim) {
|
|
1840
|
+
return `I couldn't complete the requested filesystem change. The draft claimed completion, but no current-turn ${userFacingMutationRequirementLabel(claim)} succeeded.`;
|
|
1841
|
+
}
|
|
1842
|
+
function userFacingMutationRequirementLabel(requiredMutationTool) {
|
|
1843
|
+
if (requiredMutationTool === 'file_mutation') {
|
|
1844
|
+
return 'file edit or write';
|
|
1845
|
+
}
|
|
1846
|
+
if (requiredMutationTool === 'create_directory') {
|
|
1847
|
+
return 'directory creation';
|
|
1848
|
+
}
|
|
1849
|
+
if (requiredMutationTool === 'delete_file') {
|
|
1850
|
+
return 'file deletion';
|
|
1851
|
+
}
|
|
1852
|
+
return 'empty directory deletion';
|
|
1853
|
+
}
|
|
1854
|
+
function mutationRequestPattern() {
|
|
1855
|
+
return /(新建|新增|增加|创建|建立|生成|写入|写一个|写个|写|修改|改成|改为|换成|转换|迁移|更新|补上|补充|加入|添加|加|覆盖|重写|删除|移除|删掉|去掉)|\b(create|write|edit|modify|update|add|append|insert|overwrite|replace|scaffold|generate|convert|port|migrate|delete|remove)\b/i;
|
|
1856
|
+
}
|
|
1857
|
+
function deletionMutationRequestPattern() {
|
|
1858
|
+
return /(删除|移除|删掉|去掉)|\b(?:delete|remove)\b/i;
|
|
1859
|
+
}
|
|
1860
|
+
function mutationClaimNegationPattern() {
|
|
1861
|
+
return /(没有|未|无法|不能|尚未|无需|不需要|不必|不建议).{0,16}(新增|添加|写入|更新|修改|创建|删除|验证|运行)|\b(?:did not|didn't|not|cannot|can't|unable to|no need to|do not need to|don't need to|should not)\b.{0,32}\b(?:add|write|update|modify|create|delete|verify|run|remove)\b/i;
|
|
1862
|
+
}
|
|
1863
|
+
function contentRemovalCompletionClaimPattern() {
|
|
1864
|
+
return /(已|已经|成功)?(删除|移除|删掉|去掉).{0,28}(部分|片段|段落|段|代码|逻辑|函数|方法|内容|行|块|区块|章节|小节|注释|批注|备注|log|日志|调用|section|part|snippet|block|code|logic|function|method|content|line|lines|comment|comments|logging|call)|(部分|片段|代码|内容|注释|log|日志|调用).{0,28}(删除|移除|删掉|去掉)(了|完成|成功)?/i;
|
|
1865
|
+
}
|
|
1866
|
+
function fileMutationCompletionClaimPattern() {
|
|
1867
|
+
return /(搞定|完成).{0,80}(新增|添加|写入|更新|修改|改好|实现|补上|加入|重写|替换|迁移|排序|算法)|(已|已经).{0,16}(新增|添加|写入|更新|修改|改成|实现|补上|加入|重写|替换|迁移)|(新增|添加|写入|更新|修改|实现|补上|加入|重写|替换|迁移)(了|完成|好了|成功)|\b(?:added|updated|modified|wrote|implemented|replaced|migrated)\b/i;
|
|
1868
|
+
}
|
|
1869
|
+
function directoryCreationCompletionClaimPattern() {
|
|
1870
|
+
return /(已|已经|成功)?(创建|新建|建立).{0,20}(目录|文件夹)|(目录|文件夹).{0,20}(创建|新建|建立)(了|完成|成功)?|\bcreated\b.{0,24}\b(?:directory|folder|dir)\b/i;
|
|
1871
|
+
}
|
|
1872
|
+
function deleteCompletionClaimPattern() {
|
|
1873
|
+
return /(已|已经|成功)(删除|移除|删掉|去掉)(了|完成|成功)?|(删除|移除|删掉|去掉)(了|完成|成功)|\b(?:deleted|removed)\b/i;
|
|
1874
|
+
}
|
|
1875
|
+
function createVerificationGuardState() {
|
|
1876
|
+
return {};
|
|
1877
|
+
}
|
|
1878
|
+
function shouldGuardVerificationClaimsForInput(input, requiredMutationTools) {
|
|
1879
|
+
if (requiredMutationTools.length > 0) {
|
|
1880
|
+
return true;
|
|
1881
|
+
}
|
|
1882
|
+
return verificationRequestPattern().test(input);
|
|
1883
|
+
}
|
|
1884
|
+
function recordVerificationToolOutcome(state, toolName, result) {
|
|
1885
|
+
if (toolName !== 'execute_shell' && toolName !== 'run_command') {
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
if (!result.ok) {
|
|
1889
|
+
state.lastShell = {
|
|
1890
|
+
status: 'failed',
|
|
1891
|
+
command: toolName,
|
|
1892
|
+
detail: `${result.error.code}: ${result.error.message}`,
|
|
1893
|
+
};
|
|
1894
|
+
return;
|
|
1895
|
+
}
|
|
1896
|
+
const parsed = parseToolOutput(result.output);
|
|
1897
|
+
const command = typeof parsed?.['command'] === 'string' ? parsed['command'] : toolName;
|
|
1898
|
+
const exitCode = typeof parsed?.['exitCode'] === 'number' ? parsed['exitCode'] : undefined;
|
|
1899
|
+
const timedOut = parsed?.['timedOut'] === true;
|
|
1900
|
+
const status = exitCode === 0 && !timedOut ? 'success' : 'failed';
|
|
1901
|
+
const detail = exitCode === undefined
|
|
1902
|
+
? 'exit code unavailable'
|
|
1903
|
+
: timedOut
|
|
1904
|
+
? `timed out after exit ${exitCode}`
|
|
1905
|
+
: `exit ${exitCode}`;
|
|
1906
|
+
const evidence = { status, command, detail };
|
|
1907
|
+
state.lastShell = evidence;
|
|
1908
|
+
if (isVerificationCommand(command)) {
|
|
1909
|
+
state.lastVerification = evidence;
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
function hasCurrentTurnActionEvidence(mutationState, verificationState) {
|
|
1913
|
+
return (mutationState.observedSucceeded.size > 0 ||
|
|
1914
|
+
verificationState.lastShell?.status === 'success' ||
|
|
1915
|
+
verificationState.lastVerification?.status === 'success');
|
|
1916
|
+
}
|
|
1917
|
+
function formatCurrentTurnActionEvidence(mutationState, verificationState) {
|
|
1918
|
+
const evidence = [];
|
|
1919
|
+
if (mutationState.observedSucceeded.size > 0) {
|
|
1920
|
+
evidence.push(`successful mutation tools: ${Array.from(mutationState.observedSucceeded).map(requiredMutationToolLabel).join(', ')}`);
|
|
1921
|
+
}
|
|
1922
|
+
if (verificationState.lastVerification?.status === 'success') {
|
|
1923
|
+
evidence.push(`successful verification: ${verificationState.lastVerification.command} (${verificationState.lastVerification.detail})`);
|
|
1924
|
+
}
|
|
1925
|
+
else if (verificationState.lastShell?.status === 'success') {
|
|
1926
|
+
evidence.push(`successful command: ${verificationState.lastShell.command} (${verificationState.lastShell.detail})`);
|
|
1927
|
+
}
|
|
1928
|
+
return evidence.length > 0 ? evidence.join('; ') : 'none';
|
|
1929
|
+
}
|
|
1930
|
+
function verificationClaimGuardIssue(text, state) {
|
|
1931
|
+
const claimKind = verificationSuccessClaimKind(text);
|
|
1932
|
+
if (!claimKind) {
|
|
1933
|
+
return undefined;
|
|
1934
|
+
}
|
|
1935
|
+
const evidence = claimKind === 'verification' ? state.lastVerification : state.lastShell ?? state.lastVerification;
|
|
1936
|
+
return evidence?.status === 'success' ? undefined : claimKind;
|
|
1937
|
+
}
|
|
1938
|
+
function verificationSuccessClaimKind(text) {
|
|
1939
|
+
const normalized = text.replace(/\s+/g, ' ').trim();
|
|
1940
|
+
if (normalized.length === 0 || verificationNegationPattern().test(normalized)) {
|
|
1941
|
+
return undefined;
|
|
1942
|
+
}
|
|
1943
|
+
if (verificationSuccessPattern().test(normalized)) {
|
|
1944
|
+
return 'verification';
|
|
1945
|
+
}
|
|
1946
|
+
return executionSuccessPattern().test(normalized) ? 'execution' : undefined;
|
|
1947
|
+
}
|
|
1948
|
+
function buildVerificationClaimGuardPrompt(input, draft, state, language) {
|
|
1949
|
+
const evidence = formatVerificationEvidence(state);
|
|
1950
|
+
return [
|
|
1951
|
+
'Runtime guard: your last draft claimed that verification, compilation, tests, or execution succeeded, but current-turn tool evidence does not support that claim.',
|
|
1952
|
+
runtimeLanguageRequirement(language),
|
|
1953
|
+
`Original user request: ${input}`,
|
|
1954
|
+
`Blocked draft: ${compactText(draft, 800)}`,
|
|
1955
|
+
`Current evidence: ${evidence}`,
|
|
1956
|
+
'Revise the final answer so it is strictly grounded in successful current-turn tool results.',
|
|
1957
|
+
'Do not say build/test/run/verification passed unless a successful current-turn execute_shell or run_command result proves it.',
|
|
1958
|
+
'If files were changed but validation did not run, was denied, or failed, say that directly and include the exact manual command when it is known.',
|
|
1959
|
+
].join('\n');
|
|
1960
|
+
}
|
|
1961
|
+
function verificationClaimGuardFailureMessage(state) {
|
|
1962
|
+
return `Assistant attempted to claim verification success without successful current-turn evidence. ${formatVerificationEvidence(state)}`;
|
|
1963
|
+
}
|
|
1964
|
+
function formatVerificationEvidence(state) {
|
|
1965
|
+
const evidence = state.lastVerification ?? state.lastShell;
|
|
1966
|
+
if (!evidence) {
|
|
1967
|
+
return 'no validation command has succeeded in this turn.';
|
|
1968
|
+
}
|
|
1969
|
+
return `last command ${evidence.status}: ${evidence.command} (${evidence.detail}).`;
|
|
1970
|
+
}
|
|
1971
|
+
function verificationRequestPattern() {
|
|
1972
|
+
return /(测试|验证|编译|构建|运行|跑一下|执行)|\b(test|verify|validate|compile|build|run|execute)\b/i;
|
|
1973
|
+
}
|
|
1974
|
+
function verificationSuccessPattern() {
|
|
1975
|
+
return /(测试|验证|编译|构建).{0,24}(通过|成功|正常)|(?:通过|成功).{0,24}(测试|验证|编译|构建)|\b(?:tests?|test suite|build|compile|compilation|validation|verification)\b.{0,48}\b(?:pass(?:ed|es)?|succeed(?:ed)?|successful(?:ly)?|green|ok)\b|\b(?:all|everything)\b.{0,32}\b(?:pass(?:ed)?|succeed(?:ed)?|successful)\b/i;
|
|
1976
|
+
}
|
|
1977
|
+
function executionSuccessPattern() {
|
|
1978
|
+
return /(运行|执行).{0,24}(通过|成功|正常)|(?:正常运行|运行正常|全部正常运行)|\b(?:command|run|ran|execution)\b.{0,48}\b(?:pass(?:ed)?|succeed(?:ed)?|successful(?:ly)?|ok)\b/i;
|
|
1979
|
+
}
|
|
1980
|
+
function verificationNegationPattern() {
|
|
1981
|
+
return /(未|没|没有|无法|不能|未能|尚未).{0,24}(测试|验证|编译|构建|运行|执行|通过|成功)|手动运行|需要.{0,12}运行|自行.{0,12}运行|\b(?:not|never)\s+(?:run|ran|verified|tested|built|compiled)|\b(?:could not|unable to|manual|not verified|not run|did not run)\b/i;
|
|
1982
|
+
}
|
|
1983
|
+
function isVerificationCommand(command) {
|
|
1984
|
+
const normalized = command.trim().toLowerCase();
|
|
1985
|
+
return /(^|[;&]\s*)(go\s+(test|run|vet|build)|cargo\s+(test|build|run)|pnpm\s+(test|build|run\s+\S+)|npm\s+(test|run\s+\S+)|yarn\s+(test|build|run\s+\S+)|bun\s+(test|run|build)|python3?\s+-m\s+(pytest|unittest|mypy|ruff)|pytest|vitest|tsc|make|cmake|gcc|g\+\+|clang|clang\+\+|javac|mvn\s+test|gradle\s+test|swift\s+test|xcodebuild|node\s+\S+|\.\/\S*test\S*)(\s|$)/.test(normalized);
|
|
1986
|
+
}
|
|
1987
|
+
function toolDefinitionsForProvider(registry, extraTools = []) {
|
|
1988
|
+
return [
|
|
1989
|
+
...extraTools,
|
|
1990
|
+
...registry.list().map(tool => ({
|
|
1991
|
+
name: tool.name,
|
|
1992
|
+
description: tool.description,
|
|
1993
|
+
parameters: tool.inputSchema,
|
|
1994
|
+
})),
|
|
1995
|
+
];
|
|
1996
|
+
}
|
|
1997
|
+
function skillToolDefinitionsForProvider(index) {
|
|
1998
|
+
return index.skills.some(skill => skill.enabled)
|
|
1999
|
+
? [
|
|
2000
|
+
{
|
|
2001
|
+
name: loadSkillTool.name,
|
|
2002
|
+
description: loadSkillTool.description,
|
|
2003
|
+
parameters: loadSkillTool.inputSchema,
|
|
2004
|
+
},
|
|
2005
|
+
]
|
|
2006
|
+
: [];
|
|
2007
|
+
}
|
|
2008
|
+
function loadSkillToolCallOutcome(toolCall, options) {
|
|
2009
|
+
const parsed = parseToolArguments(toolCall.arguments, loadSkillTool);
|
|
2010
|
+
if (!parsed.ok) {
|
|
2011
|
+
return {
|
|
2012
|
+
result: {
|
|
2013
|
+
ok: false,
|
|
2014
|
+
callId: toolCall.id,
|
|
2015
|
+
toolName: loadSkillToolName,
|
|
2016
|
+
error: {
|
|
2017
|
+
code: 'TOOL_ARGUMENTS_INVALID',
|
|
2018
|
+
message: parsed.result.message,
|
|
2019
|
+
},
|
|
2020
|
+
},
|
|
2021
|
+
};
|
|
2022
|
+
}
|
|
2023
|
+
const skillName = parsed.input['name'];
|
|
2024
|
+
if (typeof skillName !== 'string' || skillName.trim().length === 0) {
|
|
2025
|
+
return {
|
|
2026
|
+
result: {
|
|
2027
|
+
ok: false,
|
|
2028
|
+
callId: toolCall.id,
|
|
2029
|
+
toolName: loadSkillToolName,
|
|
2030
|
+
error: {
|
|
2031
|
+
code: 'TOOL_ARGUMENTS_INVALID',
|
|
2032
|
+
message: 'Tool arguments are missing required field(s): name.',
|
|
2033
|
+
},
|
|
2034
|
+
},
|
|
2035
|
+
};
|
|
2036
|
+
}
|
|
2037
|
+
const skill = loadSkillContent({
|
|
2038
|
+
...options,
|
|
2039
|
+
name: skillName,
|
|
2040
|
+
maxContentChars: maxLoadedSkillContentChars,
|
|
2041
|
+
});
|
|
2042
|
+
if (!skill) {
|
|
2043
|
+
return {
|
|
2044
|
+
result: {
|
|
2045
|
+
ok: false,
|
|
2046
|
+
callId: toolCall.id,
|
|
2047
|
+
toolName: loadSkillToolName,
|
|
2048
|
+
error: {
|
|
2049
|
+
code: 'SKILL_NOT_FOUND',
|
|
2050
|
+
message: `Skill ${skillName} is not available in the current Remi skill index.`,
|
|
2051
|
+
},
|
|
2052
|
+
},
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
2055
|
+
const output = JSON.stringify({
|
|
2056
|
+
ok: true,
|
|
2057
|
+
skill: {
|
|
2058
|
+
name: skill.name,
|
|
2059
|
+
displayName: skill.displayName,
|
|
2060
|
+
source: skill.source,
|
|
2061
|
+
filePath: skill.filePath,
|
|
2062
|
+
instructions: skill.content,
|
|
2063
|
+
},
|
|
2064
|
+
guidance: 'Apply these skill instructions for the rest of this turn before producing the user-facing answer.',
|
|
2065
|
+
});
|
|
2066
|
+
return {
|
|
2067
|
+
skill,
|
|
2068
|
+
result: {
|
|
2069
|
+
ok: true,
|
|
2070
|
+
callId: toolCall.id,
|
|
2071
|
+
toolName: loadSkillToolName,
|
|
2072
|
+
output,
|
|
2073
|
+
truncated: false,
|
|
2074
|
+
bytes: Buffer.byteLength(output, 'utf8'),
|
|
2075
|
+
},
|
|
2076
|
+
};
|
|
2077
|
+
}
|
|
2078
|
+
function normalizeSkillNameForRuntime(name) {
|
|
2079
|
+
return name.trim().toLowerCase();
|
|
2080
|
+
}
|
|
2081
|
+
function buildToolUseSystemPrompt(cwd, env, planMode = undefined, language) {
|
|
2082
|
+
const base = readToolUseSystemPromptTemplate()
|
|
2083
|
+
.replaceAll('{{cwd}}', cwd)
|
|
2084
|
+
.replaceAll('{{home_line}}', env.HOME ? `User home directory: ${env.HOME}` : '')
|
|
2085
|
+
.replaceAll('{{host_platform}}', hostPlatformForPrompt());
|
|
2086
|
+
const languageBlock = `Response language:\n- ${runtimeLanguageRequirement(language)}`;
|
|
2087
|
+
const baseWithLanguage = `${base}\n\n${languageBlock}`;
|
|
2088
|
+
if (!planMode) {
|
|
2089
|
+
return baseWithLanguage;
|
|
2090
|
+
}
|
|
2091
|
+
const heading = planMode === 'required' ? 'Plan-first is required for this turn:' : 'Plan mode is active:';
|
|
2092
|
+
const approvalLine = planMode === 'required'
|
|
2093
|
+
? '- After todo_write, continue implementing the original request when normal tools become available; ask the user only if the plan reveals ambiguity, destructive risk, or a permission blocker.'
|
|
2094
|
+
: '- Ask the user to approve or refine the plan before execution.';
|
|
2095
|
+
return `${baseWithLanguage}
|
|
2096
|
+
|
|
2097
|
+
${heading}
|
|
2098
|
+
- Do not edit files, create/delete files, or run build/test/execute commands in this turn.
|
|
2099
|
+
- Inspect with read-only tools when needed, then use todo_write to produce or update the plan.
|
|
2100
|
+
${approvalLine}`;
|
|
2101
|
+
}
|
|
2102
|
+
let cachedToolUseSystemPromptTemplate;
|
|
2103
|
+
function readToolUseSystemPromptTemplate() {
|
|
2104
|
+
if (cachedToolUseSystemPromptTemplate !== undefined) {
|
|
2105
|
+
return cachedToolUseSystemPromptTemplate;
|
|
2106
|
+
}
|
|
2107
|
+
try {
|
|
2108
|
+
cachedToolUseSystemPromptTemplate = readFileSync(new URL('../../../prompt/tool-use-system.md', import.meta.url), 'utf8').trim();
|
|
2109
|
+
}
|
|
2110
|
+
catch {
|
|
2111
|
+
cachedToolUseSystemPromptTemplate = [
|
|
2112
|
+
'You can use Remi local tools for read-only project inspection.',
|
|
2113
|
+
'Current working directory: {{cwd}}',
|
|
2114
|
+
'{{home_line}}',
|
|
2115
|
+
'Host platform: {{host_platform}}',
|
|
2116
|
+
'Prefer structured filesystem tools over shell commands.',
|
|
2117
|
+
'For broad directory overviews, inspect one useful next layer and summarize meaning.',
|
|
2118
|
+
'If you do not know an exact file path, discover it with list_files or glob before calling read_file.',
|
|
2119
|
+
'Use create_directory, edit_file, and write_file when the user asks you to change local files.',
|
|
2120
|
+
'Use delete_file when the user explicitly asks to remove a file; prefer it over shell rm. For multiple files, call delete_file once per file.',
|
|
2121
|
+
'Use delete_directory when the user explicitly asks to remove an empty directory; prefer it over shell rmdir. For multiple empty directories, call delete_directory once per directory.',
|
|
2122
|
+
'Read existing files before editing or overwriting them.',
|
|
2123
|
+
'Prior assistant messages are conversation history, not proof of the current filesystem state.',
|
|
2124
|
+
'Use `run_command` only for simple read-only local inspection commands when structured tools are insufficient.',
|
|
2125
|
+
'Treat common host tasks as command capabilities: network.ip, network.mac, network.ports, download.url, logs.search, and tool.install.',
|
|
2126
|
+
'Prefer host-platform query commands. For IP use ifconfig/ip on macOS/Linux and ipconfig on Windows. For MAC use ifconfig/ip link/getmac. For ports use lsof/netstat/ss on macOS/Linux and netstat on Windows.',
|
|
2127
|
+
'If a read-only command returns exit code 127 or says Command not found, try a platform-equivalent allowlisted command before concluding failure.',
|
|
2128
|
+
'Allowed `run_command` executables include safe read-only local inspection commands such as `grep`, `find`, `ps`, `wc`, `file`, `stat`, `du`, `df`, `sort`, `diff`, `strings`, `ifconfig`, `ip`, `ipconfig`, `getmac`, `netstat`, `ss`, `lsof`, and read-only `git` subcommands.',
|
|
2129
|
+
'Do not call `run_command` for `cd`, `go`, `go run`, `go test`, `go mod`, package managers, interpreters, build/test commands, write commands, curl/wget downloads, or chained shell commands.',
|
|
2130
|
+
'Use `execute_shell` for build/test/verification commands, package manager installs, network downloads, and side-effect commands such as `sed`, `cp`, `mv`, `rm`, `rmdir`, `sh`, `curl`, `wget`, `brew`, `apt`, `apt-get`, `winget`, `choco`, `scoop`, or project scripts after file work is ready. Pass its `cwd` argument when the command must run from a package directory; single-line `cd <dir> && command` is also supported when needed. The runtime will request approval when needed. Do not add sudo; admin-risk installs need a separate explicit approval path.',
|
|
2131
|
+
'The current working directory is Remi runtime cwd, not always the target project. If Recent Filesystem Locations are present, resolve repeated project names to those absolute paths instead of creating same-named directories under cwd.',
|
|
2132
|
+
'For nested projects, inspect the directory tree before reading conventional filenames; those files may live under subdirectories.',
|
|
2133
|
+
'For simple project scaffolding, create the directory and minimal source/config files directly when the file contents are straightforward.',
|
|
2134
|
+
'If a command is denied but files can still be written, write the files and tell the user which command to run manually.',
|
|
2135
|
+
'If a denied command was an rm or rmdir deletion attempt, do not retry with a broader shell form. Use delete_file or delete_directory when that can express the requested target, or explain the safety blocker.',
|
|
2136
|
+
'Never read secrets or local credentials.',
|
|
2137
|
+
'After using tools, produce a user-facing answer.',
|
|
2138
|
+
].join('\n');
|
|
2139
|
+
}
|
|
2140
|
+
return cachedToolUseSystemPromptTemplate;
|
|
2141
|
+
}
|
|
2142
|
+
function hostPlatformForPrompt() {
|
|
2143
|
+
if (process.platform === 'darwin') {
|
|
2144
|
+
return 'macOS (darwin)';
|
|
2145
|
+
}
|
|
2146
|
+
if (process.platform === 'linux') {
|
|
2147
|
+
return 'Linux (linux)';
|
|
2148
|
+
}
|
|
2149
|
+
if (process.platform === 'win32') {
|
|
2150
|
+
return 'Windows (win32)';
|
|
2151
|
+
}
|
|
2152
|
+
return `${process.platform}`;
|
|
2153
|
+
}
|
|
2154
|
+
function toolCallFromLLM(toolCall, registry) {
|
|
2155
|
+
const tool = registry.get(toolCall.name);
|
|
2156
|
+
const parsed = parseToolArguments(toolCall.arguments, tool);
|
|
2157
|
+
if (!parsed.ok) {
|
|
2158
|
+
return parsed;
|
|
2159
|
+
}
|
|
2160
|
+
return {
|
|
2161
|
+
ok: true,
|
|
2162
|
+
call: {
|
|
2163
|
+
id: toolCall.id,
|
|
2164
|
+
toolName: toolCall.name,
|
|
2165
|
+
input: parsed.input,
|
|
2166
|
+
},
|
|
2167
|
+
};
|
|
2168
|
+
}
|
|
2169
|
+
function parseToolArguments(rawArguments, tool) {
|
|
2170
|
+
if (rawArguments.trim().length === 0) {
|
|
2171
|
+
const input = {};
|
|
2172
|
+
const validation = validateToolArguments(input, tool?.inputSchema);
|
|
2173
|
+
return validation.ok
|
|
2174
|
+
? { ok: true, input }
|
|
2175
|
+
: {
|
|
2176
|
+
ok: false,
|
|
2177
|
+
result: {
|
|
2178
|
+
code: 'TOOL_ARGUMENTS_INVALID',
|
|
2179
|
+
message: validation.message,
|
|
2180
|
+
rawArguments,
|
|
2181
|
+
repairedArguments: input,
|
|
2182
|
+
},
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
2185
|
+
const parsed = parseToolArgumentsJson(rawArguments, tool?.inputSchema);
|
|
2186
|
+
if (!parsed.ok) {
|
|
2187
|
+
return parsed;
|
|
2188
|
+
}
|
|
2189
|
+
const validation = validateToolArguments(parsed.input, tool?.inputSchema);
|
|
2190
|
+
if (!validation.ok) {
|
|
2191
|
+
return {
|
|
2192
|
+
ok: false,
|
|
2193
|
+
result: {
|
|
2194
|
+
code: 'TOOL_ARGUMENTS_INVALID',
|
|
2195
|
+
message: validation.message,
|
|
2196
|
+
rawArguments,
|
|
2197
|
+
...(parsed.repaired ? { repairedArguments: parsed.input } : {}),
|
|
2198
|
+
},
|
|
2199
|
+
};
|
|
2200
|
+
}
|
|
2201
|
+
return { ok: true, input: parsed.input };
|
|
2202
|
+
}
|
|
2203
|
+
function parseToolArgumentsJson(rawArguments, schema) {
|
|
2204
|
+
const raw = rawArguments.trim();
|
|
2205
|
+
try {
|
|
2206
|
+
return { ok: true, input: normalizeParsedToolArguments(JSON.parse(raw)), repaired: false };
|
|
2207
|
+
}
|
|
2208
|
+
catch (error) {
|
|
2209
|
+
const repairedRaw = repairToolArgumentsJson(raw, schema);
|
|
2210
|
+
if (repairedRaw && repairedRaw !== raw) {
|
|
2211
|
+
try {
|
|
2212
|
+
return { ok: true, input: normalizeParsedToolArguments(JSON.parse(repairedRaw)), repaired: true };
|
|
2213
|
+
}
|
|
2214
|
+
catch {
|
|
2215
|
+
// Fall through to the structured recovery pass below.
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
const recovered = recoverToolArgumentsObject(raw, schema);
|
|
2219
|
+
if (recovered && Object.keys(recovered).length > 0) {
|
|
2220
|
+
return { ok: true, input: recovered, repaired: true };
|
|
2221
|
+
}
|
|
2222
|
+
return {
|
|
2223
|
+
ok: false,
|
|
2224
|
+
result: {
|
|
2225
|
+
code: 'TOOL_ARGUMENTS_INVALID',
|
|
2226
|
+
message: `Tool arguments must be a valid JSON object. ${safeJsonParseError(error)}`,
|
|
2227
|
+
rawArguments,
|
|
2228
|
+
},
|
|
2229
|
+
};
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
function normalizeParsedToolArguments(parsed) {
|
|
2233
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
2234
|
+
throw new Error('Tool arguments must be a JSON object');
|
|
2235
|
+
}
|
|
2236
|
+
return parsed;
|
|
2237
|
+
}
|
|
2238
|
+
function repairToolArgumentsJson(raw, schema) {
|
|
2239
|
+
if (!schema) {
|
|
2240
|
+
return undefined;
|
|
2241
|
+
}
|
|
2242
|
+
return raw.replace(/("[^"]+"\s*:\s*)([^",{}\[\]\r\n][^,}\]\r\n]*)/g, (match, prefix, value) => {
|
|
2243
|
+
const keyMatch = /"([^"]+)"\s*:\s*$/.exec(prefix);
|
|
2244
|
+
const key = keyMatch?.[1];
|
|
2245
|
+
const property = key ? schema.properties[key] : undefined;
|
|
2246
|
+
const trimmed = value.trim();
|
|
2247
|
+
if (!property || property.type !== 'string' || isJsonLiteralLike(trimmed)) {
|
|
2248
|
+
return match;
|
|
2249
|
+
}
|
|
2250
|
+
return `${prefix}${JSON.stringify(trimmed)}`;
|
|
2251
|
+
});
|
|
2252
|
+
}
|
|
2253
|
+
function recoverToolArgumentsObject(raw, schema) {
|
|
2254
|
+
if (!schema) {
|
|
2255
|
+
return undefined;
|
|
2256
|
+
}
|
|
2257
|
+
const recovered = {};
|
|
2258
|
+
for (const [key, property] of Object.entries(schema.properties)) {
|
|
2259
|
+
const rawValue = recoverRawObjectPropertyValue(raw, key);
|
|
2260
|
+
if (rawValue === undefined) {
|
|
2261
|
+
continue;
|
|
2262
|
+
}
|
|
2263
|
+
const value = recoverToolArgumentValue(rawValue, property);
|
|
2264
|
+
if (value !== undefined) {
|
|
2265
|
+
recovered[key] = value;
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
return Object.keys(recovered).length > 0 ? recovered : undefined;
|
|
2269
|
+
}
|
|
2270
|
+
function recoverRawObjectPropertyValue(raw, key) {
|
|
2271
|
+
const keyMatch = new RegExp(`(?:"${escapeRegExp(key)}"|${escapeRegExp(key)})\\s*:\\s*`).exec(raw);
|
|
2272
|
+
if (!keyMatch) {
|
|
2273
|
+
return undefined;
|
|
2274
|
+
}
|
|
2275
|
+
const start = keyMatch.index + keyMatch[0].length;
|
|
2276
|
+
let depth = 0;
|
|
2277
|
+
let quote;
|
|
2278
|
+
for (let index = start; index < raw.length; index += 1) {
|
|
2279
|
+
const char = raw[index];
|
|
2280
|
+
const previous = index > start ? raw[index - 1] : undefined;
|
|
2281
|
+
if (quote) {
|
|
2282
|
+
if (char === quote && previous !== '\\') {
|
|
2283
|
+
quote = undefined;
|
|
2284
|
+
}
|
|
2285
|
+
continue;
|
|
2286
|
+
}
|
|
2287
|
+
if (char === '"' || char === "'") {
|
|
2288
|
+
quote = char;
|
|
2289
|
+
continue;
|
|
2290
|
+
}
|
|
2291
|
+
if (char === '{' || char === '[') {
|
|
2292
|
+
depth += 1;
|
|
2293
|
+
continue;
|
|
2294
|
+
}
|
|
2295
|
+
if (char === '}' || char === ']') {
|
|
2296
|
+
if (depth === 0) {
|
|
2297
|
+
return raw.slice(start, index).trim();
|
|
2298
|
+
}
|
|
2299
|
+
depth -= 1;
|
|
2300
|
+
continue;
|
|
2301
|
+
}
|
|
2302
|
+
if (char === ',' && depth === 0) {
|
|
2303
|
+
return raw.slice(start, index).trim();
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
return raw.slice(start).trim();
|
|
2307
|
+
}
|
|
2308
|
+
function recoverToolArgumentValue(rawValue, property) {
|
|
2309
|
+
const trimmed = rawValue.trim();
|
|
2310
|
+
if (trimmed.length === 0) {
|
|
2311
|
+
return undefined;
|
|
2312
|
+
}
|
|
2313
|
+
try {
|
|
2314
|
+
const parsed = JSON.parse(trimmed);
|
|
2315
|
+
return validateToolArgumentType(parsed, property) ? parsed : undefined;
|
|
2316
|
+
}
|
|
2317
|
+
catch {
|
|
2318
|
+
if (property.type === 'string') {
|
|
2319
|
+
return stripLooseQuotes(trimmed);
|
|
2320
|
+
}
|
|
2321
|
+
if (property.type === 'boolean') {
|
|
2322
|
+
return trimmed === 'true' ? true : trimmed === 'false' ? false : undefined;
|
|
2323
|
+
}
|
|
2324
|
+
if (property.type === 'integer') {
|
|
2325
|
+
return /^-?\d+$/.test(trimmed) ? Number(trimmed) : undefined;
|
|
2326
|
+
}
|
|
2327
|
+
if (property.type === 'number') {
|
|
2328
|
+
return /^-?\d+(?:\.\d+)?$/.test(trimmed) ? Number(trimmed) : undefined;
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
2331
|
+
return undefined;
|
|
2332
|
+
}
|
|
2333
|
+
function stripLooseQuotes(value) {
|
|
2334
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
2335
|
+
return value.slice(1, -1);
|
|
2336
|
+
}
|
|
2337
|
+
return value;
|
|
2338
|
+
}
|
|
2339
|
+
function validateToolArguments(input, schema) {
|
|
2340
|
+
if (!schema) {
|
|
2341
|
+
return { ok: true };
|
|
2342
|
+
}
|
|
2343
|
+
const missing = (schema.required ?? []).filter(key => input[key] === undefined || input[key] === null);
|
|
2344
|
+
if (missing.length > 0) {
|
|
2345
|
+
return { ok: false, message: `Tool arguments are missing required field(s): ${missing.join(', ')}.` };
|
|
2346
|
+
}
|
|
2347
|
+
const invalid = [];
|
|
2348
|
+
for (const [key, value] of Object.entries(input)) {
|
|
2349
|
+
const property = schema.properties[key];
|
|
2350
|
+
if (!property) {
|
|
2351
|
+
if (schema.additionalProperties === false) {
|
|
2352
|
+
delete input[key];
|
|
2353
|
+
}
|
|
2354
|
+
continue;
|
|
2355
|
+
}
|
|
2356
|
+
if (!validateToolArgumentType(value, property)) {
|
|
2357
|
+
invalid.push(`${key} must be ${property.type}`);
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
return invalid.length > 0 ? { ok: false, message: `Tool arguments have invalid field type(s): ${invalid.join(', ')}.` } : { ok: true };
|
|
2361
|
+
}
|
|
2362
|
+
function validateToolArgumentType(value, property) {
|
|
2363
|
+
if (value === undefined || value === null) {
|
|
2364
|
+
return false;
|
|
2365
|
+
}
|
|
2366
|
+
if (property.type === 'array') {
|
|
2367
|
+
return Array.isArray(value);
|
|
2368
|
+
}
|
|
2369
|
+
if (property.type === 'object') {
|
|
2370
|
+
return typeof value === 'object' && !Array.isArray(value);
|
|
2371
|
+
}
|
|
2372
|
+
if (property.type === 'integer') {
|
|
2373
|
+
return typeof value === 'number' && Number.isInteger(value);
|
|
2374
|
+
}
|
|
2375
|
+
if (property.type === 'number') {
|
|
2376
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
2377
|
+
}
|
|
2378
|
+
return typeof value === property.type;
|
|
2379
|
+
}
|
|
2380
|
+
function isJsonLiteralLike(value) {
|
|
2381
|
+
return value === 'true' || value === 'false' || value === 'null' || /^-?\d+(?:\.\d+)?$/.test(value);
|
|
2382
|
+
}
|
|
2383
|
+
function safeJsonParseError(error) {
|
|
2384
|
+
return error instanceof Error ? error.message.replace(/ at position \d+.*/, '') : String(error);
|
|
2385
|
+
}
|
|
2386
|
+
function invalidToolArgumentsOutcome(toolCall, failure) {
|
|
2387
|
+
const decision = {
|
|
2388
|
+
status: 'deny',
|
|
2389
|
+
reason: 'Tool arguments need to be retried with valid JSON.',
|
|
2390
|
+
requirements: [],
|
|
2391
|
+
};
|
|
2392
|
+
const result = {
|
|
2393
|
+
ok: false,
|
|
2394
|
+
callId: toolCall.id,
|
|
2395
|
+
toolName: toolCall.name,
|
|
2396
|
+
error: {
|
|
2397
|
+
code: failure.code,
|
|
2398
|
+
message: failure.message,
|
|
2399
|
+
},
|
|
2400
|
+
};
|
|
2401
|
+
return {
|
|
2402
|
+
dryRun: false,
|
|
2403
|
+
callId: toolCall.id,
|
|
2404
|
+
toolName: toolCall.name,
|
|
2405
|
+
permissionDecision: decision,
|
|
2406
|
+
transcriptEvents: [
|
|
2407
|
+
{
|
|
2408
|
+
type: 'tool_result',
|
|
2409
|
+
callId: toolCall.id,
|
|
2410
|
+
toolName: toolCall.name,
|
|
2411
|
+
ok: false,
|
|
2412
|
+
summary: failure.message,
|
|
2413
|
+
errorCode: failure.code,
|
|
2414
|
+
},
|
|
2415
|
+
],
|
|
2416
|
+
result,
|
|
2417
|
+
};
|
|
2418
|
+
}
|
|
2419
|
+
function summarizeInvalidToolArgumentsCall(toolCall) {
|
|
2420
|
+
return `invalid arguments for ${toolCall.name}`;
|
|
2421
|
+
}
|
|
2422
|
+
function toolArgumentFailureMessage(retries, failure) {
|
|
2423
|
+
const hint = failure.repairedArguments
|
|
2424
|
+
? ` Recovered fields were: ${Object.keys(failure.repairedArguments).join(', ')}.`
|
|
2425
|
+
: '';
|
|
2426
|
+
return `The model kept sending invalid tool arguments after ${retries} repair attempt(s).${hint} Please retry the request so Remi can ask the model to issue valid tool JSON.`;
|
|
2427
|
+
}
|
|
2428
|
+
function escapeRegExp(value) {
|
|
2429
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
2430
|
+
}
|
|
2431
|
+
function summarizeToolCall(call) {
|
|
2432
|
+
const path = typeof call.input['path'] === 'string' ? call.input['path'] : undefined;
|
|
2433
|
+
const query = typeof call.input['query'] === 'string' ? call.input['query'] : undefined;
|
|
2434
|
+
const pattern = typeof call.input['pattern'] === 'string' ? call.input['pattern'] : undefined;
|
|
2435
|
+
const command = typeof call.input['command'] === 'string' ? call.input['command'] : undefined;
|
|
2436
|
+
const includeGlob = typeof call.input['includeGlob'] === 'string' ? call.input['includeGlob'] : undefined;
|
|
2437
|
+
if (call.toolName === 'search_text' && query) {
|
|
2438
|
+
return formatSearchToolCallSummary(query, path, includeGlob);
|
|
2439
|
+
}
|
|
2440
|
+
if (call.toolName === 'glob' && pattern) {
|
|
2441
|
+
return pattern;
|
|
2442
|
+
}
|
|
2443
|
+
if (call.toolName === 'todo_write') {
|
|
2444
|
+
return 'plan update';
|
|
2445
|
+
}
|
|
2446
|
+
const target = command ?? path ?? pattern ?? query;
|
|
2447
|
+
return target ? target : JSON.stringify(call.input);
|
|
2448
|
+
}
|
|
2449
|
+
function formatSearchToolCallSummary(query, path, includeGlob) {
|
|
2450
|
+
const scope = path && path !== '.' ? ` in ${path}` : '';
|
|
2451
|
+
const include = includeGlob ? ` matching ${includeGlob}` : '';
|
|
2452
|
+
return `${query}${scope}${include}`;
|
|
2453
|
+
}
|
|
2454
|
+
function summarizeToolResult(result) {
|
|
2455
|
+
if (!result.ok) {
|
|
2456
|
+
return summarizeToolError(result);
|
|
2457
|
+
}
|
|
2458
|
+
return summarizeToolOutput(result.output, result.truncated);
|
|
2459
|
+
}
|
|
2460
|
+
function summarizeToolError(result) {
|
|
2461
|
+
const message = compactText(result.error.message, 200);
|
|
2462
|
+
if (result.error.code === 'COMMAND_DENIED') {
|
|
2463
|
+
return message ? `Command could not run: ${message}` : 'Command could not run.';
|
|
2464
|
+
}
|
|
2465
|
+
if (result.error.code === 'PERMISSION_REQUIRED') {
|
|
2466
|
+
return message ? `Permission required: ${message}` : 'Permission required.';
|
|
2467
|
+
}
|
|
2468
|
+
if (result.error.code === 'PERMISSION_DENIED') {
|
|
2469
|
+
return message ? `Permission was not granted: ${message}` : 'Permission was not granted.';
|
|
2470
|
+
}
|
|
2471
|
+
if (result.error.code === 'UNKNOWN_TOOL' || result.error.code === 'PLAN_FIRST_TOOL_NOT_AVAILABLE') {
|
|
2472
|
+
return message ? `Tool is not available: ${message}` : 'Tool is not available.';
|
|
2473
|
+
}
|
|
2474
|
+
if (result.error.code === 'REPEATED_TOOL_CALL') {
|
|
2475
|
+
return message ? `Repeated tool call skipped: ${message}` : 'Repeated tool call skipped.';
|
|
2476
|
+
}
|
|
2477
|
+
return message ? `Tool failed: ${message}` : 'Tool failed.';
|
|
2478
|
+
}
|
|
2479
|
+
function summarizeToolOutput(output, truncated) {
|
|
2480
|
+
const parsed = parseToolOutput(output);
|
|
2481
|
+
const summary = parsed ? summarizeParsedToolOutput(parsed) : compactText(output, 240);
|
|
2482
|
+
return truncated ? `${summary} [truncated]` : summary;
|
|
2483
|
+
}
|
|
2484
|
+
function summarizeToolDetail(result) {
|
|
2485
|
+
if (!result.ok) {
|
|
2486
|
+
return undefined;
|
|
2487
|
+
}
|
|
2488
|
+
const parsed = parseToolOutput(result.output);
|
|
2489
|
+
if (!parsed) {
|
|
2490
|
+
return undefined;
|
|
2491
|
+
}
|
|
2492
|
+
if (typeof parsed['diffPreview'] === 'string') {
|
|
2493
|
+
const diffPreview = parsed['diffPreview'].trim();
|
|
2494
|
+
return diffPreview.length > 0 ? diffPreview : undefined;
|
|
2495
|
+
}
|
|
2496
|
+
if (typeof parsed['command'] === 'string' && typeof parsed['stdout'] === 'string') {
|
|
2497
|
+
const stdout = parsed['stdout'];
|
|
2498
|
+
const stderr = typeof parsed['stderr'] === 'string' ? parsed['stderr'] : '';
|
|
2499
|
+
return compactMultilineOutput(joinCommandOutput(stdout, stderr), 8_000);
|
|
2500
|
+
}
|
|
2501
|
+
return undefined;
|
|
2502
|
+
}
|
|
2503
|
+
function isRecoverableToolProtocolError(result) {
|
|
2504
|
+
if (result.ok) {
|
|
2505
|
+
return false;
|
|
2506
|
+
}
|
|
2507
|
+
if ((result.toolName === 'run_command' || result.toolName === 'execute_shell') && result.error.code === 'COMMAND_DENIED') {
|
|
2508
|
+
return true;
|
|
2509
|
+
}
|
|
2510
|
+
if (result.error.code === 'UNKNOWN_TOOL') {
|
|
2511
|
+
return true;
|
|
2512
|
+
}
|
|
2513
|
+
if (result.error.code === 'TOOL_ARGUMENTS_INVALID') {
|
|
2514
|
+
return true;
|
|
2515
|
+
}
|
|
2516
|
+
if (result.error.code === 'REPEATED_TOOL_CALL') {
|
|
2517
|
+
return true;
|
|
2518
|
+
}
|
|
2519
|
+
if ((result.toolName === 'edit_file' || result.toolName === 'write_file') && recoverableWriteToolProtocolErrors.has(result.error.code)) {
|
|
2520
|
+
return true;
|
|
2521
|
+
}
|
|
2522
|
+
return ((result.toolName === 'read_file' || result.toolName === 'list_files' || result.toolName === 'search_text' || result.toolName === 'glob') &&
|
|
2523
|
+
recoverableExplorationToolProtocolErrors.has(result.error.code));
|
|
2524
|
+
}
|
|
2525
|
+
function planItemsFromToolResult(result) {
|
|
2526
|
+
if (!result.ok || result.toolName !== 'todo_write') {
|
|
2527
|
+
return undefined;
|
|
2528
|
+
}
|
|
2529
|
+
const parsed = parseToolOutput(result.output);
|
|
2530
|
+
return planItemsFromRawItems(parsed?.['items']);
|
|
2531
|
+
}
|
|
2532
|
+
function toolResultMessageContent(result, call) {
|
|
2533
|
+
if (result.ok) {
|
|
2534
|
+
return result.output;
|
|
2535
|
+
}
|
|
2536
|
+
const guidance = repeatedToolCallRecoveryGuidance(result) ?? toolArgumentRecoveryGuidance(result) ?? runCommandRecoveryGuidance(result, call);
|
|
2537
|
+
return JSON.stringify({
|
|
2538
|
+
ok: false,
|
|
2539
|
+
error: result.error,
|
|
2540
|
+
...(guidance ? { guidance } : {}),
|
|
2541
|
+
});
|
|
2542
|
+
}
|
|
2543
|
+
function repeatedToolCallRecoveryGuidance(result) {
|
|
2544
|
+
if (result.ok || result.error.code !== 'REPEATED_TOOL_CALL') {
|
|
2545
|
+
return undefined;
|
|
2546
|
+
}
|
|
2547
|
+
return [
|
|
2548
|
+
'Do not call the same tool again with identical arguments in this turn.',
|
|
2549
|
+
'Use the previous result if it answered the question.',
|
|
2550
|
+
'If more evidence is needed, change the path, query, glob, or command so the next tool call can produce new information.',
|
|
2551
|
+
].join(' ');
|
|
2552
|
+
}
|
|
2553
|
+
function toolArgumentRecoveryGuidance(result) {
|
|
2554
|
+
if (result.ok || result.error.code !== 'TOOL_ARGUMENTS_INVALID') {
|
|
2555
|
+
return undefined;
|
|
2556
|
+
}
|
|
2557
|
+
return [
|
|
2558
|
+
'Retry the same tool call with arguments as a strict JSON object.',
|
|
2559
|
+
'Quote string values such as filesystem paths.',
|
|
2560
|
+
'Include every required field from the tool schema.',
|
|
2561
|
+
'Example for a path argument: {"path":"/absolute/or/relative/path"}.',
|
|
2562
|
+
].join(' ');
|
|
2563
|
+
}
|
|
2564
|
+
function runCommandRecoveryGuidance(result, call) {
|
|
2565
|
+
if (result.ok ||
|
|
2566
|
+
(result.toolName !== 'run_command' && result.toolName !== 'execute_shell') ||
|
|
2567
|
+
!['PERMISSION_DENIED', 'PERMISSION_REQUIRED', 'COMMAND_DENIED'].includes(result.error.code)) {
|
|
2568
|
+
return undefined;
|
|
2569
|
+
}
|
|
2570
|
+
const requestedCommand = typeof call?.input['command'] === 'string' ? call.input['command'] : undefined;
|
|
2571
|
+
if (requestedCommand && /\b(?:rm|rmdir)\b/.test(requestedCommand)) {
|
|
2572
|
+
if (result.toolName === 'run_command') {
|
|
2573
|
+
return [
|
|
2574
|
+
'run_command cannot run rm/rmdir deletion.',
|
|
2575
|
+
'Prefer delete_file for regular files and delete_directory for empty directories.',
|
|
2576
|
+
'If the user specifically needs the Unix command form, use execute_shell with a simple rm/rmdir command and the correct cwd so approval can be requested.',
|
|
2577
|
+
'Do not retry with pipes, redirects, command substitution, shell wrappers, cd, or a broader command.',
|
|
2578
|
+
].join(' ');
|
|
2579
|
+
}
|
|
2580
|
+
return [
|
|
2581
|
+
'This rm/rmdir command was not allowed.',
|
|
2582
|
+
'Do not retry with a broader or more complex shell form.',
|
|
2583
|
+
'Prefer delete_file for regular files and delete_directory for empty directories when those tools can express the target.',
|
|
2584
|
+
'If the safety policy blocked the target, explain the exact blocker instead of claiming deletion.',
|
|
2585
|
+
].join(' ');
|
|
2586
|
+
}
|
|
2587
|
+
if (result.toolName === 'execute_shell') {
|
|
2588
|
+
return [
|
|
2589
|
+
'Do not retry with pipes, redirects, command substitution, environment assignment, or another shell wrapper.',
|
|
2590
|
+
'If approval was denied, finish the file work that can be done safely and tell the user the exact validation command to run manually.',
|
|
2591
|
+
].join(' ');
|
|
2592
|
+
}
|
|
2593
|
+
return [
|
|
2594
|
+
'Do not retry this command in another shell form.',
|
|
2595
|
+
'run_command only supports safe read-only local inspection commands and read-only git subcommands.',
|
|
2596
|
+
'If this was a build/test/validation command that can be expressed as a simple command, call execute_shell with its cwd set to the target project directory so the runtime can request approval.',
|
|
2597
|
+
'Do not pass cd, pipes, redirects, backgrounding, command substitution, shell wrappers, or environment assignments to execute_shell.',
|
|
2598
|
+
'If it cannot be expressed with execute_shell, finish the file work and tell the user the manual command to run.',
|
|
2599
|
+
].join(' ');
|
|
2600
|
+
}
|
|
2601
|
+
function appendToolTranscriptEvents(cwd, sessionStore, events) {
|
|
2602
|
+
const artifacts = new Map();
|
|
2603
|
+
for (const event of events) {
|
|
2604
|
+
if (event.type === 'tool_call') {
|
|
2605
|
+
sessionStore.append({
|
|
2606
|
+
type: 'tool_call',
|
|
2607
|
+
callId: event.callId,
|
|
2608
|
+
toolName: event.toolName,
|
|
2609
|
+
input: event.input,
|
|
2610
|
+
status: event.status,
|
|
2611
|
+
});
|
|
2612
|
+
continue;
|
|
2613
|
+
}
|
|
2614
|
+
const artifact = maybeWriteToolResultArtifact(cwd, sessionStore.sessionId, event);
|
|
2615
|
+
if (artifact) {
|
|
2616
|
+
artifacts.set(event.callId, artifact);
|
|
2617
|
+
}
|
|
2618
|
+
sessionStore.append({
|
|
2619
|
+
type: 'tool_result',
|
|
2620
|
+
callId: event.callId,
|
|
2621
|
+
toolName: event.toolName,
|
|
2622
|
+
ok: event.ok,
|
|
2623
|
+
summary: event.ok ? summarizeToolOutput(event.summary, event.truncated) : event.summary,
|
|
2624
|
+
...(event.bytes !== undefined ? { bytes: event.bytes } : {}),
|
|
2625
|
+
...(event.truncated !== undefined ? { truncated: event.truncated } : {}),
|
|
2626
|
+
...(event.errorCode !== undefined ? { errorCode: event.errorCode } : {}),
|
|
2627
|
+
...(artifact ? { artifact } : {}),
|
|
2628
|
+
});
|
|
2629
|
+
}
|
|
2630
|
+
return artifacts;
|
|
2631
|
+
}
|
|
2632
|
+
function maybeWriteToolResultArtifact(cwd, sessionId, event) {
|
|
2633
|
+
if (!event.ok || !shouldWriteToolResultArtifact(event)) {
|
|
2634
|
+
return undefined;
|
|
2635
|
+
}
|
|
2636
|
+
return writeToolResultArtifact(cwd, sessionId, {
|
|
2637
|
+
callId: event.callId,
|
|
2638
|
+
toolName: event.toolName,
|
|
2639
|
+
content: event.summary,
|
|
2640
|
+
contentType: looksLikeJson(event.summary) ? 'application/json' : 'text/plain',
|
|
2641
|
+
});
|
|
2642
|
+
}
|
|
2643
|
+
function shouldWriteToolResultArtifact(event) {
|
|
2644
|
+
const bytes = event.bytes ?? Buffer.byteLength(event.summary, 'utf8');
|
|
2645
|
+
return event.truncated === true || bytes >= toolResultArtifactThresholdBytes;
|
|
2646
|
+
}
|
|
2647
|
+
function looksLikeJson(value) {
|
|
2648
|
+
const trimmed = value.trim();
|
|
2649
|
+
return (trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'));
|
|
2650
|
+
}
|
|
2651
|
+
function recordRecalledMemoryUse(cwd, memoryIds) {
|
|
2652
|
+
if (memoryIds.length === 0) {
|
|
2653
|
+
return;
|
|
2654
|
+
}
|
|
2655
|
+
try {
|
|
2656
|
+
recordMemoryUse(cwd, memoryIds);
|
|
2657
|
+
}
|
|
2658
|
+
catch {
|
|
2659
|
+
// Memory usage stats must never fail the user-facing chat turn.
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
function recordSessionMemoryIfNeeded(cwd, sessionId, config) {
|
|
2663
|
+
if (config.sessionStore || config.disableSessionMemory) {
|
|
2664
|
+
return;
|
|
2665
|
+
}
|
|
2666
|
+
try {
|
|
2667
|
+
maybeUpdateSessionMemory(cwd, sessionId, {
|
|
2668
|
+
...(config.maxSessionMemoryChars !== undefined ? { maxSummaryChars: config.maxSessionMemoryChars } : {}),
|
|
2669
|
+
...(config.sessionMemoryMinimumTokensToInit !== undefined ? { minimumTokensToInit: config.sessionMemoryMinimumTokensToInit } : {}),
|
|
2670
|
+
...(config.sessionMemoryMinimumTokensBetweenUpdates !== undefined ? { minimumTokensBetweenUpdates: config.sessionMemoryMinimumTokensBetweenUpdates } : {}),
|
|
2671
|
+
...(config.sessionMemoryToolCallsBetweenUpdates !== undefined ? { toolCallsBetweenUpdates: config.sessionMemoryToolCallsBetweenUpdates } : {}),
|
|
2672
|
+
});
|
|
2673
|
+
}
|
|
2674
|
+
catch {
|
|
2675
|
+
// Session memory is continuity help. It must never fail the active user turn.
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
function allowedReadRoots(cwd, env) {
|
|
2679
|
+
const roots = [cwd];
|
|
2680
|
+
if (env.HOME && env.HOME !== cwd) {
|
|
2681
|
+
roots.push(env.HOME);
|
|
2682
|
+
}
|
|
2683
|
+
return roots;
|
|
2684
|
+
}
|
|
2685
|
+
function allowedWriteRoots(cwd, env) {
|
|
2686
|
+
void env;
|
|
2687
|
+
return [cwd];
|
|
2688
|
+
}
|
|
2689
|
+
function latestPermissionRules(cwd, env, runtimeRules = []) {
|
|
2690
|
+
return [...(loadRemiConfig({ cwd, env }).config.permissions?.allow ?? []), ...runtimeRules];
|
|
2691
|
+
}
|
|
2692
|
+
function runtimePermissionRules(source) {
|
|
2693
|
+
if (!source) {
|
|
2694
|
+
return [];
|
|
2695
|
+
}
|
|
2696
|
+
return typeof source === 'function' ? source() : source;
|
|
2697
|
+
}
|
|
2698
|
+
function mergeTokenUsage(current, next) {
|
|
2699
|
+
return {
|
|
2700
|
+
inputTokens: (current?.inputTokens ?? 0) + next.inputTokens,
|
|
2701
|
+
outputTokens: (current?.outputTokens ?? 0) + next.outputTokens,
|
|
2702
|
+
totalTokens: normalizedTokenUsageTotal(current) + normalizedTokenUsageTotal(next),
|
|
2703
|
+
...mergeOptionalTokenField('cachedInputTokens', current, next),
|
|
2704
|
+
...mergeOptionalTokenField('reasoningOutputTokens', current, next),
|
|
2705
|
+
};
|
|
2706
|
+
}
|
|
2707
|
+
function tokenUsageDelta(previous, next) {
|
|
2708
|
+
const inputTokens = Math.max(0, next.inputTokens - (previous?.inputTokens ?? 0));
|
|
2709
|
+
const outputTokens = Math.max(0, next.outputTokens - (previous?.outputTokens ?? 0));
|
|
2710
|
+
const cachedInputTokens = optionalTokenDelta(previous?.cachedInputTokens, next.cachedInputTokens);
|
|
2711
|
+
const reasoningOutputTokens = optionalTokenDelta(previous?.reasoningOutputTokens, next.reasoningOutputTokens);
|
|
2712
|
+
const totalTokens = Math.max(0, normalizedTokenUsageTotal(next) - normalizedTokenUsageTotal(previous));
|
|
2713
|
+
return {
|
|
2714
|
+
inputTokens,
|
|
2715
|
+
outputTokens,
|
|
2716
|
+
totalTokens,
|
|
2717
|
+
...(cachedInputTokens !== undefined ? { cachedInputTokens } : {}),
|
|
2718
|
+
...(reasoningOutputTokens !== undefined ? { reasoningOutputTokens } : {}),
|
|
2719
|
+
};
|
|
2720
|
+
}
|
|
2721
|
+
function optionalTokenDelta(previous, next) {
|
|
2722
|
+
if (previous === undefined && next === undefined) {
|
|
2723
|
+
return undefined;
|
|
2724
|
+
}
|
|
2725
|
+
return Math.max(0, (next ?? 0) - (previous ?? 0));
|
|
2726
|
+
}
|
|
2727
|
+
function hasTokenUsage(usage) {
|
|
2728
|
+
return usage.inputTokens > 0 || usage.outputTokens > 0 || usage.totalTokens > 0 || (usage.cachedInputTokens ?? 0) > 0 || (usage.reasoningOutputTokens ?? 0) > 0;
|
|
2729
|
+
}
|
|
2730
|
+
function normalizedTokenUsageTotal(usage) {
|
|
2731
|
+
if (!usage) {
|
|
2732
|
+
return 0;
|
|
2733
|
+
}
|
|
2734
|
+
return Math.max(usage.totalTokens, usage.inputTokens + usage.outputTokens + (usage.cachedInputTokens ?? 0));
|
|
2735
|
+
}
|
|
2736
|
+
function mergeOptionalTokenField(field, current, next) {
|
|
2737
|
+
const currentValue = current?.[field];
|
|
2738
|
+
const nextValue = next[field];
|
|
2739
|
+
if (currentValue === undefined && nextValue === undefined) {
|
|
2740
|
+
return {};
|
|
2741
|
+
}
|
|
2742
|
+
return { [field]: (currentValue ?? 0) + (nextValue ?? 0) };
|
|
2743
|
+
}
|
|
2744
|
+
function parseToolOutput(output) {
|
|
2745
|
+
try {
|
|
2746
|
+
const parsed = JSON.parse(output);
|
|
2747
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
2748
|
+
return undefined;
|
|
2749
|
+
}
|
|
2750
|
+
return parsed;
|
|
2751
|
+
}
|
|
2752
|
+
catch {
|
|
2753
|
+
return undefined;
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
function summarizeParsedToolOutput(output) {
|
|
2757
|
+
if (Array.isArray(output['items'])) {
|
|
2758
|
+
return `plan updated (${output['items'].length} ${output['items'].length === 1 ? 'item' : 'items'})`;
|
|
2759
|
+
}
|
|
2760
|
+
if (Array.isArray(output['entries'])) {
|
|
2761
|
+
return summarizePathArray('entries', output['entries'], output['path']);
|
|
2762
|
+
}
|
|
2763
|
+
if (Array.isArray(output['matches'])) {
|
|
2764
|
+
return summarizePathArray('matches', output['matches'], output['path']);
|
|
2765
|
+
}
|
|
2766
|
+
if (typeof output['content'] === 'string') {
|
|
2767
|
+
const bytes = typeof output['bytes'] === 'number' ? output['bytes'] : output['content'].length;
|
|
2768
|
+
const path = typeof output['path'] === 'string' ? ` from ${output['path']}` : '';
|
|
2769
|
+
return `read ${bytes} bytes${path}`;
|
|
2770
|
+
}
|
|
2771
|
+
if (typeof output['path'] === 'string' && typeof output['created'] === 'boolean' && typeof output['bytes'] !== 'number') {
|
|
2772
|
+
return `${output['created'] ? 'created' : 'exists'} ${output['path']}`;
|
|
2773
|
+
}
|
|
2774
|
+
if (typeof output['path'] === 'string' &&
|
|
2775
|
+
output['deleted'] === true &&
|
|
2776
|
+
typeof output['addedLines'] === 'number' &&
|
|
2777
|
+
typeof output['removedLines'] === 'number') {
|
|
2778
|
+
return `deleted ${output['path']} (+${output['addedLines']} -${output['removedLines']})`;
|
|
2779
|
+
}
|
|
2780
|
+
if (typeof output['path'] === 'string' && output['deleted'] === true) {
|
|
2781
|
+
return `deleted ${output['path']}`;
|
|
2782
|
+
}
|
|
2783
|
+
if (typeof output['path'] === 'string' && typeof output['bytes'] === 'number') {
|
|
2784
|
+
if (typeof output['addedLines'] === 'number' && typeof output['removedLines'] === 'number') {
|
|
2785
|
+
return `edited ${output['path']} (+${output['addedLines']} -${output['removedLines']})`;
|
|
2786
|
+
}
|
|
2787
|
+
const overwritten = output['overwritten'] === true;
|
|
2788
|
+
return `${overwritten ? 'updated' : 'created'} ${output['path']} (${output['bytes']} bytes)`;
|
|
2789
|
+
}
|
|
2790
|
+
if (typeof output['command'] === 'string' && typeof output['stdout'] === 'string') {
|
|
2791
|
+
const exitCode = typeof output['exitCode'] === 'number' ? output['exitCode'] : 0;
|
|
2792
|
+
const stderr = typeof output['stderr'] === 'string' ? output['stderr'] : '';
|
|
2793
|
+
const preview = compactText(output['stdout'] || stderr, 160);
|
|
2794
|
+
return preview.length > 0 ? `exit ${exitCode}: ${preview}` : `exit ${exitCode}`;
|
|
2795
|
+
}
|
|
2796
|
+
return compactText(JSON.stringify(output), 240);
|
|
2797
|
+
}
|
|
2798
|
+
function summarizePathArray(label, rawItems, rawPath) {
|
|
2799
|
+
const items = rawItems
|
|
2800
|
+
.map(item => {
|
|
2801
|
+
if (typeof item === 'string') {
|
|
2802
|
+
return item;
|
|
2803
|
+
}
|
|
2804
|
+
if (!item || typeof item !== 'object') {
|
|
2805
|
+
return undefined;
|
|
2806
|
+
}
|
|
2807
|
+
const record = item;
|
|
2808
|
+
const path = typeof record['path'] === 'string' ? record['path'] : undefined;
|
|
2809
|
+
const line = typeof record['line'] === 'number' ? record['line'] : undefined;
|
|
2810
|
+
return path && line !== undefined ? `${path}:${line}` : path;
|
|
2811
|
+
})
|
|
2812
|
+
.filter((item) => Boolean(item));
|
|
2813
|
+
const prefix = `${rawItems.length} ${label}`;
|
|
2814
|
+
const root = typeof rawPath === 'string' ? ` in ${rawPath}` : '';
|
|
2815
|
+
const preview = items.slice(0, 4).join(', ');
|
|
2816
|
+
return preview.length > 0 ? `${prefix}${root}: ${preview}` : `${prefix}${root}`;
|
|
2817
|
+
}
|
|
2818
|
+
function compactText(text, maxLength) {
|
|
2819
|
+
const compacted = text.replace(/\s+/g, ' ').trim();
|
|
2820
|
+
return compacted.length > maxLength ? `${compacted.slice(0, maxLength - 3)}...` : compacted;
|
|
2821
|
+
}
|
|
2822
|
+
function joinCommandOutput(stdout, stderr) {
|
|
2823
|
+
if (!stdout) {
|
|
2824
|
+
return stderr;
|
|
2825
|
+
}
|
|
2826
|
+
if (!stderr) {
|
|
2827
|
+
return stdout;
|
|
2828
|
+
}
|
|
2829
|
+
return stdout.endsWith('\n') ? `${stdout}${stderr}` : `${stdout}\n${stderr}`;
|
|
2830
|
+
}
|
|
2831
|
+
function compactMultilineOutput(text, maxLength) {
|
|
2832
|
+
const trimmed = text.replace(/\r\n/g, '\n').trimEnd();
|
|
2833
|
+
if (trimmed.length === 0) {
|
|
2834
|
+
return undefined;
|
|
2835
|
+
}
|
|
2836
|
+
return trimmed.length > maxLength ? `${trimmed.slice(0, maxLength - 24)}\n... output truncated ...` : trimmed;
|
|
2837
|
+
}
|
|
2838
|
+
function safeErrorMessage(error) {
|
|
2839
|
+
if (!(error instanceof Error)) {
|
|
2840
|
+
return String(error);
|
|
2841
|
+
}
|
|
2842
|
+
return error.message.replace(/Bearer\s+[A-Za-z0-9._-]+/g, 'Bearer [redacted]');
|
|
2843
|
+
}
|