@kafca/agentdock 0.1.10 → 0.1.11

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.
@@ -5,7 +5,7 @@
5
5
  <link rel="icon" type="image/svg+xml" href="./favicon.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>AgentDock</title>
8
- <script type="module" crossorigin src="./assets/index-B67E3Ah_.js"></script>
8
+ <script type="module" crossorigin src="./assets/index-BM0Jl5gB.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="./assets/index-DrLbJPYN.css">
10
10
  </head>
11
11
  <body class="bg-[#f5f5f7] dark:bg-[#151516]">
@@ -14,6 +14,7 @@ const scheduler_run_lifecycle_js_1 = require("../services/local-ai-core/src/sche
14
14
  const lark_execution_policies_js_1 = require("../services/local-ai-core/src/scheduler/lark-execution-policies.js");
15
15
  const local_core_weixin_gateway_js_1 = require("../services/local-ai-core/src/gateway/local-core-weixin-gateway.js");
16
16
  const local_core_acp_turn_coordinator_js_1 = require("../services/local-ai-core/src/acp/local-core-acp-turn-coordinator.js");
17
+ const local_core_acp_store_js_1 = require("../services/local-ai-core/src/acp/local-core-acp-store.js");
17
18
  (0, node_test_1.default)('ACP tool call update is emitted with its pending tool name', () => {
18
19
  const appended = [];
19
20
  const emitted = [];
@@ -161,6 +162,93 @@ const local_core_acp_turn_coordinator_js_1 = require("../services/local-ai-core/
161
162
  strict_1.default.equal(emitted[0]?.type, 'reply');
162
163
  strict_1.default.equal(emitted[0]?.content, appended[0]?.content);
163
164
  });
165
+ (0, node_test_1.default)('ACP thought chunks are streamed as thinking preview updates', () => {
166
+ const appended = [];
167
+ const upserted = [];
168
+ const emitted = [];
169
+ const coordinator = new local_core_acp_turn_coordinator_js_1.LocalCoreAcpTurnCoordinator({
170
+ appendMessage: (_threadId, _role, content, kind) => appended.push({ content, kind }),
171
+ upsertMessage: (_threadId, id, _role, content, kind) => upserted.push({ id, content, kind }),
172
+ emitBridge: (event) => emitted.push(event),
173
+ updateRunStatus: () => { },
174
+ sendRaw: () => true,
175
+ });
176
+ const session = {
177
+ threadId: 'thread-1',
178
+ bridgeSessionKey: 'session:thread-1',
179
+ currentRunId: 'run-1',
180
+ currentTurn: {
181
+ runId: 'run-1',
182
+ replyCtx: 'run-1',
183
+ previewHandle: 'preview-1',
184
+ thoughtPreviewHandle: 'thought-preview-1',
185
+ thoughtMessageId: 'run-1-thought',
186
+ assistantText: '',
187
+ thoughtText: '',
188
+ typingStarted: true,
189
+ previewStarted: false,
190
+ thoughtPreviewStarted: false,
191
+ permission: null,
192
+ },
193
+ loadReplayMode: false,
194
+ schedulerJobCreatedByRun: new Map(),
195
+ };
196
+ coordinator.handleAgentNotification(session, {
197
+ method: 'session/update',
198
+ params: {
199
+ update: {
200
+ sessionUpdate: 'agent_thought_chunk',
201
+ content: { type: 'text', text: '先理解问题' },
202
+ },
203
+ },
204
+ });
205
+ coordinator.handleAgentNotification(session, {
206
+ method: 'session/update',
207
+ params: {
208
+ update: {
209
+ sessionUpdate: 'agent_thought_chunk',
210
+ content: { type: 'text', text: ',再检查代码' },
211
+ },
212
+ },
213
+ });
214
+ strict_1.default.deepEqual(appended, []);
215
+ strict_1.default.deepEqual(upserted, [
216
+ {
217
+ id: 'run-1-thought',
218
+ content: '💭 先理解问题',
219
+ kind: 'progress',
220
+ },
221
+ {
222
+ id: 'run-1-thought',
223
+ content: '💭 先理解问题,再检查代码',
224
+ kind: 'progress',
225
+ },
226
+ ]);
227
+ strict_1.default.deepEqual(emitted.map((event) => event.type), ['preview_start', 'update_message']);
228
+ strict_1.default.equal(emitted[0]?.previewHandle, 'thought-preview-1');
229
+ strict_1.default.equal(emitted[0]?.content, '💭 先理解问题');
230
+ strict_1.default.equal(emitted[1]?.content, '💭 先理解问题,再检查代码');
231
+ strict_1.default.equal(session.currentTurn.thoughtText, '先理解问题,再检查代码');
232
+ });
233
+ (0, node_test_1.default)('ACP store upserts thought progress as one durable message', () => {
234
+ const userDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'agentdock-thought-store-'));
235
+ const store = new local_core_acp_store_js_1.LocalCoreAcpStore(userDataPath);
236
+ try {
237
+ const thread = store.createThread('project-1', 'Thread');
238
+ store.upsertMessage(thread.id, 'run-1-thought', 'assistant', '💭 先理解问题', 'progress');
239
+ store.upsertMessage(thread.id, 'run-1-thought', 'assistant', '💭 先理解问题,再检查代码', 'progress');
240
+ const detail = store.getThread(thread.id, []);
241
+ strict_1.default.equal(detail.messages.length, 1);
242
+ strict_1.default.equal(detail.messages[0]?.id, 'run-1-thought');
243
+ strict_1.default.equal(detail.messages[0]?.kind, 'progress');
244
+ strict_1.default.equal(detail.messages[0]?.content, '💭 先理解问题,再检查代码');
245
+ strict_1.default.equal(detail.historyCount, 1);
246
+ }
247
+ finally {
248
+ store.close();
249
+ (0, node_fs_1.rmSync)(userDataPath, { recursive: true, force: true });
250
+ }
251
+ });
164
252
  (0, node_test_1.default)('ACP skips empty generic running tool updates', () => {
165
253
  const appended = [];
166
254
  const emitted = [];
@@ -27,6 +27,9 @@ class LocalCoreAcpBackend {
27
27
  appendMessage: (threadId, role, content, kind) => {
28
28
  this.options.store.appendMessage(threadId, role, content, kind);
29
29
  },
30
+ upsertMessage: (threadId, id, role, content, kind) => {
31
+ this.options.store.upsertMessage(threadId, id, role, content, kind);
32
+ },
30
33
  updateRunStatus: (runId, threadId, status) => {
31
34
  this.options.store.updateRun(runId, threadId, status);
32
35
  },
@@ -180,9 +183,13 @@ class LocalCoreAcpBackend {
180
183
  runId,
181
184
  replyCtx: runId,
182
185
  previewHandle: (0, node_crypto_1.randomUUID)(),
186
+ thoughtPreviewHandle: (0, node_crypto_1.randomUUID)(),
187
+ thoughtMessageId: `${runId}-thought`,
183
188
  assistantText: '',
189
+ thoughtText: '',
184
190
  typingStarted: true,
185
191
  previewStarted: false,
192
+ thoughtPreviewStarted: false,
186
193
  pendingToolCallTitle: undefined,
187
194
  permission: null,
188
195
  };
@@ -244,6 +244,45 @@ class LocalCoreAcpStore {
244
244
  }
245
245
  return { id, timestamp };
246
246
  }
247
+ upsertMessage(threadId, id, role, content, kind) {
248
+ const timestamp = new Date().toISOString();
249
+ const excerpt = (0, workspace_thread_mappers_js_1.normalizeMessageContent)(content);
250
+ const existing = this.db.prepare('SELECT id FROM messages WHERE id = ? AND thread_id = ?').get(id, threadId);
251
+ this.db.exec('BEGIN IMMEDIATE');
252
+ try {
253
+ if (existing) {
254
+ this.db.prepare(`
255
+ UPDATE messages
256
+ SET content = ?, timestamp = ?, kind = ?
257
+ WHERE id = ? AND thread_id = ?
258
+ `).run(content, timestamp, kind, id, threadId);
259
+ }
260
+ else {
261
+ const nextSequenceRow = this.db.prepare('SELECT COALESCE(MAX(seq), -1) + 1 AS next_seq FROM messages WHERE thread_id = ?').get(threadId);
262
+ const nextSeq = Number(nextSequenceRow?.next_seq || 0);
263
+ this.db.prepare(`
264
+ INSERT INTO messages (id, thread_id, role, content, timestamp, kind, seq)
265
+ VALUES (?, ?, ?, ?, ?, ?, ?)
266
+ `).run(id, threadId, role, content, timestamp, kind, nextSeq);
267
+ this.db.prepare(`
268
+ UPDATE threads
269
+ SET history_count = history_count + 1
270
+ WHERE id = ?
271
+ `).run(threadId);
272
+ }
273
+ this.db.prepare(`
274
+ UPDATE threads
275
+ SET updated_at = ?, excerpt = ?
276
+ WHERE id = ?
277
+ `).run(timestamp, excerpt, threadId);
278
+ this.db.exec('COMMIT');
279
+ }
280
+ catch (error) {
281
+ this.db.exec('ROLLBACK');
282
+ throw error;
283
+ }
284
+ return { id, timestamp };
285
+ }
247
286
  updateRun(runId, threadId, status) {
248
287
  const now = new Date().toISOString();
249
288
  const existing = this.db.prepare('SELECT id FROM runs WHERE id = ?').get(runId);
@@ -163,6 +163,40 @@ class LocalCoreAcpTurnCoordinator {
163
163
  });
164
164
  return;
165
165
  }
166
+ case 'agent_thought_chunk': {
167
+ this.flushPendingToolCall(session);
168
+ if (update.content?.type !== 'text') {
169
+ return;
170
+ }
171
+ const thoughtChunk = String(update.content.text || '');
172
+ if (!thoughtChunk) {
173
+ return;
174
+ }
175
+ currentTurn.thoughtText += thoughtChunk;
176
+ const content = `💭 ${currentTurn.thoughtText.trim()}`;
177
+ if (this.options.upsertMessage) {
178
+ this.options.upsertMessage(session.threadId, currentTurn.thoughtMessageId, 'assistant', content, 'progress');
179
+ }
180
+ if (!currentTurn.thoughtPreviewStarted) {
181
+ currentTurn.thoughtPreviewStarted = true;
182
+ this.options.emitBridge({
183
+ type: 'preview_start',
184
+ sessionKey: session.bridgeSessionKey,
185
+ replyCtx: currentRunId,
186
+ previewHandle: currentTurn.thoughtPreviewHandle,
187
+ content,
188
+ });
189
+ return;
190
+ }
191
+ this.options.emitBridge({
192
+ type: 'update_message',
193
+ sessionKey: session.bridgeSessionKey,
194
+ replyCtx: currentRunId,
195
+ previewHandle: currentTurn.thoughtPreviewHandle,
196
+ content,
197
+ });
198
+ return;
199
+ }
166
200
  case 'tool_call': {
167
201
  const title = String(update.title || 'Running tool').trim();
168
202
  this.flushPendingToolCall(session);
@@ -3,11 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.upsertSessionGroup = exports.ASSISTANT_REPLY_TIMEOUT_MS = void 0;
4
4
  exports.isInternalProgressMessage = isInternalProgressMessage;
5
5
  exports.isAwaitingInputMessage = isAwaitingInputMessage;
6
+ exports.findStreamingPreviewMessage = findStreamingPreviewMessage;
7
+ exports.shouldReplacePreviewWithReply = shouldReplacePreviewWithReply;
6
8
  exports.extractVisibleMessageContent = extractVisibleMessageContent;
7
9
  exports.sessionMatchesDesktop = sessionMatchesDesktop;
8
10
  exports.toMessages = toMessages;
9
11
  exports.toMessagesFromThread = toMessagesFromThread;
10
- exports.mergePolledThreadMessages = mergePolledThreadMessages;
11
12
  exports.toChatThreadSummary = toChatThreadSummary;
12
13
  exports.toCoreChatThreadSummary = toCoreChatThreadSummary;
13
14
  exports.sortChatThreadsByLiveAndUpdated = sortChatThreadsByLiveAndUpdated;
@@ -48,6 +49,22 @@ function isAwaitingInputMessage(content) {
48
49
  normalized.includes('请直接回复') ||
49
50
  normalized.includes('请输入你的回答'));
50
51
  }
52
+ function findStreamingPreviewMessage(messages, previewHandle, replyCtx) {
53
+ if (previewHandle) {
54
+ return messages.find((message) => message.id === previewHandle);
55
+ }
56
+ if (!replyCtx) {
57
+ return undefined;
58
+ }
59
+ return messages.find((message) => message.preview && message.turnKey === replyCtx);
60
+ }
61
+ function shouldReplacePreviewWithReply(message, replyContent, replyCtx) {
62
+ if (!message.preview || !replyCtx || message.turnKey !== replyCtx) {
63
+ return false;
64
+ }
65
+ const content = message.streamTargetContent ?? message.content;
66
+ return Boolean(replyContent && content === replyContent && !isInternalProgressMessage(content));
67
+ }
51
68
  function extractVisibleMessageContent(content) {
52
69
  if (!content) {
53
70
  return '';
@@ -85,30 +102,6 @@ function toMessagesFromThread(history) {
85
102
  timestamp: message.timestamp,
86
103
  }));
87
104
  }
88
- function mergePolledThreadMessages(current, polled) {
89
- const polledIds = new Set(polled.map((message) => message.id));
90
- const polledSignatures = new Set(polled.map((message) => `${message.role}:${message.kind || 'final'}:${message.content}`));
91
- const polledFinalAssistantContent = new Set(polled
92
- .filter((message) => message.role === 'assistant' && (message.kind || 'final') === 'final')
93
- .map((message) => message.content));
94
- const retained = current.filter((message) => {
95
- if (polledIds.has(message.id)) {
96
- return false;
97
- }
98
- if (!message.preview && message.kind !== 'progress') {
99
- return false;
100
- }
101
- const content = message.streamTargetContent ?? message.content;
102
- if (polledFinalAssistantContent.has(content)) {
103
- return false;
104
- }
105
- if (message.preview) {
106
- return true;
107
- }
108
- return !polledSignatures.has(`${message.role}:${message.kind || 'final'}:${message.content}`);
109
- });
110
- return sortChatMessages([...polled, ...retained]);
111
- }
112
105
  function toChatThreadSummary(project, session) {
113
106
  return {
114
107
  id: session.id,
@@ -51,114 +51,74 @@ function createMessage(overrides) {
51
51
  strict_1.default.equal((0, thread_chat_task_state_1.taskStateForBridgeButtons)(false, false), 'idle');
52
52
  strict_1.default.equal((0, thread_chat_task_state_1.taskStateReasonForBridgeButtons)(false, false), 'bridge-buttons-idle');
53
53
  });
54
- (0, node_test_1.default)('deriveTaskStateFromThreadDetail recognizes permission and input blocking states', () => {
55
- const pendingPermission = (0, thread_chat_task_state_1.deriveTaskStateFromThreadDetail)({
56
- id: 'thread-1',
57
- workspaceId: 'default',
58
- title: 'Thread',
59
- live: false,
60
- updatedAt: '2026-04-22T00:00:00.000Z',
61
- createdAt: '2026-04-22T00:00:00.000Z',
62
- historyCount: 1,
63
- excerpt: '',
64
- messages: [],
65
- selectedKnowledgeBaseIds: [],
66
- pendingPermissionRequest: {
67
- id: 'permission-1',
68
- content: 'Permission required',
69
- actions: createPermissionActions(),
70
- actionMode: 'permission',
71
- actionInteractive: true,
72
- },
73
- }, 0, 0);
74
- strict_1.default.deepEqual(pendingPermission, {
75
- state: 'awaiting_permission',
76
- reason: 'local-core-poll-awaiting-permission',
77
- });
78
- const awaitingInput = (0, thread_chat_task_state_1.deriveTaskStateFromThreadDetail)({
79
- id: 'thread-1',
80
- workspaceId: 'default',
81
- title: 'Thread',
82
- live: false,
83
- updatedAt: '2026-04-22T00:00:00.000Z',
84
- createdAt: '2026-04-22T00:00:00.000Z',
85
- historyCount: 1,
86
- excerpt: '',
87
- messages: [
88
- {
89
- id: 'assistant-1',
90
- role: 'assistant',
91
- content: '等待你的回复,请直接回复选项编号',
92
- timestamp: '2026-04-22T00:00:00.000Z',
93
- kind: 'progress',
94
- },
95
- ],
96
- selectedKnowledgeBaseIds: [],
97
- pendingPermissionRequest: null,
98
- }, 0, 0);
99
- strict_1.default.deepEqual(awaitingInput, {
100
- state: 'awaiting_input',
101
- reason: 'local-core-poll-awaiting-input',
102
- });
103
- });
104
- (0, node_test_1.default)('mergePolledThreadMessages keeps active streaming previews during polling', () => {
54
+ (0, node_test_1.default)('reply replacement removes only the matching answer preview from a multi-message turn', () => {
105
55
  const current = [
106
56
  {
107
- id: 'user-1',
108
- role: 'user',
109
- content: 'hello',
110
- kind: 'final',
111
- order: 0,
112
- },
113
- {
114
- id: 'run-1-preview',
57
+ id: 'thought-preview',
115
58
  role: 'assistant',
116
- content: 'thinking',
117
- streamTargetContent: 'thinking through the answer',
59
+ content: '💭 checking',
60
+ streamTargetContent: '💭 checking the request',
118
61
  kind: 'progress',
119
62
  order: 1,
120
63
  turnKey: 'run-1',
121
64
  preview: true,
122
65
  previewPlainText: true,
123
66
  },
124
- ];
125
- const polled = [
126
67
  {
127
- id: 'persisted-user-1',
128
- role: 'user',
129
- content: 'hello',
130
- kind: 'final',
131
- order: 0,
132
- },
133
- ];
134
- const merged = (0, thread_chat_model_1.mergePolledThreadMessages)(current, polled);
135
- strict_1.default.equal(merged.length, 2);
136
- strict_1.default.equal(merged[1]?.id, 'run-1-preview');
137
- strict_1.default.equal(merged[1]?.content, 'thinking');
138
- });
139
- (0, node_test_1.default)('mergePolledThreadMessages drops previews once the final answer is persisted', () => {
140
- const current = [
141
- {
142
- id: 'run-1-preview',
68
+ id: 'answer-preview',
143
69
  role: 'assistant',
144
- content: 'final answer',
145
- streamTargetContent: 'final answer',
70
+ content: 'Hi',
71
+ streamTargetContent: 'Hi! How can I help you today?',
146
72
  kind: 'progress',
147
- order: 1,
73
+ order: 2,
148
74
  turnKey: 'run-1',
149
75
  preview: true,
150
76
  previewPlainText: true,
151
77
  },
152
78
  ];
153
- const polled = [
79
+ const retained = current.filter((message) => !(0, thread_chat_model_1.shouldReplacePreviewWithReply)(message, 'Hi! How can I help you today?', 'run-1'));
80
+ strict_1.default.deepEqual(retained.map((message) => message.id), ['thought-preview']);
81
+ });
82
+ (0, node_test_1.default)('findStreamingPreviewMessage keeps distinct preview handles in the same turn separate', () => {
83
+ const messages = [
154
84
  {
155
- id: 'assistant-1',
85
+ id: 'thought-preview',
156
86
  role: 'assistant',
157
- content: 'final answer',
158
- kind: 'final',
159
- order: 1,
87
+ content: '💭 checking',
88
+ kind: 'progress',
89
+ order: 0,
90
+ turnKey: 'run-1',
91
+ preview: true,
92
+ previewPlainText: true,
160
93
  },
161
94
  ];
162
- const merged = (0, thread_chat_model_1.mergePolledThreadMessages)(current, polled);
163
- strict_1.default.deepEqual(merged.map((message) => message.id), ['assistant-1']);
95
+ strict_1.default.equal((0, thread_chat_model_1.findStreamingPreviewMessage)(messages, 'assistant-preview', 'run-1'), undefined);
96
+ strict_1.default.equal((0, thread_chat_model_1.findStreamingPreviewMessage)(messages, 'thought-preview', 'run-1')?.id, 'thought-preview');
97
+ strict_1.default.equal((0, thread_chat_model_1.findStreamingPreviewMessage)(messages, undefined, 'run-1')?.id, 'thought-preview');
98
+ });
99
+ (0, node_test_1.default)('shouldReplacePreviewWithReply keeps thought previews when final answer arrives', () => {
100
+ const thoughtPreview = {
101
+ id: 'thought-preview',
102
+ role: 'assistant',
103
+ content: '💭 checking',
104
+ streamTargetContent: '💭 checking the request',
105
+ kind: 'progress',
106
+ order: 0,
107
+ turnKey: 'run-1',
108
+ preview: true,
109
+ previewPlainText: true,
110
+ };
111
+ const answerPreview = {
112
+ id: 'answer-preview',
113
+ role: 'assistant',
114
+ content: 'Hi',
115
+ streamTargetContent: 'Hi! How can I help you today?',
116
+ kind: 'progress',
117
+ order: 1,
118
+ turnKey: 'run-1',
119
+ preview: true,
120
+ previewPlainText: true,
121
+ };
122
+ strict_1.default.equal((0, thread_chat_model_1.shouldReplacePreviewWithReply)(thoughtPreview, 'Hi! How can I help you today?', 'run-1'), false);
123
+ strict_1.default.equal((0, thread_chat_model_1.shouldReplacePreviewWithReply)(answerPreview, 'Hi! How can I help you today?', 'run-1'), true);
164
124
  });
@@ -3,19 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.taskStateAfterTypingStop = taskStateAfterTypingStop;
4
4
  exports.taskStateForBridgeButtons = taskStateForBridgeButtons;
5
5
  exports.taskStateReasonForBridgeButtons = taskStateReasonForBridgeButtons;
6
- exports.deriveTaskStateFromThreadDetail = deriveTaskStateFromThreadDetail;
7
- function isAwaitingInputMessage(content) {
8
- if (!content) {
9
- return false;
10
- }
11
- const normalized = content.replace(/\s+/g, ' ').trim();
12
- return (/^Agent 提问(?:\s*\(\d+\/\d+\))?/i.test(normalized) ||
13
- normalized.includes('请回复选项编号') ||
14
- normalized.includes('直接输入你的回答') ||
15
- normalized.includes('等待你的回复') ||
16
- normalized.includes('请直接回复') ||
17
- normalized.includes('请输入你的回答'));
18
- }
19
6
  function taskStateAfterTypingStop(taskState) {
20
7
  return taskState === 'awaiting_permission' ? 'awaiting_permission' : 'idle';
21
8
  }
@@ -37,29 +24,3 @@ function taskStateReasonForBridgeButtons(hasActions, hasInteractivePermission) {
37
24
  }
38
25
  return 'bridge-buttons-idle';
39
26
  }
40
- function deriveTaskStateFromThreadDetail(detail, baselineResponseCount, unchangedPolls) {
41
- if (detail.pendingPermissionRequest) {
42
- return {
43
- state: 'awaiting_permission',
44
- reason: 'local-core-poll-awaiting-permission',
45
- };
46
- }
47
- const assistantMessages = detail.messages.filter((message) => message.role === 'assistant');
48
- const latestAssistantMessage = assistantMessages[assistantMessages.length - 1];
49
- if (latestAssistantMessage && isAwaitingInputMessage(latestAssistantMessage.content)) {
50
- return {
51
- state: 'awaiting_input',
52
- reason: 'local-core-poll-awaiting-input',
53
- };
54
- }
55
- if (latestAssistantMessage &&
56
- latestAssistantMessage.kind !== 'progress' &&
57
- detail.messages.filter((message) => message.role !== 'user' && message.kind !== 'progress').length > baselineResponseCount &&
58
- unchangedPolls >= 1) {
59
- return {
60
- state: 'idle',
61
- reason: 'local-core-poll-complete',
62
- };
63
- }
64
- return null;
65
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kafca/agentdock",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "type": "module",
5
5
  "main": "dist-electron/electron/main.js",
6
6
  "bin": {