chatccc 0.2.204 → 0.2.206
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/__tests__/agent-activity.test.ts +76 -0
- package/src/__tests__/codex-adapter.test.ts +7 -7
- package/src/__tests__/cursor-adapter.test.ts +5 -5
- package/src/__tests__/response-stall.test.ts +49 -0
- package/src/__tests__/session.test.ts +277 -33
- package/src/adapters/codex-adapter.ts +1 -0
- package/src/adapters/cursor-adapter.ts +1 -0
- package/src/agent-activity.ts +170 -0
- package/src/response-stall.ts +28 -0
- package/src/session-chat-binding.ts +20 -10
- package/src/session.ts +317 -121
- package/src/stream-state.ts +18 -10
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type { UnifiedBlock } from "./adapters/adapter-interface.ts";
|
|
2
|
+
|
|
3
|
+
export type AgentActivityKind =
|
|
4
|
+
| "starting"
|
|
5
|
+
| "thinking"
|
|
6
|
+
| "tool"
|
|
7
|
+
| "processing"
|
|
8
|
+
| "responding"
|
|
9
|
+
| "searching"
|
|
10
|
+
| "compacting";
|
|
11
|
+
|
|
12
|
+
/** The user-visible activity of a running Agent turn. */
|
|
13
|
+
export interface AgentActivity {
|
|
14
|
+
kind: AgentActivityKind;
|
|
15
|
+
/** Time when the current activity began, used for truthful elapsed time. */
|
|
16
|
+
startedAt: number;
|
|
17
|
+
toolName?: string;
|
|
18
|
+
toolCount?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ActiveTool {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
startedAt: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AgentActivityTracker {
|
|
28
|
+
activity: AgentActivity;
|
|
29
|
+
activeTools: Map<string, ActiveTool>;
|
|
30
|
+
nextAnonymousToolId: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function createAgentActivityTracker(now = Date.now()): AgentActivityTracker {
|
|
34
|
+
return {
|
|
35
|
+
activity: { kind: "starting", startedAt: now },
|
|
36
|
+
activeTools: new Map(),
|
|
37
|
+
nextAnonymousToolId: 1,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function sameVisibleActivity(left: AgentActivity, right: AgentActivity): boolean {
|
|
42
|
+
return left.kind === right.kind
|
|
43
|
+
&& left.toolName === right.toolName
|
|
44
|
+
&& left.toolCount === right.toolCount;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function setActivity(tracker: AgentActivityTracker, next: AgentActivity): boolean {
|
|
48
|
+
if (sameVisibleActivity(tracker.activity, next)) return false;
|
|
49
|
+
tracker.activity = next;
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function refreshToolActivity(tracker: AgentActivityTracker): boolean {
|
|
54
|
+
const tools = [...tracker.activeTools.values()];
|
|
55
|
+
const first = tools[0];
|
|
56
|
+
if (!first) return false;
|
|
57
|
+
return setActivity(tracker, {
|
|
58
|
+
kind: "tool",
|
|
59
|
+
startedAt: first.startedAt,
|
|
60
|
+
toolName: first.name,
|
|
61
|
+
toolCount: tools.length,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function removeCompletedTool(tracker: AgentActivityTracker, toolUseId: string): void {
|
|
66
|
+
if (toolUseId && tracker.activeTools.delete(toolUseId)) return;
|
|
67
|
+
|
|
68
|
+
// Older adapters did not always include a tool ID. Prefer an anonymous entry;
|
|
69
|
+
// if there is only one active call, it is still safe to match that result.
|
|
70
|
+
const anonymousId = [...tracker.activeTools.keys()].find((id) => id.startsWith("anonymous:"));
|
|
71
|
+
if (anonymousId) {
|
|
72
|
+
tracker.activeTools.delete(anonymousId);
|
|
73
|
+
} else if (tracker.activeTools.size === 1) {
|
|
74
|
+
const onlyId = tracker.activeTools.keys().next().value as string | undefined;
|
|
75
|
+
if (onlyId) tracker.activeTools.delete(onlyId);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Applies one normalized Agent event and returns whether the persisted activity
|
|
81
|
+
* changed. Tool activity takes precedence while a tool call is still active.
|
|
82
|
+
*/
|
|
83
|
+
export function updateAgentActivity(
|
|
84
|
+
tracker: AgentActivityTracker,
|
|
85
|
+
block: UnifiedBlock,
|
|
86
|
+
now = Date.now(),
|
|
87
|
+
): boolean {
|
|
88
|
+
if (block.type === "tool_use") {
|
|
89
|
+
const id = block.id || `anonymous:${tracker.nextAnonymousToolId++}`;
|
|
90
|
+
const existing = tracker.activeTools.get(id);
|
|
91
|
+
tracker.activeTools.set(id, {
|
|
92
|
+
id,
|
|
93
|
+
name: block.name || "未知工具",
|
|
94
|
+
startedAt: existing?.startedAt ?? now,
|
|
95
|
+
});
|
|
96
|
+
return refreshToolActivity(tracker);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (block.type === "tool_result") {
|
|
100
|
+
removeCompletedTool(tracker, block.tool_use_id);
|
|
101
|
+
if (tracker.activeTools.size > 0) return refreshToolActivity(tracker);
|
|
102
|
+
return setActivity(tracker, { kind: "processing", startedAt: now });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (tracker.activeTools.size > 0) return false;
|
|
106
|
+
|
|
107
|
+
switch (block.type) {
|
|
108
|
+
case "thinking":
|
|
109
|
+
case "redacted_thinking":
|
|
110
|
+
return setActivity(tracker, { kind: "thinking", startedAt: now });
|
|
111
|
+
case "text":
|
|
112
|
+
case "text_final":
|
|
113
|
+
return setActivity(tracker, { kind: "responding", startedAt: now });
|
|
114
|
+
case "search_result":
|
|
115
|
+
return setActivity(tracker, { kind: "searching", startedAt: now });
|
|
116
|
+
case "compact_boundary":
|
|
117
|
+
return setActivity(tracker, { kind: "compacting", startedAt: now });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function formatElapsed(startedAt: number, now: number): string {
|
|
122
|
+
const totalSeconds = Math.max(0, Math.floor((now - startedAt) / 1000));
|
|
123
|
+
if (totalSeconds < 60) return `${totalSeconds}秒`;
|
|
124
|
+
const totalMinutes = Math.floor(totalSeconds / 60);
|
|
125
|
+
const seconds = totalSeconds % 60;
|
|
126
|
+
if (totalMinutes < 60) return `${totalMinutes}分${seconds}秒`;
|
|
127
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
128
|
+
const minutes = totalMinutes % 60;
|
|
129
|
+
return `${hours}小时${minutes}分`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function displayToolName(name: string | undefined): string {
|
|
133
|
+
const normalized = (name || "未知工具").replace(/\s+/g, " ").trim();
|
|
134
|
+
return normalized.length > 24 ? `${normalized.slice(0, 23)}…` : normalized;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function formatAgentActivityTitle(
|
|
138
|
+
activity: AgentActivity | undefined,
|
|
139
|
+
now = Date.now(),
|
|
140
|
+
): string {
|
|
141
|
+
if (!activity) return "正在处理";
|
|
142
|
+
|
|
143
|
+
let label: string;
|
|
144
|
+
switch (activity.kind) {
|
|
145
|
+
case "starting":
|
|
146
|
+
label = "正在启动 Agent";
|
|
147
|
+
break;
|
|
148
|
+
case "thinking":
|
|
149
|
+
label = "思考中";
|
|
150
|
+
break;
|
|
151
|
+
case "tool": {
|
|
152
|
+
const count = Math.max(1, activity.toolCount ?? 1);
|
|
153
|
+
label = `正在执行 ${displayToolName(activity.toolName)}${count > 1 ? ` 等 ${count} 项` : ""}`;
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
case "processing":
|
|
157
|
+
label = "正在处理工具结果";
|
|
158
|
+
break;
|
|
159
|
+
case "responding":
|
|
160
|
+
label = "正在生成回复";
|
|
161
|
+
break;
|
|
162
|
+
case "searching":
|
|
163
|
+
label = "正在处理搜索结果";
|
|
164
|
+
break;
|
|
165
|
+
case "compacting":
|
|
166
|
+
label = "正在整理上下文";
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
return `${label} · ${formatElapsed(activity.startedAt, now)}`;
|
|
170
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** A snapshot of response output progress while the Agent is generating a reply. */
|
|
2
|
+
export interface ResponseProgressObservation {
|
|
3
|
+
totalChars: number;
|
|
4
|
+
unchangedSince: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Tracks how long the displayed response character count has remained unchanged.
|
|
9
|
+
* Leaving the responding phase clears the window; returning starts a fresh one.
|
|
10
|
+
*/
|
|
11
|
+
export function observeResponseProgress(
|
|
12
|
+
previous: ResponseProgressObservation | undefined,
|
|
13
|
+
isResponding: boolean,
|
|
14
|
+
totalChars: number,
|
|
15
|
+
now = Date.now(),
|
|
16
|
+
): ResponseProgressObservation | undefined {
|
|
17
|
+
if (!isResponding) return undefined;
|
|
18
|
+
if (previous?.totalChars === totalChars) return previous;
|
|
19
|
+
return { totalChars, unchangedSince: now };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function hasResponseStalled(
|
|
23
|
+
observation: ResponseProgressObservation | undefined,
|
|
24
|
+
now: number,
|
|
25
|
+
timeoutMs: number,
|
|
26
|
+
): boolean {
|
|
27
|
+
return observation !== undefined && now - observation.unchangedSince >= timeoutMs;
|
|
28
|
+
}
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// 由 session.ts 在初始化时调用 rebuildSessionChatsFromRegistry 重建
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
7
7
|
|
|
8
|
-
import type { PlatformAdapter } from "./platform-adapter.ts";
|
|
8
|
+
import type { PlatformAdapter } from "./platform-adapter.ts";
|
|
9
|
+
import type { ResponseProgressObservation } from "./response-stall.ts";
|
|
9
10
|
|
|
10
11
|
const sessionChatsMap = new Map<string, Set<string>>();
|
|
11
12
|
|
|
@@ -66,8 +67,14 @@ export interface ActivePrompt {
|
|
|
66
67
|
stopped: boolean;
|
|
67
68
|
startTime: number;
|
|
68
69
|
/** Root PID for the CLI process currently serving this prompt, if the adapter exposes one. */
|
|
69
|
-
processPid?: number;
|
|
70
|
-
processMonitor?: ReturnType<typeof setInterval>;
|
|
70
|
+
processPid?: number;
|
|
71
|
+
processMonitor?: ReturnType<typeof setInterval>;
|
|
72
|
+
responseStallMonitor?: ReturnType<typeof setInterval>;
|
|
73
|
+
/** Character-count progress observed only while the activity is "responding". */
|
|
74
|
+
responseProgress?: ResponseProgressObservation;
|
|
75
|
+
/** Set before a response-stall auto-end begins so competing monitors cannot win the race. */
|
|
76
|
+
autoEnded?: boolean;
|
|
77
|
+
autoEndedAt?: number;
|
|
71
78
|
/** Set when the watchdog detects that the CLI process disappeared before stream finalization. */
|
|
72
79
|
abnormalExit?: boolean;
|
|
73
80
|
abnormalExitNotified?: boolean;
|
|
@@ -139,8 +146,10 @@ export interface DisplayCardState {
|
|
|
139
146
|
cardId: string;
|
|
140
147
|
sequence: number;
|
|
141
148
|
cardBusy: boolean;
|
|
142
|
-
cardCreatedAt: number;
|
|
143
|
-
lastSentContent: string;
|
|
149
|
+
cardCreatedAt: number;
|
|
150
|
+
lastSentContent: string;
|
|
151
|
+
/** Last rendered activity header; elapsed time can change without body output. */
|
|
152
|
+
lastSentHeaderTitle?: string;
|
|
144
153
|
streamErrorNotified: boolean;
|
|
145
154
|
/** 所属 session */
|
|
146
155
|
sessionId: string;
|
|
@@ -153,8 +162,8 @@ export interface DisplayCardState {
|
|
|
153
162
|
lastSentAccLen?: number;
|
|
154
163
|
/** WeChat delta: 上次发送时的 finalReply */
|
|
155
164
|
lastSentFinalReply?: string;
|
|
156
|
-
/**
|
|
157
|
-
dotCount: number;
|
|
165
|
+
/** Liveness animation counter; the explicit activity header conveys Agent state. */
|
|
166
|
+
dotCount: number;
|
|
158
167
|
}
|
|
159
168
|
|
|
160
169
|
export const displayCards = new Map<string, DisplayCardState>();
|
|
@@ -218,9 +227,10 @@ export function consumeQueuedMessage(platform: PlatformAdapter, msg: QueuedMessa
|
|
|
218
227
|
export function resetBindingState(): void {
|
|
219
228
|
sessionChatsMap.clear();
|
|
220
229
|
lastActiveChatMap.clear();
|
|
221
|
-
for (const prompt of activePrompts.values()) {
|
|
222
|
-
if (prompt.processMonitor) clearInterval(prompt.processMonitor);
|
|
223
|
-
|
|
230
|
+
for (const prompt of activePrompts.values()) {
|
|
231
|
+
if (prompt.processMonitor) clearInterval(prompt.processMonitor);
|
|
232
|
+
if (prompt.responseStallMonitor) clearInterval(prompt.responseStallMonitor);
|
|
233
|
+
}
|
|
224
234
|
activePrompts.clear();
|
|
225
235
|
queuedMessages.clear();
|
|
226
236
|
displayCards.clear();
|