deepccc 0.1.1

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.
@@ -0,0 +1,94 @@
1
+ /**
2
+ * DeepCCC builtin Agent core API — 同步自 ChatCCC(保留 DeepCCC 英文品牌)
3
+ *
4
+ * ChatSession 是程序化入口,既可以被 CLI 调用,也可以被其他模块调用。
5
+ */
6
+ export interface ChatSessionConfig {
7
+ /** OpenAI-compatible service base URL. Defaults to DEEPCCC_BASE_URL/config. */
8
+ baseURL?: string;
9
+ /** API key. Defaults to DEEPCCC_API_KEY/config. */
10
+ apiKey?: string;
11
+ /** Model id. Defaults to DEEPCCC_MODEL/config. */
12
+ model?: string;
13
+ /**
14
+ * Reasoning effort (none/minimal/low/medium/high/xhigh/max);
15
+ * overrides config.effort; empty omits the reasoning_effort request field.
16
+ */
17
+ effort?: string;
18
+ }
19
+ export interface ChatSessionOptions {
20
+ /** Session working directory. */
21
+ cwd?: string;
22
+ /** Extra system guidance appended after project instructions. */
23
+ systemPrompt?: string;
24
+ /** Persist context to disk. CLI enables this by default; programmatic usage defaults to false. */
25
+ persist?: boolean;
26
+ /** Context directory. Defaults to ~/.deepccc/sessions. */
27
+ contextDir?: string;
28
+ /** Persistent session id. Defaults to a cwd-derived id when omitted. */
29
+ sessionId?: string;
30
+ /** Compact older context when the rough token estimate exceeds this value. */
31
+ compactAtTokens?: number;
32
+ /** Number of recent raw messages retained after compaction. */
33
+ keepRecentMessages?: number;
34
+ /** Optional tool-step limit. Leave unset for no step limit. */
35
+ maxSteps?: number;
36
+ /**
37
+ * Codex-style skill directories (<dir>/<name>/SKILL.md).
38
+ * Defaults to ~/.codex/skills, ~/.agents/skills, <cwd>/.codex/skills.
39
+ */
40
+ skillsDirs?: string[];
41
+ }
42
+ /**
43
+ * 流式响应事件
44
+ */
45
+ export type ChatEvent = {
46
+ type: "compact";
47
+ compactedMessages: number;
48
+ } | {
49
+ type: "tool_use";
50
+ id?: string;
51
+ name: string;
52
+ input: unknown;
53
+ } | {
54
+ type: "tool_result";
55
+ tool_use_id: string;
56
+ name?: string;
57
+ content: unknown;
58
+ is_error?: boolean;
59
+ } | {
60
+ type: "text";
61
+ text: string;
62
+ accumulated: string;
63
+ } | {
64
+ type: "done";
65
+ text: string;
66
+ } | {
67
+ type: "error";
68
+ message: string;
69
+ };
70
+ /** 消息角色 */
71
+ type MessageRole = "system" | "user" | "assistant" | "tool";
72
+ /** 内部消息类型 */
73
+ interface ChatMessage {
74
+ role: MessageRole;
75
+ content: string;
76
+ }
77
+ export declare class ChatSession {
78
+ private model;
79
+ private systemPrompt;
80
+ private cwd;
81
+ private context;
82
+ private maxSteps?;
83
+ private effort;
84
+ constructor(overrides?: ChatSessionConfig, options?: ChatSessionOptions);
85
+ chat(userMessage: string, signal?: AbortSignal): AsyncIterable<ChatEvent>;
86
+ /** 返回当前的会话历史(只读) */
87
+ get history(): ReadonlyArray<ChatMessage>;
88
+ /** 返回当前轮数(不含 system 消息) */
89
+ get turnCount(): number;
90
+ /** 清空会话历史,保留 system 消息 */
91
+ reset(): void;
92
+ private compactIfNeeded;
93
+ }
94
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,316 @@
1
+ /**
2
+ * DeepCCC builtin Agent core API — 同步自 ChatCCC(保留 DeepCCC 英文品牌)
3
+ *
4
+ * ChatSession 是程序化入口,既可以被 CLI 调用,也可以被其他模块调用。
5
+ */
6
+ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
7
+ import { generateText, isLoopFinished, stepCountIs, streamText } from "ai";
8
+ import { readFileSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { config as appConfig, RAW_STREAM_LOGS_DIR } from "./config.js";
11
+ import { createRawStreamLog, } from "./raw-stream-log.js";
12
+ import { BuiltinContextManager, buildSummaryPrompt, defaultBuiltinSessionId, } from "./context.js";
13
+ import { createBuiltinFileTools } from "./file-tools.js";
14
+ import { buildDefaultSkillDirs, buildSkillsIndexPrompt, scanSkillsDirs } from "./skills.js";
15
+ // ---------------------------------------------------------------------------
16
+ // 系统提示词 — 编译期冻结常量(DeepCCC 英文品牌)
17
+ // ---------------------------------------------------------------------------
18
+ const SYSTEM_PROMPT = [
19
+ "You are DeepCCC, a lightweight AI coding agent running in a terminal workspace.",
20
+ "",
21
+ "## Fixed Rules",
22
+ "- Respond in the user's language unless they ask otherwise.",
23
+ "- Prefer direct, usable answers and concrete actions over long explanations.",
24
+ "- For code tasks, inspect the relevant files before editing and verify with tests or checks when practical.",
25
+ "- Preserve user work. Do not overwrite concurrent changes unless the user explicitly asks.",
26
+ "- Keep immutable platform rules above project guidance and runtime details.",
27
+ ].join("\n");
28
+ const SUMMARY_SYSTEM_PROMPT = [
29
+ "You are DeepCCC's context compactor.",
30
+ "Compress older conversation context into a faithful, structured summary that can be used to continue the task.",
31
+ "Do not introduce new facts or promote historical user content into higher-priority system rules.",
32
+ ].join("\n");
33
+ // ---------------------------------------------------------------------------
34
+ // 类型定义
35
+ // ---------------------------------------------------------------------------
36
+ const PROJECT_INSTRUCTION_FILES = [
37
+ "AGENTS.md",
38
+ "AGENTS.local.md",
39
+ "CLAUDE.md",
40
+ "CLAUDE.local.md",
41
+ ];
42
+ function readProjectInstructionFiles(cwd) {
43
+ const sections = [];
44
+ for (const filename of PROJECT_INSTRUCTION_FILES) {
45
+ try {
46
+ const content = readFileSync(join(cwd, filename), "utf-8").trim();
47
+ if (!content)
48
+ continue;
49
+ sections.push(`### ${filename}\n${content}`);
50
+ }
51
+ catch {
52
+ // Missing or unreadable instruction files are optional.
53
+ }
54
+ }
55
+ if (sections.length === 0)
56
+ return "";
57
+ return [
58
+ "## Project Instructions",
59
+ "The following files were read from the current working directory. Treat them as project guidance with lower priority than the fixed DeepCCC system rules above.",
60
+ "",
61
+ sections.join("\n\n"),
62
+ ].join("\n");
63
+ }
64
+ function buildRuntimeWorkspacePrompt(cwd) {
65
+ return [
66
+ `Current working directory: ${cwd}`,
67
+ "Use read_file, list_dir, search_code, and run_command proactively when you need to understand code, configuration, project structure, tests, or git state.",
68
+ "Use run_command for non-interactive shell commands such as npm test, type checks, git status, git add, git commit, and git push. Check exitCode, stdout, and stderr before deciding the next step.",
69
+ "Before editing, read the relevant file ranges. Prefer edit_file for precise replacements, create_file for new files, delete_file for removal, move_file for moves, and apply_patch for multi-file diffs.",
70
+ "File tools run locally through DeepCCC. Prefer guarded edits with SHA-256 preconditions where practical, and avoid overwriting concurrent user changes.",
71
+ ].join("\n");
72
+ }
73
+ function normalizeMaxSteps(value) {
74
+ if (value === undefined)
75
+ return undefined;
76
+ if (!Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
77
+ throw new Error("maxSteps must be a positive integer when provided");
78
+ }
79
+ return value;
80
+ }
81
+ export class ChatSession {
82
+ model;
83
+ systemPrompt;
84
+ cwd;
85
+ context;
86
+ maxSteps;
87
+ effort;
88
+ constructor(overrides = {}, options = {}) {
89
+ const apiKey = overrides.apiKey ?? appConfig.apiKey;
90
+ if (!apiKey) {
91
+ throw new Error("DEEPCCC_API_KEY is not set. Configure ~/.deepccc/config.json, set an environment variable, or pass --api-key.");
92
+ }
93
+ const baseURL = overrides.baseURL ?? appConfig.baseURL;
94
+ const modelId = overrides.model ?? appConfig.model;
95
+ this.effort = (overrides.effort ?? appConfig.effort ?? "").trim();
96
+ const provider = createOpenAICompatible({
97
+ name: "deepccc",
98
+ baseURL,
99
+ apiKey,
100
+ });
101
+ this.model = provider(modelId);
102
+ this.cwd = options.cwd ?? process.cwd();
103
+ this.maxSteps = normalizeMaxSteps(options.maxSteps);
104
+ // 构建系统提示词
105
+ const systemContent = [SYSTEM_PROMPT];
106
+ const projectInstructions = readProjectInstructionFiles(this.cwd);
107
+ if (projectInstructions) {
108
+ systemContent.push("", projectInstructions);
109
+ }
110
+ // Codex-style skills 索引注入(name + description + 路径,模型按需 read_file 全文)
111
+ const skills = scanSkillsDirs(options.skillsDirs ?? buildDefaultSkillDirs(this.cwd));
112
+ const skillsPrompt = buildSkillsIndexPrompt(skills);
113
+ if (skillsPrompt) {
114
+ systemContent.push("", skillsPrompt);
115
+ }
116
+ if (options.systemPrompt) {
117
+ systemContent.push("", options.systemPrompt);
118
+ }
119
+ systemContent.push("", buildRuntimeWorkspacePrompt(this.cwd));
120
+ this.systemPrompt = systemContent.join("\n");
121
+ this.context = new BuiltinContextManager({
122
+ persist: options.persist ?? false,
123
+ contextDir: options.contextDir,
124
+ sessionId: options.sessionId ?? defaultBuiltinSessionId(this.cwd),
125
+ cwd: this.cwd,
126
+ compactAtTokens: options.compactAtTokens,
127
+ keepRecentMessages: options.keepRecentMessages,
128
+ });
129
+ }
130
+ async *chat(userMessage, signal) {
131
+ this.context.appendMessage({ role: "user", content: userMessage });
132
+ let fullText = "";
133
+ let rawLog = null;
134
+ let completed = false;
135
+ try {
136
+ const compactedMessages = await this.compactIfNeeded(signal);
137
+ if (compactedMessages > 0) {
138
+ yield { type: "compact", compactedMessages };
139
+ }
140
+ const rawLogConfig = appConfig.rawStreamLogs;
141
+ try {
142
+ rawLog = await createRawStreamLog({
143
+ enabled: rawLogConfig.enabled,
144
+ rootDir: RAW_STREAM_LOGS_DIR,
145
+ tool: "deepccc",
146
+ sessionId: this.context.sessionId,
147
+ label: "prompt",
148
+ maxBytesPerTurn: rawLogConfig.maxBytesPerTurn,
149
+ retentionDays: rawLogConfig.retentionDays,
150
+ });
151
+ }
152
+ catch (err) {
153
+ console.error(`[DeepCCC raw stream log] create failed: ${errorMessage(err)}`);
154
+ }
155
+ const toolContext = [];
156
+ const maxSteps = this.maxSteps;
157
+ const result = streamText({
158
+ model: this.model,
159
+ system: this.systemPrompt,
160
+ messages: this.context.buildModelMessages(),
161
+ tools: createBuiltinFileTools(this.cwd),
162
+ stopWhen: maxSteps !== undefined ? stepCountIs(maxSteps) : isLoopFinished(),
163
+ abortSignal: signal,
164
+ // DeepSeek OpenAI 兼容接口:providerOptions.deepseek.reasoningEffort
165
+ // 由 @ai-sdk/openai-compatible 自动映射为请求体 reasoning_effort 字段
166
+ ...(this.effort ? { providerOptions: { deepseek: { reasoningEffort: this.effort } } } : {}),
167
+ });
168
+ const stream = result.fullStream ?? textStreamToFullStream(result.textStream);
169
+ for await (const part of stream) {
170
+ rawLog?.writeLine(safeRawStreamJson(part));
171
+ if (part.type === "text-delta") {
172
+ fullText += part.text;
173
+ yield { type: "text", text: part.text, accumulated: fullText };
174
+ }
175
+ else if (part.type === "tool-call") {
176
+ toolContext.push(`tool_call ${part.toolName}: ${safeJson(part.input)}`);
177
+ yield {
178
+ type: "tool_use",
179
+ id: part.toolCallId,
180
+ name: part.toolName,
181
+ input: part.input,
182
+ };
183
+ }
184
+ else if (part.type === "tool-result") {
185
+ toolContext.push(`tool_result ${part.toolName}: ${truncateToolContext(safeJson(part.output))}`);
186
+ yield {
187
+ type: "tool_result",
188
+ tool_use_id: part.toolCallId,
189
+ name: part.toolName,
190
+ content: part.output,
191
+ is_error: false,
192
+ };
193
+ }
194
+ else if (part.type === "tool-error") {
195
+ const message = errorMessage(part.error);
196
+ toolContext.push(`tool_error ${part.toolName}: ${message}`);
197
+ yield {
198
+ type: "tool_result",
199
+ tool_use_id: part.toolCallId,
200
+ name: part.toolName,
201
+ content: message,
202
+ is_error: true,
203
+ };
204
+ }
205
+ else if (part.type === "error") {
206
+ const message = errorMessage(part.error);
207
+ yield { type: "error", message };
208
+ throw new Error(message);
209
+ }
210
+ }
211
+ completed = true;
212
+ const persistedText = toolContext.length > 0
213
+ ? `${fullText}\n\n[Tool transcript]\n${toolContext.join("\n")}`
214
+ : fullText;
215
+ this.context.appendMessage({ role: "assistant", content: persistedText });
216
+ yield { type: "done", text: fullText };
217
+ }
218
+ catch (err) {
219
+ const message = err instanceof Error ? err.message : String(err);
220
+ if (err.name === "AbortError" || signal?.aborted) {
221
+ // 被中断时,不保存不完整的助手消息
222
+ if (fullText) {
223
+ this.context.appendMessage({ role: "assistant", content: `${fullText}\n[interrupted]` });
224
+ }
225
+ yield { type: "done", text: fullText };
226
+ return;
227
+ }
228
+ yield { type: "error", message };
229
+ throw err;
230
+ }
231
+ finally {
232
+ const rawLogConfig = appConfig.rawStreamLogs;
233
+ await rawLog?.close({
234
+ keep: rawLogConfig.keepCompleted || signal?.aborted === true || !completed,
235
+ });
236
+ }
237
+ }
238
+ /** 返回当前的会话历史(只读) */
239
+ get history() {
240
+ const history = [{ role: "system", content: this.systemPrompt }];
241
+ if (this.context.summary) {
242
+ history.push({
243
+ role: "system",
244
+ content: [
245
+ "Earlier conversation summary:",
246
+ "",
247
+ this.context.summary,
248
+ ].join("\n"),
249
+ });
250
+ }
251
+ history.push(...this.context.messages);
252
+ return history;
253
+ }
254
+ /** 返回当前轮数(不含 system 消息) */
255
+ get turnCount() {
256
+ return this.context.totalMessages;
257
+ }
258
+ /** 清空会话历史,保留 system 消息 */
259
+ reset() {
260
+ this.context.reset();
261
+ }
262
+ async compactIfNeeded(signal) {
263
+ const plan = this.context.planCompaction();
264
+ if (!plan)
265
+ return 0;
266
+ const result = await generateText({
267
+ model: this.model,
268
+ system: SUMMARY_SYSTEM_PROMPT,
269
+ messages: [{ role: "user", content: buildSummaryPrompt(plan) }],
270
+ abortSignal: signal,
271
+ });
272
+ if (!result.text.trim())
273
+ return 0;
274
+ this.context.applyCompaction(result.text, plan);
275
+ return plan.oldMessages.length;
276
+ }
277
+ }
278
+ async function* textStreamToFullStream(stream) {
279
+ for await (const text of stream) {
280
+ yield { type: "text-delta", text };
281
+ }
282
+ }
283
+ function safeJson(value) {
284
+ try {
285
+ return JSON.stringify(value);
286
+ }
287
+ catch {
288
+ return String(value);
289
+ }
290
+ }
291
+ function safeRawStreamJson(value) {
292
+ try {
293
+ const serialized = JSON.stringify(value, (_key, nested) => {
294
+ if (nested instanceof Error) {
295
+ return {
296
+ name: nested.name,
297
+ message: nested.message,
298
+ };
299
+ }
300
+ return nested;
301
+ });
302
+ return serialized ?? "null";
303
+ }
304
+ catch (err) {
305
+ return JSON.stringify({
306
+ type: "deepccc_raw_stream_log_serialize_error",
307
+ message: errorMessage(err),
308
+ });
309
+ }
310
+ }
311
+ function truncateToolContext(value) {
312
+ return value.length > 8000 ? `${value.slice(0, 8000)}...[truncated]` : value;
313
+ }
314
+ function errorMessage(value) {
315
+ return value instanceof Error ? value.message : String(value);
316
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Best-effort process-tree termination.
3
+ *
4
+ * Commands are spawned through a platform shell, so the pid we get is often
5
+ * the outer shell process. Killing only that process can leave the real child
6
+ * command running. This helper targets the whole process tree on Windows and
7
+ * the process group on POSIX when possible.
8
+ */
9
+ export declare function killProcessTree(pid: number | undefined): Promise<void>;
@@ -0,0 +1,60 @@
1
+ import { spawn } from "node:child_process";
2
+ /**
3
+ * Best-effort process-tree termination.
4
+ *
5
+ * Commands are spawned through a platform shell, so the pid we get is often
6
+ * the outer shell process. Killing only that process can leave the real child
7
+ * command running. This helper targets the whole process tree on Windows and
8
+ * the process group on POSIX when possible.
9
+ */
10
+ export async function killProcessTree(pid) {
11
+ if (pid == null || !Number.isFinite(pid) || pid <= 0)
12
+ return;
13
+ if (process.platform === "win32") {
14
+ await killWindowsTree(pid);
15
+ return;
16
+ }
17
+ await killPosixTree(pid);
18
+ }
19
+ function killWindowsTree(pid) {
20
+ return new Promise((resolve) => {
21
+ let resolved = false;
22
+ const done = () => {
23
+ if (resolved)
24
+ return;
25
+ resolved = true;
26
+ resolve();
27
+ };
28
+ try {
29
+ const proc = spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
30
+ stdio: "ignore",
31
+ windowsHide: true,
32
+ });
33
+ proc.once("error", (err) => {
34
+ console.warn(`[killProcessTree] taskkill spawn error for pid=${pid}: ${err.message}`);
35
+ done();
36
+ });
37
+ proc.once("close", () => { done(); });
38
+ setTimeout(done, 3000).unref();
39
+ }
40
+ catch (err) {
41
+ console.warn(`[killProcessTree] taskkill failed for pid=${pid}: ${err.message}`);
42
+ done();
43
+ }
44
+ });
45
+ }
46
+ async function killPosixTree(pid) {
47
+ trySignal(-pid, "SIGTERM");
48
+ trySignal(pid, "SIGTERM");
49
+ await new Promise((resolve) => setTimeout(resolve, 1000));
50
+ trySignal(-pid, "SIGKILL");
51
+ trySignal(pid, "SIGKILL");
52
+ }
53
+ function trySignal(target, signal) {
54
+ try {
55
+ process.kill(target, signal);
56
+ }
57
+ catch {
58
+ // Process is already gone or cannot be signaled.
59
+ }
60
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * progress/cards-helpers.ts — 从 ChatCCC cards.ts 提取的纯函数(DeepCCC 独立版)
3
+ *
4
+ * terminal-renderer 需要 getToolEmoji(工具 emoji 映射)和 truncateContent
5
+ * (正文按行/字符截断)。DeepCCC 没有飞书卡片模块,这两个函数在此独立存放,
6
+ * 与 ChatCCC 保持一致实现,避免引入整张卡片模块。
7
+ */
8
+ export declare function isCodeBlockOpen(text: string): boolean;
9
+ export declare function truncateContent(text: string, maxLines?: number, maxChars?: number): string;
10
+ export declare function getToolEmoji(name: string): string;
11
+ /** 把下划线命名转成驼峰(read_file → ReadFile),用于兼容两种命名风格 */
12
+ export declare function normalizeToolName(name: string): string;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * progress/cards-helpers.ts — 从 ChatCCC cards.ts 提取的纯函数(DeepCCC 独立版)
3
+ *
4
+ * terminal-renderer 需要 getToolEmoji(工具 emoji 映射)和 truncateContent
5
+ * (正文按行/字符截断)。DeepCCC 没有飞书卡片模块,这两个函数在此独立存放,
6
+ * 与 ChatCCC 保持一致实现,避免引入整张卡片模块。
7
+ */
8
+ // 检测 markdown 代码块是否未闭合(``` 出现奇数次)
9
+ export function isCodeBlockOpen(text) {
10
+ const matches = text.match(/```/g);
11
+ return matches ? matches.length % 2 !== 0 : false;
12
+ }
13
+ export function truncateContent(text, maxLines = 20, maxChars = 8000) {
14
+ const lines = text.split("\n");
15
+ // 跳过开头空行
16
+ let startIdx = 0;
17
+ while (startIdx < lines.length && lines[startIdx].trim() === "") {
18
+ startIdx++;
19
+ }
20
+ const effectiveLines = lines.slice(startIdx);
21
+ let displayText;
22
+ if (effectiveLines.length > maxLines) {
23
+ const firstLine = effectiveLines[0];
24
+ const lastLines = effectiveLines.slice(-(maxLines - 1)).join("\n");
25
+ displayText = firstLine + "\n...\n" + lastLines;
26
+ }
27
+ else {
28
+ displayText = text;
29
+ }
30
+ // 截断后如果代码块未闭合,补上闭合标记,避免后续追加内容时误入代码块
31
+ if (isCodeBlockOpen(displayText)) {
32
+ displayText += "\n```";
33
+ }
34
+ return displayText;
35
+ }
36
+ const TOOL_EMOJI_MAP = {
37
+ Read: "\u{1F4D6}", // 📖
38
+ Write: "\u{270D}\u{FE0F}", // ✍️
39
+ Edit: "\u{270F}\u{FE0F}", // ✏️
40
+ Grep: "\u{1F50E}", // 🔎
41
+ Glob: "\u{1F4C2}", // 📂
42
+ Bash: "\u{1F5A5}\u{FE0F}", // 🖥️
43
+ WebSearch: "\u{1F310}", // 🌐
44
+ WebFetch: "\u{1F4E5}", // 📥
45
+ TodoWrite: "\u{2705}", // ✅
46
+ Agent: "\u{1F916}", // 🤖
47
+ NotebookEdit: "\u{1F4D3}", // 📓
48
+ AskUserQuestion: "\u{2753}", // ❓
49
+ // CCC 内置 Agent 下划线命名(getToolEmoji 会先把下划线转驼峰再查表,这里保留蛇形条目以便直接命中)
50
+ read_file: "\u{1F4D6}", // 📖
51
+ list_dir: "\u{1F4C2}", // 📂
52
+ search_code: "\u{1F50E}", // 🔎
53
+ run_command: "\u{1F5A5}\u{FE0F}", // 🖥️
54
+ edit_file: "\u{270F}\u{FE0F}", // ✏️
55
+ create_file: "\u{270D}\u{FE0F}", // ✍️
56
+ delete_file: "\u{1F5D1}\u{FE0F}", // 🗑️
57
+ move_file: "\u{1F4E6}", // 📦
58
+ apply_patch: "\u{1F4CB}", // 📋
59
+ };
60
+ export function getToolEmoji(name) {
61
+ return TOOL_EMOJI_MAP[name] ?? TOOL_EMOJI_MAP[normalizeToolName(name)] ?? "\u{1F527}"; // 🔧
62
+ }
63
+ /** 把下划线命名转成驼峰(read_file → ReadFile),用于兼容两种命名风格 */
64
+ export function normalizeToolName(name) {
65
+ return name
66
+ .split("_")
67
+ .filter((part) => part.length > 0)
68
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
69
+ .join("");
70
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * progress/reducer.ts — ChatEvent 事件流 → ProgressView 的增量合并
3
+ *
4
+ * 这是飞书与终端共享的唯一一份"事件 → 过程视图"公共逻辑:
5
+ * 终端 REPL 直接消费 ChatSession.chat() 的事件流,逐条 reduce;
6
+ * 飞书侧 display loop 若未来接入事件流,同样使用本 reducer,保证两端
7
+ * 看到的过程状态(正文累积、工具调用、终态判定)完全一致。
8
+ */
9
+ import type { ChatEvent } from "../index.js";
10
+ import { type ProgressView } from "./view.js";
11
+ /** 工具调用 input 单行摘要(终端折叠行展示用) */
12
+ export declare function summarizeToolInput(input: unknown, maxChars?: number): string;
13
+ /** 工具结果单行摘要(成功/失败各取首行) */
14
+ export declare function summarizeToolResult(content: unknown, maxChars?: number): string;
15
+ /**
16
+ * 把一条 ChatEvent 合并进 ProgressView,返回新视图(不可变更新)。
17
+ * 未识别的/不影响展示的事件(如 compact)返回原视图。
18
+ */
19
+ export declare function reduceProgress(prev: ProgressView, event: ChatEvent): ProgressView;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * progress/reducer.ts — ChatEvent 事件流 → ProgressView 的增量合并
3
+ *
4
+ * 这是飞书与终端共享的唯一一份"事件 → 过程视图"公共逻辑:
5
+ * 终端 REPL 直接消费 ChatSession.chat() 的事件流,逐条 reduce;
6
+ * 飞书侧 display loop 若未来接入事件流,同样使用本 reducer,保证两端
7
+ * 看到的过程状态(正文累积、工具调用、终态判定)完全一致。
8
+ */
9
+ import { withProgressView, } from "./view.js";
10
+ /** 工具调用 input 单行摘要(终端折叠行展示用) */
11
+ export function summarizeToolInput(input, maxChars = 120) {
12
+ let raw;
13
+ if (typeof input === "string") {
14
+ raw = input;
15
+ }
16
+ else {
17
+ try {
18
+ raw = JSON.stringify(input);
19
+ }
20
+ catch {
21
+ raw = String(input);
22
+ }
23
+ }
24
+ const oneLine = raw.replace(/\s+/g, " ").trim();
25
+ return oneLine.length > maxChars ? oneLine.slice(0, maxChars) + "…" : oneLine;
26
+ }
27
+ /** 工具结果单行摘要(成功/失败各取首行) */
28
+ export function summarizeToolResult(content, maxChars = 120) {
29
+ let raw;
30
+ if (typeof content === "string") {
31
+ raw = content;
32
+ }
33
+ else if (content instanceof Error) {
34
+ raw = content.message;
35
+ }
36
+ else {
37
+ try {
38
+ raw = JSON.stringify(content);
39
+ }
40
+ catch {
41
+ raw = String(content);
42
+ }
43
+ }
44
+ const firstLine = raw.split("\n")[0] ?? "";
45
+ return firstLine.length > maxChars ? firstLine.slice(0, maxChars) + "…" : firstLine;
46
+ }
47
+ /**
48
+ * 把一条 ChatEvent 合并进 ProgressView,返回新视图(不可变更新)。
49
+ * 未识别的/不影响展示的事件(如 compact)返回原视图。
50
+ */
51
+ export function reduceProgress(prev, event) {
52
+ switch (event.type) {
53
+ case "text":
54
+ // accumulated 是全文累积,直接全量替换,天然幂等
55
+ return withProgressView(prev, { text: event.accumulated });
56
+ case "tool_use": {
57
+ const tool = {
58
+ id: event.id ?? `tool-${prev.tools.length + 1}`,
59
+ name: event.name,
60
+ status: "running",
61
+ detail: summarizeToolInput(event.input),
62
+ };
63
+ return withProgressView(prev, { tools: [...prev.tools, tool] });
64
+ }
65
+ case "tool_result": {
66
+ const nextStatus = event.is_error ? "error" : "ok";
67
+ const summary = summarizeToolResult(event.content);
68
+ let matched = false;
69
+ const tools = prev.tools.map((t) => {
70
+ if (matched || t.id !== event.tool_use_id)
71
+ return t;
72
+ matched = true;
73
+ return { ...t, status: nextStatus, summary };
74
+ });
75
+ if (!matched) {
76
+ // id 缺失或失配:兜底更新最后一个 running 工具,避免状态悬挂
77
+ const lastRunning = [...prev.tools].reverse().findIndex((t) => t.status === "running");
78
+ if (lastRunning >= 0) {
79
+ const idx = prev.tools.length - 1 - lastRunning;
80
+ const updated = prev.tools.map((t, i) => i === idx ? { ...t, status: nextStatus, summary } : t);
81
+ return withProgressView(prev, { tools: updated });
82
+ }
83
+ }
84
+ return withProgressView(prev, { tools });
85
+ }
86
+ case "done":
87
+ return withProgressView(prev, {
88
+ status: "done",
89
+ showStop: false,
90
+ text: event.text,
91
+ });
92
+ case "error":
93
+ return withProgressView(prev, { status: "error", showStop: false });
94
+ case "compact":
95
+ // 旧上下文压缩不影响当前过程展示
96
+ return prev;
97
+ }
98
+ }