@ai-setting/roy-agent-cli 1.0.0
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 +126 -0
- package/dist/bin/roy.js +127297 -0
- package/dist/roy-agent-darwin-arm64/bin/roy.js +127297 -0
- package/dist/roy-agent-darwin-x64/bin/roy.js +127297 -0
- package/dist/roy-agent-linux-arm64/bin/roy.js +127297 -0
- package/dist/roy-agent-linux-x64/bin/roy.js +127297 -0
- package/dist/roy-agent-windows-x64/bin/roy.js +127297 -0
- package/package.json +91 -0
- package/src/bin/roy.ts +12 -0
- package/src/cli.ts +101 -0
- package/src/commands/act.ts +480 -0
- package/src/commands/commands-add.ts +110 -0
- package/src/commands/commands-dirs.ts +70 -0
- package/src/commands/commands-info.ts +90 -0
- package/src/commands/commands-list.ts +161 -0
- package/src/commands/commands-remove.ts +147 -0
- package/src/commands/commands.ts +55 -0
- package/src/commands/config/config-service.test.ts +449 -0
- package/src/commands/config/config-service.ts +312 -0
- package/src/commands/config/deep-merge.test.ts +168 -0
- package/src/commands/config/deep-merge.ts +63 -0
- package/src/commands/config/export.ts +97 -0
- package/src/commands/config/filter-history-e2e.test.ts +141 -0
- package/src/commands/config/import-preserve-refs.test.ts +212 -0
- package/src/commands/config/import.ts +119 -0
- package/src/commands/config/index.ts +35 -0
- package/src/commands/config/list.ts +281 -0
- package/src/commands/config/roy-config-e2e.test.ts +297 -0
- package/src/commands/config/types.ts +54 -0
- package/src/commands/debug/index.ts +38 -0
- package/src/commands/debug/log.test.ts +233 -0
- package/src/commands/debug/log.ts +123 -0
- package/src/commands/debug/span.test.ts +297 -0
- package/src/commands/debug/span.ts +211 -0
- package/src/commands/debug/trace.test.ts +254 -0
- package/src/commands/debug/trace.ts +140 -0
- package/src/commands/eventsource/add.ts +133 -0
- package/src/commands/eventsource/index.ts +48 -0
- package/src/commands/eventsource/list.ts +194 -0
- package/src/commands/eventsource/remove.ts +95 -0
- package/src/commands/eventsource/start.ts +103 -0
- package/src/commands/eventsource/status.ts +185 -0
- package/src/commands/eventsource/stop.ts +89 -0
- package/src/commands/index.ts +22 -0
- package/src/commands/input-handler.test.ts +76 -0
- package/src/commands/input-handler.ts +43 -0
- package/src/commands/interactive-esc.test.ts +254 -0
- package/src/commands/interactive.shutdown.test.ts +122 -0
- package/src/commands/interactive.test.ts +221 -0
- package/src/commands/interactive.ts +1015 -0
- package/src/commands/lsp/check.ts +92 -0
- package/src/commands/lsp/index.ts +32 -0
- package/src/commands/lsp/install.ts +126 -0
- package/src/commands/lsp/list.ts +64 -0
- package/src/commands/mcp/index.ts +27 -0
- package/src/commands/mcp/list.ts +116 -0
- package/src/commands/mcp/reload.ts +70 -0
- package/src/commands/mcp/tools.ts +121 -0
- package/src/commands/memory/extract-e2e.test.ts +388 -0
- package/src/commands/memory/index.ts +11 -0
- package/src/commands/memory/memory-simplified.test.ts +58 -0
- package/src/commands/memory/memory.ts +25 -0
- package/src/commands/memory/organize.ts +300 -0
- package/src/commands/memory/recall.test.ts +120 -0
- package/src/commands/memory/recall.ts +88 -0
- package/src/commands/memory/record-extract-handle-query.test.ts +385 -0
- package/src/commands/memory/record-prompt-component.test.ts +343 -0
- package/src/commands/memory/record.test.ts +92 -0
- package/src/commands/memory/record.ts +332 -0
- package/src/commands/plugin.test.ts +292 -0
- package/src/commands/plugin.ts +267 -0
- package/src/commands/sessions/active.ts +96 -0
- package/src/commands/sessions/add-message.ts +96 -0
- package/src/commands/sessions/checkpoints.ts +154 -0
- package/src/commands/sessions/compact.test.ts +215 -0
- package/src/commands/sessions/compact.ts +269 -0
- package/src/commands/sessions/delete.ts +236 -0
- package/src/commands/sessions/get.ts +165 -0
- package/src/commands/sessions/grep.ts +233 -0
- package/src/commands/sessions/index.ts +95 -0
- package/src/commands/sessions/list.ts +210 -0
- package/src/commands/sessions/messages.test.ts +333 -0
- package/src/commands/sessions/messages.ts +248 -0
- package/src/commands/sessions/mock.ts +194 -0
- package/src/commands/sessions/new.ts +82 -0
- package/src/commands/sessions/rename.ts +98 -0
- package/src/commands/shared/event-handler.ts +213 -0
- package/src/commands/shared/event-message-formatter.ts +295 -0
- package/src/commands/shared/index.ts +11 -0
- package/src/commands/shared/query-executor.test.ts +434 -0
- package/src/commands/shared/query-executor.ts +324 -0
- package/src/commands/shared/repl-engine.test.ts +354 -0
- package/src/commands/shared/session-manager.test.ts +212 -0
- package/src/commands/shared/session-manager.ts +114 -0
- package/src/commands/skills/get.ts +90 -0
- package/src/commands/skills/index.ts +39 -0
- package/src/commands/skills/list.ts +129 -0
- package/src/commands/skills/reload.ts +59 -0
- package/src/commands/skills/search.ts +132 -0
- package/src/commands/skills/show-config.ts +93 -0
- package/src/commands/tasks/complete.ts +92 -0
- package/src/commands/tasks/create.ts +118 -0
- package/src/commands/tasks/delete.ts +86 -0
- package/src/commands/tasks/get.ts +116 -0
- package/src/commands/tasks/index.ts +53 -0
- package/src/commands/tasks/list.ts +140 -0
- package/src/commands/tasks/operations.ts +120 -0
- package/src/commands/tasks/update.ts +122 -0
- package/src/commands/tools/exec-tool.ts +128 -0
- package/src/commands/tools/get.ts +114 -0
- package/src/commands/tools/index.ts +35 -0
- package/src/commands/tools/list.ts +107 -0
- package/src/commands/tools/shared/index.ts +7 -0
- package/src/commands/tools/shared/schema-helper.ts +111 -0
- package/src/commands/workflow/commands/add.ts +315 -0
- package/src/commands/workflow/commands/get.ts +193 -0
- package/src/commands/workflow/commands/list.ts +137 -0
- package/src/commands/workflow/commands/nodes.ts +528 -0
- package/src/commands/workflow/commands/remove.ts +94 -0
- package/src/commands/workflow/commands/run.ts +398 -0
- package/src/commands/workflow/commands/status.ts +147 -0
- package/src/commands/workflow/commands/stop.ts +91 -0
- package/src/commands/workflow/commands/update.ts +130 -0
- package/src/commands/workflow/commands/validate.ts +139 -0
- package/src/commands/workflow/commands/workflow-cli.test.ts +196 -0
- package/src/commands/workflow/index.ts +65 -0
- package/src/commands/workflow/renderers.ts +358 -0
- package/src/commands/workflow/validators/index.ts +8 -0
- package/src/commands/workflow/validators/node-validator-factory.ts +40 -0
- package/src/commands/workflow/validators/node-validator.ts +125 -0
- package/src/commands/workflow/validators/nodes/agent-node-validator.ts +58 -0
- package/src/commands/workflow/validators/nodes/condition-node-validator.ts +34 -0
- package/src/commands/workflow/validators/nodes/decorator-node-validator.ts +45 -0
- package/src/commands/workflow/validators/nodes/merge-node-validator.ts +46 -0
- package/src/commands/workflow/validators/nodes/skill-node-validator.ts +33 -0
- package/src/commands/workflow/validators/nodes/tool-node-validator.ts +54 -0
- package/src/commands/workflow/validators/nodes/workflow-node-validator.ts +33 -0
- package/src/commands/workflow/validators/types.ts +78 -0
- package/src/commands/workflow/validators/workflow-validator.test.ts +273 -0
- package/src/commands/workflow/validators/workflow-validator.ts +320 -0
- package/src/index.ts +19 -0
- package/src/plugin/apply.ts +103 -0
- package/src/plugin/discover.ts +219 -0
- package/src/plugin/index.ts +45 -0
- package/src/plugin/registry.ts +272 -0
- package/src/plugin/types.ts +165 -0
- package/src/services/context-handler.service.test.ts +501 -0
- package/src/services/context-handler.service.ts +372 -0
- package/src/services/environment.service.commands-prompt.test.ts +167 -0
- package/src/services/environment.service.ts +656 -0
- package/src/services/output.service.test.ts +92 -0
- package/src/services/output.service.ts +122 -0
- package/src/services/quiet-mode.service.test.ts +114 -0
- package/src/services/quiet-mode.service.ts +81 -0
- package/src/services/stream-output.service.test.ts +214 -0
- package/src/services/stream-output.service.ts +323 -0
- package/src/util/which.test.ts +101 -0
- package/src/util/which.ts +55 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Context Handler Service
|
|
3
|
+
*
|
|
4
|
+
* 处理 env.handle_query 的上下文阈值检测和压缩逻辑
|
|
5
|
+
*
|
|
6
|
+
* 功能:
|
|
7
|
+
* 1. 包装 handle_query,捕获 ContextError
|
|
8
|
+
* 2. 两阶段自动压缩:
|
|
9
|
+
* - 阶段1: 调用 SessionComponent.generateCompactHint() 生成场景化提示
|
|
10
|
+
* - 阶段2: 调用 SessionComponent.compact() 并传入 scenarioHint
|
|
11
|
+
* 3. 用 checkpoint 构造新 query 并重试
|
|
12
|
+
* 4. 生成 trace id 用于调用链追踪
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Environment, AgentContext } from "@ai-setting/roy-agent-core";
|
|
16
|
+
import type { SessionComponent, SessionCheckpoint } from "@ai-setting/roy-agent-core";
|
|
17
|
+
import {
|
|
18
|
+
ContextError,
|
|
19
|
+
ErrorCodes,
|
|
20
|
+
getTracerProvider,
|
|
21
|
+
type OTelTracer,
|
|
22
|
+
TracedAs,
|
|
23
|
+
} from "@ai-setting/roy-agent-core";
|
|
24
|
+
import { createLogger } from "@ai-setting/roy-agent-core";
|
|
25
|
+
|
|
26
|
+
// 创建 logger
|
|
27
|
+
const logger = createLogger("context-handler");
|
|
28
|
+
|
|
29
|
+
export interface CompactResult {
|
|
30
|
+
success: boolean;
|
|
31
|
+
checkpoint?: SessionCheckpoint;
|
|
32
|
+
newQuery?: string;
|
|
33
|
+
error?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface HandleQueryOptions {
|
|
37
|
+
/** 最大重试次数(compact 后) */
|
|
38
|
+
maxRetries?: number;
|
|
39
|
+
/** 是否自动压缩 */
|
|
40
|
+
autoCompact?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Context Handler Service
|
|
45
|
+
*
|
|
46
|
+
* 处理 env.handle_query 的上下文阈值检测和压缩逻辑
|
|
47
|
+
*
|
|
48
|
+
* 采用两阶段自动压缩流程:
|
|
49
|
+
* - 阶段1: 根据历史消息生成场景化的压缩提示 (generateCompactHint)
|
|
50
|
+
* - 阶段2: 使用生成的提示进行实际的压缩 (compact with scenarioHint)
|
|
51
|
+
*/
|
|
52
|
+
export class ContextHandlerService {
|
|
53
|
+
private readonly maxRetries: number;
|
|
54
|
+
private readonly autoCompact: boolean;
|
|
55
|
+
private tracer?: OTelTracer;
|
|
56
|
+
|
|
57
|
+
constructor(
|
|
58
|
+
private env: Environment,
|
|
59
|
+
private sessionComponent: SessionComponent,
|
|
60
|
+
options: HandleQueryOptions = {}
|
|
61
|
+
) {
|
|
62
|
+
this.maxRetries = options.maxRetries ?? 1;
|
|
63
|
+
this.autoCompact = options.autoCompact ?? true;
|
|
64
|
+
|
|
65
|
+
// 初始化 tracer
|
|
66
|
+
this.initializeTracer();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 初始化 tracer
|
|
71
|
+
*/
|
|
72
|
+
private initializeTracer(): void {
|
|
73
|
+
try {
|
|
74
|
+
this.tracer = getTracerProvider().getTracer("roy-tracer");
|
|
75
|
+
} catch {
|
|
76
|
+
// Tracer 可能尚未初始化,延迟初始化
|
|
77
|
+
this.tracer = undefined;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 获取 tracer 实例
|
|
83
|
+
*/
|
|
84
|
+
private getTracerInstance(): OTelTracer | undefined {
|
|
85
|
+
if (!this.tracer) {
|
|
86
|
+
try {
|
|
87
|
+
this.tracer = getTracerProvider().getTracer("roy-tracer");
|
|
88
|
+
} catch {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return this.tracer;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 包装 handle_query,添加上下文阈值处理和 trace 追踪
|
|
97
|
+
*/
|
|
98
|
+
async handleQueryWithContext(
|
|
99
|
+
query: string,
|
|
100
|
+
context?: AgentContext
|
|
101
|
+
): Promise<string> {
|
|
102
|
+
// 从 metadata 获取 trace id(如果上层已设置)
|
|
103
|
+
const traceId = (context?.metadata as Record<string, unknown>)?.traceId as string;
|
|
104
|
+
const sessionId = context?.sessionId;
|
|
105
|
+
|
|
106
|
+
// 获取 tracer 实例并启动根 span
|
|
107
|
+
const tracer = this.getTracerInstance();
|
|
108
|
+
const span = tracer?.startSpan("handleQueryWithContext", {
|
|
109
|
+
attributes: {
|
|
110
|
+
query: query.substring(0, 200),
|
|
111
|
+
sessionId,
|
|
112
|
+
traceId,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
let lastError: Error | undefined;
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
120
|
+
try {
|
|
121
|
+
// 正常执行 handle_query
|
|
122
|
+
const result = await this.env.handle_query(query, context);
|
|
123
|
+
|
|
124
|
+
// 结束根 span(成功)
|
|
125
|
+
if (span) {
|
|
126
|
+
span?.end(result);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return result;
|
|
130
|
+
|
|
131
|
+
} catch (error) {
|
|
132
|
+
// 检查是否是上下文阈值异常
|
|
133
|
+
if (error instanceof ContextError && error.code === ErrorCodes.CONTEXT_THRESHOLD_EXCEEDED) {
|
|
134
|
+
lastError = error;
|
|
135
|
+
|
|
136
|
+
if (attempt >= this.maxRetries) {
|
|
137
|
+
logger.error("Context threshold exceeded after max retries", {
|
|
138
|
+
attempt,
|
|
139
|
+
maxRetries: this.maxRetries
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// 结束根 span(失败)
|
|
143
|
+
if (span) {
|
|
144
|
+
span?.end(undefined, error as Error);
|
|
145
|
+
}
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!this.autoCompact) {
|
|
150
|
+
logger.warn("Auto compact disabled, propagating error");
|
|
151
|
+
|
|
152
|
+
// 结束根 span(失败)
|
|
153
|
+
if (span) {
|
|
154
|
+
span?.end(undefined, error as Error);
|
|
155
|
+
}
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
logger.info(`Context threshold exceeded, compacting (attempt ${attempt + 1}/${this.maxRetries + 1})`);
|
|
160
|
+
|
|
161
|
+
// 发布阈值即将压缩事件
|
|
162
|
+
this.env.pushEnvEvent({
|
|
163
|
+
type: "context.threshold_exceeded",
|
|
164
|
+
metadata: {
|
|
165
|
+
sessionId,
|
|
166
|
+
traceId,
|
|
167
|
+
},
|
|
168
|
+
payload: {
|
|
169
|
+
totalTokens: error.usage?.totalTokens,
|
|
170
|
+
contextWindow: error.contextWindow,
|
|
171
|
+
attempt: attempt + 1,
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// 执行两阶段压缩
|
|
176
|
+
const compactResult = await this.compactSessionWithTwoPhases(
|
|
177
|
+
context?.sessionId,
|
|
178
|
+
query
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
if (!compactResult.success) {
|
|
182
|
+
logger.error("Compact failed", { error: compactResult.error });
|
|
183
|
+
|
|
184
|
+
// 结束根 span(失败)
|
|
185
|
+
if (span) {
|
|
186
|
+
span?.end(undefined, error as Error);
|
|
187
|
+
}
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 用新的 query 重试
|
|
192
|
+
if (compactResult.newQuery) {
|
|
193
|
+
query = compactResult.newQuery;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 其他错误直接抛出
|
|
200
|
+
// 结束根 span(失败)
|
|
201
|
+
if (span) {
|
|
202
|
+
span?.end(undefined, error as Error);
|
|
203
|
+
}
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
throw lastError ?? new Error("Unexpected exit from handleQueryWithContext");
|
|
209
|
+
} finally {
|
|
210
|
+
// 清理 span 上下文
|
|
211
|
+
if (span) {
|
|
212
|
+
tracer?.endSpan(span);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* 执行两阶段 session compact
|
|
219
|
+
*
|
|
220
|
+
* 阶段1: 生成场景化压缩提示 (generateCompactHint)
|
|
221
|
+
* 阶段2: 使用提示进行压缩 (compact with scenarioHint)
|
|
222
|
+
*/
|
|
223
|
+
@TracedAs("context-handler.compactSessionWithTwoPhases", { recordParams: true, recordResult: true })
|
|
224
|
+
async compactSessionWithTwoPhases(
|
|
225
|
+
sessionId?: string,
|
|
226
|
+
originalQuery?: string
|
|
227
|
+
): Promise<CompactResult> {
|
|
228
|
+
if (!sessionId) {
|
|
229
|
+
return { success: false, error: "No session ID provided" };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
// 发布压缩开始事件
|
|
234
|
+
this.env.pushEnvEvent({
|
|
235
|
+
type: "context.compacting",
|
|
236
|
+
metadata: { sessionId },
|
|
237
|
+
payload: { phase: "two-phase" },
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// ========================================
|
|
241
|
+
// 阶段1: 生成场景化压缩提示
|
|
242
|
+
// ========================================
|
|
243
|
+
let scenarioHint = "";
|
|
244
|
+
try {
|
|
245
|
+
scenarioHint = await this.sessionComponent.generateCompactHint(sessionId);
|
|
246
|
+
logger.info("[Phase 1] Compact hint generated", {
|
|
247
|
+
sessionId,
|
|
248
|
+
hintLength: scenarioHint.length,
|
|
249
|
+
hintPreview: scenarioHint.substring(0, 100),
|
|
250
|
+
});
|
|
251
|
+
} catch (hintError) {
|
|
252
|
+
// 如果生成提示失败,继续使用空提示(不影响主流程)
|
|
253
|
+
logger.warn("[Phase 1] Hint generation failed, continuing with empty hint", {
|
|
254
|
+
error: hintError instanceof Error ? hintError.message : String(hintError),
|
|
255
|
+
});
|
|
256
|
+
scenarioHint = "";
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// 发布提示生成完成事件
|
|
260
|
+
this.env.pushEnvEvent({
|
|
261
|
+
type: "context.hint_generated",
|
|
262
|
+
metadata: { sessionId },
|
|
263
|
+
payload: {
|
|
264
|
+
hintLength: scenarioHint.length,
|
|
265
|
+
hintPreview: scenarioHint.substring(0, 200),
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// ========================================
|
|
270
|
+
// 阶段2: 执行实际压缩
|
|
271
|
+
// ========================================
|
|
272
|
+
const result = await this.sessionComponent.compact(sessionId, {
|
|
273
|
+
summary: originalQuery
|
|
274
|
+
? `Auto-compacted. Original query: ${originalQuery.substring(0, 200)}${originalQuery.length > 200 ? "..." : ""}`
|
|
275
|
+
: "Auto-compacted due to context threshold",
|
|
276
|
+
scenarioHint, // 传入场景化提示
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// 发布压缩完成事件
|
|
280
|
+
this.env.pushEnvEvent({
|
|
281
|
+
type: "context.compacted",
|
|
282
|
+
metadata: { sessionId },
|
|
283
|
+
payload: {
|
|
284
|
+
checkpointId: result.checkpoint.id,
|
|
285
|
+
messagesCompacted: result.deletedMessageCount,
|
|
286
|
+
scenarioHintUsed: !!scenarioHint,
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// 构造新 query
|
|
291
|
+
const newQuery = this.constructQueryWithCheckpoint(
|
|
292
|
+
result.checkpoint,
|
|
293
|
+
originalQuery
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
logger.info("[Phase 2] Compact completed", {
|
|
297
|
+
checkpointId: result.checkpoint.id,
|
|
298
|
+
deletedMessageCount: result.deletedMessageCount,
|
|
299
|
+
scenarioHintUsed: !!scenarioHint,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
success: true,
|
|
304
|
+
checkpoint: result.checkpoint,
|
|
305
|
+
newQuery,
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
} catch (error) {
|
|
309
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
310
|
+
logger.error("Two-phase compact failed", { error: errorMessage });
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
success: false,
|
|
314
|
+
error: errorMessage,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* 执行 session compact(兼容旧接口,内部调用两阶段版本)
|
|
321
|
+
*/
|
|
322
|
+
async compactSession(
|
|
323
|
+
sessionId?: string,
|
|
324
|
+
originalQuery?: string
|
|
325
|
+
): Promise<CompactResult> {
|
|
326
|
+
return this.compactSessionWithTwoPhases(sessionId, originalQuery);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* 用 checkpoint 构造新 query
|
|
331
|
+
*/
|
|
332
|
+
@TracedAs("context-handler.constructQuery")
|
|
333
|
+
private constructQueryWithCheckpoint(
|
|
334
|
+
checkpoint: SessionCheckpoint,
|
|
335
|
+
originalQuery?: string
|
|
336
|
+
): string {
|
|
337
|
+
const parts: string[] = [];
|
|
338
|
+
|
|
339
|
+
parts.push("【会话历史摘要】");
|
|
340
|
+
parts.push(checkpoint.summary);
|
|
341
|
+
|
|
342
|
+
if (checkpoint.processKeyPoints.length > 0) {
|
|
343
|
+
parts.push("");
|
|
344
|
+
parts.push("【已完成的工作】");
|
|
345
|
+
checkpoint.processKeyPoints.forEach((point, i) => {
|
|
346
|
+
parts.push(`${i + 1}. ${point}`);
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (checkpoint.currentState) {
|
|
351
|
+
parts.push("");
|
|
352
|
+
parts.push("【当前状态】");
|
|
353
|
+
parts.push(checkpoint.currentState);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (checkpoint.nextSteps.length > 0) {
|
|
357
|
+
parts.push("");
|
|
358
|
+
parts.push("【待完成的任务】");
|
|
359
|
+
checkpoint.nextSteps.forEach((step, i) => {
|
|
360
|
+
parts.push(`${i + 1}. ${step}`);
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (originalQuery) {
|
|
365
|
+
parts.push("");
|
|
366
|
+
parts.push("【用户新输入】");
|
|
367
|
+
parts.push(originalQuery);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return parts.join("\n");
|
|
371
|
+
}
|
|
372
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import * as fs from "fs/promises";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import * as os from "os";
|
|
5
|
+
import { EnvironmentService } from "./environment.service";
|
|
6
|
+
import { OutputService } from "./output.service";
|
|
7
|
+
|
|
8
|
+
// 测试命令目录常量(避免污染真实的 ~/.roy-agent/commands)
|
|
9
|
+
const TEST_COMMANDS_DIR = path.join(os.homedir(), ".roy-agent", "commands");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Commands Prompt Integration Test
|
|
13
|
+
*
|
|
14
|
+
* 验证 EnvironmentService 在创建环境时,
|
|
15
|
+
* 是否正确注册 commands-prompt Hook,
|
|
16
|
+
* 使得 default prompt 能够动态注入用户命令信息。
|
|
17
|
+
*
|
|
18
|
+
* 注意:这些测试会创建真实的命令在 TEST_COMMANDS_DIR,
|
|
19
|
+
* 会在 afterEach 中清理测试命令。
|
|
20
|
+
*/
|
|
21
|
+
describe("EnvironmentService Commands Prompt Integration", () => {
|
|
22
|
+
let tempDir: string;
|
|
23
|
+
let configPath: string;
|
|
24
|
+
let outputService: OutputService;
|
|
25
|
+
let testCmdName: string;
|
|
26
|
+
|
|
27
|
+
beforeEach(async () => {
|
|
28
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "roy-env-commands-test-"));
|
|
29
|
+
configPath = path.join(tempDir, "config.jsonc");
|
|
30
|
+
|
|
31
|
+
// 创建空配置文件,使用测试命令目录
|
|
32
|
+
await fs.writeFile(configPath, JSON.stringify({
|
|
33
|
+
"command.userCommandsDir": TEST_COMMANDS_DIR,
|
|
34
|
+
"command.projectCommandsDir": path.join(tempDir, "project-commands"),
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
// 确保测试命令目录存在
|
|
38
|
+
await fs.mkdir(TEST_COMMANDS_DIR, { recursive: true });
|
|
39
|
+
|
|
40
|
+
// 使用真实的 OutputService(支持 quiet 模式)
|
|
41
|
+
outputService = new OutputService({ quiet: true });
|
|
42
|
+
|
|
43
|
+
// 使用唯一命令名避免冲突
|
|
44
|
+
testCmdName = `test-cmd-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(async () => {
|
|
48
|
+
// 清理测试创建的命令(只清理测试命令,不清理真实命令如 lark-cli)
|
|
49
|
+
try {
|
|
50
|
+
const entries = await fs.readdir(TEST_COMMANDS_DIR);
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
// 只清理以 test-cmd 开头的测试命令
|
|
53
|
+
if (entry.startsWith("test-cmd")) {
|
|
54
|
+
await fs.rm(path.join(TEST_COMMANDS_DIR, entry), { recursive: true, force: true });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// 忽略清理错误
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 清理临时目录
|
|
62
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("registerCommandsPromptHook", () => {
|
|
66
|
+
it("should inject commands into default prompt via hook", async () => {
|
|
67
|
+
// 1. 创建环境
|
|
68
|
+
const envService = new EnvironmentService(outputService);
|
|
69
|
+
|
|
70
|
+
await envService.create({
|
|
71
|
+
configPath,
|
|
72
|
+
sessionStorage: { type: "memory" }, // 使用内存存储,避免污染正式数据库
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
// 2. 获取 CommandsComponent 并添加命令
|
|
77
|
+
const commandsComponent = envService.getCommands();
|
|
78
|
+
expect(commandsComponent).toBeDefined();
|
|
79
|
+
|
|
80
|
+
await commandsComponent!.add({
|
|
81
|
+
name: testCmdName,
|
|
82
|
+
target: "/usr/bin/echo",
|
|
83
|
+
description: "Test command",
|
|
84
|
+
tips: "Use with args",
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// 3. 验证命令元信息存在
|
|
88
|
+
const metaList = await commandsComponent!.getCommandsMeta();
|
|
89
|
+
const testCmdMeta = metaList.find(m => m.name === testCmdName);
|
|
90
|
+
expect(testCmdMeta).toBeDefined();
|
|
91
|
+
expect(testCmdMeta?.description).toBe("Test command");
|
|
92
|
+
|
|
93
|
+
// 4. 验证 renderCommandsSection 方法存在
|
|
94
|
+
const envServiceAny = envService as any;
|
|
95
|
+
expect(typeof envServiceAny.renderCommandsSection).toBe("function");
|
|
96
|
+
|
|
97
|
+
// 5. 验证 commands section 渲染正确
|
|
98
|
+
const section = envServiceAny.renderCommandsSection(metaList);
|
|
99
|
+
expect(section).toContain("Your Commands Arsenal");
|
|
100
|
+
expect(section).toContain(`| \`${testCmdName}\` | Test command |`);
|
|
101
|
+
expect(section).toContain("Run `<command> --help`");
|
|
102
|
+
|
|
103
|
+
} finally {
|
|
104
|
+
await envService.dispose();
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should render commands section correctly", async () => {
|
|
109
|
+
// 验证 renderCommandsSection 逻辑
|
|
110
|
+
const metaList = [
|
|
111
|
+
{ name: "git", description: "Git version control", tips: "Use --help" },
|
|
112
|
+
{ name: "npm", description: "Node package manager", tips: "Use --help" },
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
const envService = new EnvironmentService(outputService);
|
|
116
|
+
|
|
117
|
+
// 通过反射获取私有方法(仅用于测试)
|
|
118
|
+
const renderCommandsSection = (envService as any).renderCommandsSection?.bind(envService);
|
|
119
|
+
|
|
120
|
+
if (renderCommandsSection) {
|
|
121
|
+
const section = renderCommandsSection(metaList);
|
|
122
|
+
|
|
123
|
+
expect(section).toContain("Your Commands Arsenal");
|
|
124
|
+
expect(section).toContain("| `git` | Git version control |");
|
|
125
|
+
expect(section).toContain("| `npm` | Node package manager |");
|
|
126
|
+
expect(section).toContain("Run `<command> --help`");
|
|
127
|
+
|
|
128
|
+
// 验证按字母顺序排列
|
|
129
|
+
const gitIndex = section.indexOf("| `git`");
|
|
130
|
+
const npmIndex = section.indexOf("| `npm`");
|
|
131
|
+
expect(gitIndex).toBeLessThan(npmIndex);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("should handle empty command list", async () => {
|
|
136
|
+
const envService = new EnvironmentService(outputService);
|
|
137
|
+
|
|
138
|
+
// 通过反射获取私有方法
|
|
139
|
+
const renderCommandsSection = (envService as any).renderCommandsSection?.bind(envService);
|
|
140
|
+
|
|
141
|
+
if (renderCommandsSection) {
|
|
142
|
+
const section = renderCommandsSection([]);
|
|
143
|
+
|
|
144
|
+
// 空命令列表应该只返回标题(无表格行)
|
|
145
|
+
expect(section).toContain("Your Commands Arsenal");
|
|
146
|
+
expect(section).not.toContain("| `");
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("should register commands-prompt hook on environment creation", async () => {
|
|
151
|
+
const envService = new EnvironmentService(outputService);
|
|
152
|
+
|
|
153
|
+
await envService.create({
|
|
154
|
+
configPath,
|
|
155
|
+
sessionStorage: { type: "memory" }, // 使用内存存储,避免污染正式数据库
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
// 验证 Hook 已注册(通过日志输出确认)
|
|
160
|
+
const promptComponent = envService.getEnvironment()?.getComponent("prompt");
|
|
161
|
+
expect(promptComponent).toBeDefined();
|
|
162
|
+
} finally {
|
|
163
|
+
await envService.dispose();
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
});
|