shennian 0.2.89 → 0.2.90
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/dist/assets/wechat-channel/macos/manifest.json +13 -4
- package/dist/assets/wechat-channel/macos/shennian-wechat-channel-helper +0 -0
- package/dist/bin/shennian.js +1 -1
- package/dist/publish-build-manifest.json +548 -0
- package/dist/scripts/wechat-rpa-confirmation.mjs +5 -97
- package/dist/src/agent-env.js +4 -105
- package/dist/src/agents/adapter.js +1 -19
- package/dist/src/agents/claude.js +8 -305
- package/dist/src/agents/codex-control.js +2 -188
- package/dist/src/agents/codex-utils.js +7 -200
- package/dist/src/agents/codex.js +15 -916
- package/dist/src/agents/command-spec.js +2 -413
- package/dist/src/agents/config-status.js +1 -226
- package/dist/src/agents/cursor.js +1 -249
- package/dist/src/agents/custom.js +4 -271
- package/dist/src/agents/detect.js +1 -56
- package/dist/src/agents/external-channel-instructions.js +10 -94
- package/dist/src/agents/gemini.js +1 -173
- package/dist/src/agents/manager.js +13 -157
- package/dist/src/agents/model-registry/cache.js +1 -37
- package/dist/src/agents/model-registry/discovery.js +2 -187
- package/dist/src/agents/model-registry/parsers.js +4 -447
- package/dist/src/agents/model-registry/runner.js +1 -30
- package/dist/src/agents/model-registry/service.js +1 -78
- package/dist/src/agents/model-registry/types.js +1 -8
- package/dist/src/agents/model-registry.js +1 -18
- package/dist/src/agents/openclaw.js +2 -275
- package/dist/src/agents/opencode.js +1 -231
- package/dist/src/agents/pi-context.js +12 -217
- package/dist/src/agents/pi.js +14 -723
- package/dist/src/agents/platform-instructions.js +9 -54
- package/dist/src/channels/base.js +1 -3
- package/dist/src/channels/registry.js +1 -30
- package/dist/src/channels/reply-split.js +10 -89
- package/dist/src/channels/runtime.js +5 -564
- package/dist/src/channels/secret-registry.js +1 -46
- package/dist/src/channels/websocket.js +8 -378
- package/dist/src/channels/wechat-channel/anchor.js +1 -65
- package/dist/src/channels/wechat-channel/client.js +1 -96
- package/dist/src/channels/wechat-channel/cooldown.js +1 -38
- package/dist/src/channels/wechat-channel/fingerprint.js +1 -71
- package/dist/src/channels/wechat-channel/helper-assets.d.ts +10 -1
- package/dist/src/channels/wechat-channel/helper-assets.js +1 -68
- package/dist/src/channels/wechat-channel/helper-client.js +3 -149
- package/dist/src/channels/wechat-channel/helper-protocol.d.ts +1 -1
- package/dist/src/channels/wechat-channel/helper-protocol.js +1 -115
- package/dist/src/channels/wechat-channel/index.d.ts +1 -0
- package/dist/src/channels/wechat-channel/index.js +1 -19
- package/dist/src/channels/wechat-channel/ledger.js +1 -54
- package/dist/src/channels/wechat-channel/media-resolver.js +1 -181
- package/dist/src/channels/wechat-channel/message-key.js +1 -105
- package/dist/src/channels/wechat-channel/observer.js +1 -118
- package/dist/src/channels/wechat-channel/outbound-ledger.d.ts +3 -0
- package/dist/src/channels/wechat-channel/outbound-ledger.js +2 -112
- package/dist/src/channels/wechat-channel/outbound-sender.d.ts +26 -0
- package/dist/src/channels/wechat-channel/outbound-sender.js +1 -0
- package/dist/src/channels/wechat-channel/preflight.js +1 -48
- package/dist/src/channels/wechat-channel/runner.js +1 -84
- package/dist/src/channels/wechat-channel/runtime.js +1 -66
- package/dist/src/channels/wechat-channel/scheduler.d.ts +5 -0
- package/dist/src/channels/wechat-channel/scheduler.js +1 -152
- package/dist/src/channels/wechat-rpa/macos-flow.js +1 -96
- package/dist/src/channels/wechat-rpa/macos.js +6 -48
- package/dist/src/channels/wechat-rpa/normalizer.js +7 -127
- package/dist/src/channels/wechat-rpa.js +6 -1028
- package/dist/src/channels/wecom.js +4 -357
- package/dist/src/commands/agent.js +6 -131
- package/dist/src/commands/daemon-windows.js +8 -48
- package/dist/src/commands/daemon.js +19 -1013
- package/dist/src/commands/external-attachments.js +1 -51
- package/dist/src/commands/external.js +1 -137
- package/dist/src/commands/manager.js +2 -391
- package/dist/src/commands/pair-qr.js +1 -6
- package/dist/src/commands/pair.js +9 -287
- package/dist/src/commands/tools.js +1 -34
- package/dist/src/commands/upgrade.js +1 -198
- package/dist/src/config/index.js +1 -35
- package/dist/src/daemon-log.js +6 -58
- package/dist/src/env-path.js +1 -64
- package/dist/src/fs/boundary.js +1 -126
- package/dist/src/fs/handler.js +1 -130
- package/dist/src/fs/security.js +1 -32
- package/dist/src/fs/text-decoder.js +1 -110
- package/dist/src/index.js +2 -404
- package/dist/src/log-reporter.js +1 -16
- package/dist/src/manager/prompt.js +29 -34
- package/dist/src/manager/registry.js +2 -269
- package/dist/src/manager/runtime.js +19 -1007
- package/dist/src/native-fusion/config.js +1 -5
- package/dist/src/native-fusion/opencode-parser.js +3 -123
- package/dist/src/native-fusion/parser-common.js +8 -264
- package/dist/src/native-fusion/parsers.js +8 -729
- package/dist/src/native-fusion/service.js +2 -225
- package/dist/src/native-fusion/state.js +1 -22
- package/dist/src/native-fusion/types.js +1 -1
- package/dist/src/region.js +1 -88
- package/dist/src/relay/client.js +1 -343
- package/dist/src/session/archive-zip.js +1 -220
- package/dist/src/session/handlers/agent-config.js +1 -150
- package/dist/src/session/handlers/agents.js +1 -55
- package/dist/src/session/handlers/chat.js +2 -751
- package/dist/src/session/handlers/control.js +1 -55
- package/dist/src/session/handlers/fs.js +1 -783
- package/dist/src/session/handlers/session-refresh.js +1 -47
- package/dist/src/session/handlers/skills.js +1 -121
- package/dist/src/session/handlers/title.js +1 -60
- package/dist/src/session/handlers/tool-detail.js +1 -218
- package/dist/src/session/manager.js +1 -319
- package/dist/src/session/projection.js +1 -54
- package/dist/src/session/queue.js +4 -317
- package/dist/src/session/remote-attachments.js +1 -72
- package/dist/src/session/store.js +3 -109
- package/dist/src/session/types.js +1 -4
- package/dist/src/skills/registry.js +15 -148
- package/dist/src/skills/setup.js +1 -101
- package/dist/src/tools/markdown-to-pdf.js +10 -346
- package/dist/src/upgrade/engine.js +3 -347
- package/package.json +3 -2
|
@@ -1,751 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
// @test src/__tests__/session-manager.test.ts
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
import { createAgent } from '../../agents/adapter.js';
|
|
5
|
-
import { buildApprovalPendingPayload, buildUserMessagePayload } from '@shennian/wire';
|
|
6
|
-
import { reportLog } from '../../log-reporter.js';
|
|
7
|
-
import { lookupClaudeTranscriptCwd } from '../../native-fusion/parsers.js';
|
|
8
|
-
import { appendMessage, recordSession } from '../store.js';
|
|
9
|
-
import { mergeProjectedSessions } from '../projection.js';
|
|
10
|
-
import { getManagerRuntimeService } from '../../manager/runtime.js';
|
|
11
|
-
import { buildManagedAgentEnv } from '../../agents/config-status.js';
|
|
12
|
-
import { materializeRemoteChatAttachments } from '../remote-attachments.js';
|
|
13
|
-
function normalizeChatAttachments(value) {
|
|
14
|
-
if (!Array.isArray(value))
|
|
15
|
-
return undefined;
|
|
16
|
-
const attachments = value
|
|
17
|
-
.map((item) => {
|
|
18
|
-
if (!item || typeof item !== 'object')
|
|
19
|
-
return null;
|
|
20
|
-
const entry = item;
|
|
21
|
-
const path = typeof entry.path === 'string' ? entry.path : '';
|
|
22
|
-
const name = typeof entry.name === 'string' ? entry.name : '';
|
|
23
|
-
const mimeType = typeof entry.mimeType === 'string' ? entry.mimeType : '';
|
|
24
|
-
if (!path || !name || !mimeType)
|
|
25
|
-
return null;
|
|
26
|
-
const previewData = typeof entry.previewData === 'string' && entry.previewData.trim() ? entry.previewData.trim() : undefined;
|
|
27
|
-
return { path, name, mimeType, kind: mimeType.startsWith('image/') ? 'image' : 'file', ...(previewData ? { previewData } : {}) };
|
|
28
|
-
})
|
|
29
|
-
.filter((item) => item != null);
|
|
30
|
-
return attachments.length ? attachments : undefined;
|
|
31
|
-
}
|
|
32
|
-
function extractSummary(text) {
|
|
33
|
-
const newline = text.indexOf('\n');
|
|
34
|
-
const end = newline > 0 ? Math.min(newline, 80) : Math.min(text.length, 80);
|
|
35
|
-
return text.slice(0, end);
|
|
36
|
-
}
|
|
37
|
-
function buildRelayAgentPayload(event, sessionId, extra = {}) {
|
|
38
|
-
if (event.state === 'tool-call' || event.state === 'tool-result') {
|
|
39
|
-
const detailRef = { runId: event.runId, sourceSeq: event.seq };
|
|
40
|
-
return {
|
|
41
|
-
state: event.state,
|
|
42
|
-
runId: event.runId,
|
|
43
|
-
seq: event.seq,
|
|
44
|
-
sessionId,
|
|
45
|
-
detailRef,
|
|
46
|
-
...(event.name ? { name: event.name } : {}),
|
|
47
|
-
...(event.source ? { source: event.source } : {}),
|
|
48
|
-
...(event.agentSessionId ? { agentSessionId: event.agentSessionId } : {}),
|
|
49
|
-
...extra,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
return { ...event, sessionId, ...extra };
|
|
53
|
-
}
|
|
54
|
-
const SESSION_ACTIVITY_HEARTBEAT_INTERVAL_MS = 30_000;
|
|
55
|
-
function runPhaseFromAgentEvent(event) {
|
|
56
|
-
if (event.state === 'heartbeat')
|
|
57
|
-
return event.runPhase ?? null;
|
|
58
|
-
if (event.state === 'tool-call' || event.state === 'tool-result')
|
|
59
|
-
return 'tool_running';
|
|
60
|
-
if (event.state === 'approval-pending')
|
|
61
|
-
return 'waiting_approval';
|
|
62
|
-
if (event.state === 'delta')
|
|
63
|
-
return event.thinking ? 'thinking' : 'streaming_text';
|
|
64
|
-
if (event.state === 'init' || event.state === 'start')
|
|
65
|
-
return 'thinking';
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
function formatAgentSendFailure(agentType, err) {
|
|
69
|
-
const raw = err instanceof Error ? err.message : String(err);
|
|
70
|
-
if (agentType === 'pi' &&
|
|
71
|
-
(raw.includes('429') || raw.includes('daily_quota_exceeded') || raw.includes('nian_quota_exceeded'))) {
|
|
72
|
-
return raw.includes('too quickly') || raw.includes('per minute')
|
|
73
|
-
? 'Nian 请求过于频繁,请稍后再试。'
|
|
74
|
-
: 'Nian 今日额度已用完,次日自动恢复。';
|
|
75
|
-
}
|
|
76
|
-
return `Agent send failed: ${raw}`;
|
|
77
|
-
}
|
|
78
|
-
function getNativeSourceAgentType(agentType, modelId) {
|
|
79
|
-
if (agentType !== 'manager')
|
|
80
|
-
return agentType;
|
|
81
|
-
return modelId === 'claude' ? 'claude' : 'codex';
|
|
82
|
-
}
|
|
83
|
-
function sendSessionMessageEvent(runtime, envelope, session) {
|
|
84
|
-
runtime.client.sendEvent({
|
|
85
|
-
type: 'event',
|
|
86
|
-
event: 'session.message',
|
|
87
|
-
payload: {
|
|
88
|
-
sessionId: envelope.sessionId,
|
|
89
|
-
message: envelope,
|
|
90
|
-
session: {
|
|
91
|
-
id: envelope.sessionId,
|
|
92
|
-
agentType: session.agentType,
|
|
93
|
-
agentSessionId: session.agentSessionId ?? null,
|
|
94
|
-
modelId: session.modelId ?? null,
|
|
95
|
-
workDir: session.workDir,
|
|
96
|
-
status: 'active',
|
|
97
|
-
externalChannel: getSessionExternalChannel(runtime, envelope.sessionId, session.agentType),
|
|
98
|
-
},
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
function sendSessionUpdateEvent(runtime, input) {
|
|
103
|
-
recordSession({
|
|
104
|
-
sessionId: input.sessionId,
|
|
105
|
-
agentType: input.agentType,
|
|
106
|
-
workDir: input.workDir,
|
|
107
|
-
agentSessionId: input.agentSessionId ?? null,
|
|
108
|
-
modelId: input.modelId ?? null,
|
|
109
|
-
});
|
|
110
|
-
runtime.client.sendEvent({
|
|
111
|
-
type: 'event',
|
|
112
|
-
event: 'session.update',
|
|
113
|
-
payload: {
|
|
114
|
-
session: {
|
|
115
|
-
id: input.sessionId,
|
|
116
|
-
agentType: input.agentType,
|
|
117
|
-
agentSessionId: input.agentSessionId ?? null,
|
|
118
|
-
modelId: input.modelId ?? null,
|
|
119
|
-
workDir: input.workDir,
|
|
120
|
-
status: 'active',
|
|
121
|
-
externalChannel: getSessionExternalChannel(runtime, input.sessionId, input.agentType),
|
|
122
|
-
},
|
|
123
|
-
},
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
function normalizeExternalChannel(value) {
|
|
127
|
-
if (!value || typeof value !== 'object')
|
|
128
|
-
return null;
|
|
129
|
-
const raw = value;
|
|
130
|
-
return {
|
|
131
|
-
configured: raw.configured === undefined ? undefined : Boolean(raw.configured),
|
|
132
|
-
connected: Boolean(raw.connected),
|
|
133
|
-
type: typeof raw.type === 'string' ? raw.type : null,
|
|
134
|
-
channelId: typeof raw.channelId === 'string' ? raw.channelId : null,
|
|
135
|
-
name: typeof raw.name === 'string' ? raw.name : null,
|
|
136
|
-
canReply: raw.canReply === undefined || raw.canReply === null ? null : Boolean(raw.canReply),
|
|
137
|
-
systemPrompt: typeof raw.systemPrompt === 'string' ? raw.systemPrompt : null,
|
|
138
|
-
wechatRpaSource: typeof raw.wechatRpaSource === 'string' ? raw.wechatRpaSource : null,
|
|
139
|
-
wechatRpaGroups: Array.isArray(raw.wechatRpaGroups)
|
|
140
|
-
? raw.wechatRpaGroups
|
|
141
|
-
.map((item) => ({ name: String(item?.name || '').trim() }))
|
|
142
|
-
.filter((item) => item.name)
|
|
143
|
-
: null,
|
|
144
|
-
pollIntervalMs: Number.isFinite(raw.pollIntervalMs) ? Number(raw.pollIntervalMs) : null,
|
|
145
|
-
recentLimit: Number.isFinite(raw.recentLimit) ? Number(raw.recentLimit) : null,
|
|
146
|
-
idleSeconds: Number.isFinite(raw.idleSeconds) ? Number(raw.idleSeconds) : null,
|
|
147
|
-
forceForeground: raw.forceForeground === undefined || raw.forceForeground === null ? null : Boolean(raw.forceForeground),
|
|
148
|
-
noRestore: raw.noRestore === undefined || raw.noRestore === null ? null : Boolean(raw.noRestore),
|
|
149
|
-
downloadAttachments: raw.downloadAttachments === undefined || raw.downloadAttachments === null ? null : Boolean(raw.downloadAttachments),
|
|
150
|
-
downloadAttachmentsDir: typeof raw.downloadAttachmentsDir === 'string' ? raw.downloadAttachmentsDir : null,
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
function externalChannelEnabled(channel) {
|
|
154
|
-
return Boolean(channel?.configured ?? channel?.connected);
|
|
155
|
-
}
|
|
156
|
-
function managedProviderEnv(agentType) {
|
|
157
|
-
return buildManagedAgentEnv(agentType);
|
|
158
|
-
}
|
|
159
|
-
function externalChannelEnv(sessionId, channel, replyTarget) {
|
|
160
|
-
if (!externalChannelEnabled(channel))
|
|
161
|
-
return {};
|
|
162
|
-
const service = getManagerRuntimeService();
|
|
163
|
-
const injected = service?.getInjectedEnv(sessionId, null, process.cwd(), 'external') ?? {};
|
|
164
|
-
return {
|
|
165
|
-
...injected,
|
|
166
|
-
SHENNIAN_EXTERNAL_SESSION_ID: sessionId,
|
|
167
|
-
SHENNIAN_MANAGER_SESSION_ID: sessionId,
|
|
168
|
-
...(replyTarget?.trim() ? { SHENNIAN_EXTERNAL_REPLY_TARGET: replyTarget.trim() } : {}),
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
function configureAdapterForSession(adapter, sessionId, agentType, channel, replyTarget) {
|
|
172
|
-
adapter.configure?.({
|
|
173
|
-
sessionId,
|
|
174
|
-
externalChannel: channel ?? null,
|
|
175
|
-
env: {
|
|
176
|
-
...managedProviderEnv(agentType),
|
|
177
|
-
...externalChannelEnv(sessionId, channel, replyTarget),
|
|
178
|
-
},
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
function getSessionExternalChannel(runtime, sessionId, agentType) {
|
|
182
|
-
const active = runtime.sessions.get(sessionId);
|
|
183
|
-
if (active?.externalChannel)
|
|
184
|
-
return active.externalChannel;
|
|
185
|
-
if (agentType === 'manager')
|
|
186
|
-
return runtime.managerRuntime?.getExternalChannelStatus(sessionId) ?? null;
|
|
187
|
-
return null;
|
|
188
|
-
}
|
|
189
|
-
function maybeResolveClaudeImportedWorkDir(agentType, workDir, agentSessionId) {
|
|
190
|
-
if (agentType !== 'claude')
|
|
191
|
-
return workDir;
|
|
192
|
-
if (!agentSessionId)
|
|
193
|
-
return workDir;
|
|
194
|
-
const trimmed = workDir.trim();
|
|
195
|
-
const looksLikeTranscriptSlug = !!trimmed && trimmed.startsWith('-') && !trimmed.includes('/');
|
|
196
|
-
if (!looksLikeTranscriptSlug)
|
|
197
|
-
return workDir;
|
|
198
|
-
return lookupClaudeTranscriptCwd(agentSessionId) ?? workDir;
|
|
199
|
-
}
|
|
200
|
-
function bindAdapterEvents(runtime, sessionId, agentType, adapter) {
|
|
201
|
-
let emittedAgentSessionId = null;
|
|
202
|
-
function sendAgentEvent(event, extra = {}) {
|
|
203
|
-
runtime.client.sendAgentEvent({
|
|
204
|
-
type: 'event',
|
|
205
|
-
event: 'agent',
|
|
206
|
-
payload: buildRelayAgentPayload(event, sessionId, extra),
|
|
207
|
-
seq: event.seq,
|
|
208
|
-
id: `agent-evt-${event.runId}-${event.seq}`,
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
function stopActivityHeartbeat(activeSession) {
|
|
212
|
-
if (!activeSession?.heartbeatTimer)
|
|
213
|
-
return;
|
|
214
|
-
clearInterval(activeSession.heartbeatTimer);
|
|
215
|
-
activeSession.heartbeatTimer = null;
|
|
216
|
-
}
|
|
217
|
-
function sendActivityHeartbeat(activeSession) {
|
|
218
|
-
if (!activeSession.currentRunId || !activeSession.currentRunPhase)
|
|
219
|
-
return;
|
|
220
|
-
const seq = activeSession.heartbeatSeq++;
|
|
221
|
-
runtime.client.sendAgentEvent({
|
|
222
|
-
type: 'event',
|
|
223
|
-
event: 'agent',
|
|
224
|
-
payload: {
|
|
225
|
-
state: 'heartbeat',
|
|
226
|
-
sessionId,
|
|
227
|
-
runId: activeSession.currentRunId,
|
|
228
|
-
seq,
|
|
229
|
-
runPhase: activeSession.currentRunPhase,
|
|
230
|
-
},
|
|
231
|
-
seq,
|
|
232
|
-
id: `agent-heartbeat-${activeSession.currentRunId}-${seq}-${Date.now()}`,
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
function ensureActivityHeartbeat(activeSession) {
|
|
236
|
-
if (!activeSession || activeSession.heartbeatTimer)
|
|
237
|
-
return;
|
|
238
|
-
activeSession.heartbeatTimer = setInterval(() => {
|
|
239
|
-
sendActivityHeartbeat(activeSession);
|
|
240
|
-
}, SESSION_ACTIVITY_HEARTBEAT_INTERVAL_MS);
|
|
241
|
-
activeSession.heartbeatTimer.unref?.();
|
|
242
|
-
}
|
|
243
|
-
function flushTextBuffer(activeSession) {
|
|
244
|
-
const textBuffer = activeSession?.pendingTextEvent;
|
|
245
|
-
if (!activeSession || !textBuffer || !textBuffer.text)
|
|
246
|
-
return;
|
|
247
|
-
sendAgentEvent({
|
|
248
|
-
state: 'delta',
|
|
249
|
-
runId: textBuffer.runId,
|
|
250
|
-
seq: textBuffer.seq,
|
|
251
|
-
text: textBuffer.text,
|
|
252
|
-
thinking: textBuffer.thinking || undefined,
|
|
253
|
-
});
|
|
254
|
-
activeSession.pendingTextEvent = null;
|
|
255
|
-
}
|
|
256
|
-
adapter.on('agentEvent', (event) => {
|
|
257
|
-
const activeSession = runtime.sessions.get(sessionId);
|
|
258
|
-
const runPhase = runPhaseFromAgentEvent(event);
|
|
259
|
-
const isTerminalEvent = event.state === 'final' || event.state === 'error' || event.state === 'aborted';
|
|
260
|
-
if (activeSession) {
|
|
261
|
-
activeSession.nextEventSeq = event.seq + 1;
|
|
262
|
-
if (!isTerminalEvent)
|
|
263
|
-
activeSession.currentRunId = event.runId;
|
|
264
|
-
if (!isTerminalEvent && runPhase) {
|
|
265
|
-
activeSession.currentRunPhase = runPhase;
|
|
266
|
-
ensureActivityHeartbeat(activeSession);
|
|
267
|
-
}
|
|
268
|
-
if (event.agentSessionId)
|
|
269
|
-
activeSession.agentSessionId = event.agentSessionId;
|
|
270
|
-
}
|
|
271
|
-
runtime.managerRuntime?.noteAgentEvent(sessionId, event);
|
|
272
|
-
if (event.state !== 'delta') {
|
|
273
|
-
reportLog({
|
|
274
|
-
level: 'info',
|
|
275
|
-
sessionId,
|
|
276
|
-
wsEvent: `agent.${event.state}`,
|
|
277
|
-
wsDirection: 'out',
|
|
278
|
-
metadata: { runId: event.runId, seq: event.seq, agentType },
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
if (event.state === 'delta' && event.text && !event.thinking) {
|
|
282
|
-
appendMessage(sessionId, {
|
|
283
|
-
id: `agent-${event.runId}-${event.seq}`,
|
|
284
|
-
sessionId,
|
|
285
|
-
role: 'agent',
|
|
286
|
-
ts: Date.now(),
|
|
287
|
-
payload: event.text,
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
else if (event.state === 'tool-call' || event.state === 'tool-result') {
|
|
291
|
-
appendMessage(sessionId, {
|
|
292
|
-
id: `agent-${event.runId}-${event.seq}`,
|
|
293
|
-
sessionId,
|
|
294
|
-
role: 'agent',
|
|
295
|
-
ts: Date.now(),
|
|
296
|
-
payload: JSON.stringify({
|
|
297
|
-
v: 1,
|
|
298
|
-
type: event.state === 'tool-call' ? 'tool_use' : 'tool_result',
|
|
299
|
-
name: event.name,
|
|
300
|
-
status: event.state === 'tool-call' ? 'running' : 'completed',
|
|
301
|
-
detailRef: { runId: event.runId, sourceSeq: event.seq },
|
|
302
|
-
args: event.args,
|
|
303
|
-
result: event.result,
|
|
304
|
-
}),
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
else if (event.state === 'approval-pending') {
|
|
308
|
-
appendMessage(sessionId, {
|
|
309
|
-
id: `agent-${event.runId || 'run'}-${event.seq}`,
|
|
310
|
-
sessionId,
|
|
311
|
-
role: 'agent',
|
|
312
|
-
ts: Date.now(),
|
|
313
|
-
payload: buildApprovalPendingPayload(event.approval),
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
else if ((event.state === 'error' || event.state === 'aborted') && event.message) {
|
|
317
|
-
appendMessage(sessionId, {
|
|
318
|
-
id: `agent-${event.runId || 'run'}-${event.seq}`,
|
|
319
|
-
sessionId,
|
|
320
|
-
role: 'agent',
|
|
321
|
-
ts: Date.now(),
|
|
322
|
-
payload: event.message,
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
const runKey = `${sessionId}:${event.runId}`;
|
|
326
|
-
if (event.state === 'delta' && !event.thinking && event.text) {
|
|
327
|
-
runtime.runTextAcc.set(runKey, (runtime.runTextAcc.get(runKey) ?? '') + event.text);
|
|
328
|
-
}
|
|
329
|
-
if (event.agentSessionId && event.agentSessionId !== emittedAgentSessionId) {
|
|
330
|
-
emittedAgentSessionId = event.agentSessionId;
|
|
331
|
-
runtime.nativeFusion?.noteManagedSourceSession(sessionId, getNativeSourceAgentType(agentType, event.source), event.agentSessionId);
|
|
332
|
-
if (activeSession) {
|
|
333
|
-
recordSession({
|
|
334
|
-
sessionId,
|
|
335
|
-
agentType,
|
|
336
|
-
workDir: activeSession.workDir,
|
|
337
|
-
agentSessionId: event.agentSessionId,
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
runtime.client.sendEvent({
|
|
341
|
-
type: 'event',
|
|
342
|
-
event: 'session.update',
|
|
343
|
-
payload: {
|
|
344
|
-
session: {
|
|
345
|
-
id: sessionId,
|
|
346
|
-
agentType,
|
|
347
|
-
agentSessionId: event.agentSessionId,
|
|
348
|
-
},
|
|
349
|
-
},
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
if (event.state === 'delta') {
|
|
353
|
-
const text = event.text ?? '';
|
|
354
|
-
if (!text)
|
|
355
|
-
return;
|
|
356
|
-
const thinking = Boolean(event.thinking);
|
|
357
|
-
if (activeSession?.pendingTextEvent &&
|
|
358
|
-
(activeSession.pendingTextEvent.runId !== event.runId ||
|
|
359
|
-
activeSession.pendingTextEvent.thinking !== thinking)) {
|
|
360
|
-
flushTextBuffer(activeSession);
|
|
361
|
-
}
|
|
362
|
-
if (activeSession && !activeSession.pendingTextEvent) {
|
|
363
|
-
activeSession.pendingTextEvent = { runId: event.runId, seq: event.seq, text: '', thinking };
|
|
364
|
-
}
|
|
365
|
-
if (activeSession?.pendingTextEvent) {
|
|
366
|
-
activeSession.pendingTextEvent.text += text;
|
|
367
|
-
activeSession.pendingTextEvent.seq = event.seq;
|
|
368
|
-
}
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
let extra = {};
|
|
372
|
-
if (event.state === 'final') {
|
|
373
|
-
flushTextBuffer(activeSession);
|
|
374
|
-
const accumulated = runtime.runTextAcc.get(runKey) ?? '';
|
|
375
|
-
if (accumulated)
|
|
376
|
-
extra = { messageSummary: extractSummary(accumulated) };
|
|
377
|
-
runtime.runTextAcc.delete(runKey);
|
|
378
|
-
}
|
|
379
|
-
else if (event.state === 'error' || event.state === 'aborted') {
|
|
380
|
-
flushTextBuffer(activeSession);
|
|
381
|
-
runtime.runTextAcc.delete(runKey);
|
|
382
|
-
}
|
|
383
|
-
else if (event.state === 'tool-call' || event.state === 'tool-result' || event.state === 'approval-pending') {
|
|
384
|
-
flushTextBuffer(activeSession);
|
|
385
|
-
}
|
|
386
|
-
if (isTerminalEvent &&
|
|
387
|
-
activeSession?.currentRunId === event.runId) {
|
|
388
|
-
activeSession.currentRunId = null;
|
|
389
|
-
activeSession.currentRunPhase = null;
|
|
390
|
-
activeSession.nextEventSeq = 0;
|
|
391
|
-
stopActivityHeartbeat(activeSession);
|
|
392
|
-
runtime.chatQueue?.noteTerminal(sessionId);
|
|
393
|
-
}
|
|
394
|
-
sendAgentEvent(event, extra);
|
|
395
|
-
});
|
|
396
|
-
adapter.on('error', (error) => {
|
|
397
|
-
console.error(`[chat.send] adapter error sessionId=${sessionId} agentType=${agentType}: ${error.message}`);
|
|
398
|
-
runtime.sessions.delete(sessionId);
|
|
399
|
-
runtime.chatQueue?.noteTerminal(sessionId);
|
|
400
|
-
runtime.client.sendEvent({
|
|
401
|
-
type: 'event',
|
|
402
|
-
event: 'agent',
|
|
403
|
-
payload: { state: 'error', sessionId, message: error.message, runId: '', seq: 0 },
|
|
404
|
-
});
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
function rememberProcessedReqId(runtime, reqId) {
|
|
408
|
-
runtime.processedReqIds.add(reqId);
|
|
409
|
-
if (runtime.processedReqIds.size > 1000) {
|
|
410
|
-
const first = runtime.processedReqIds.values().next().value;
|
|
411
|
-
runtime.processedReqIds.delete(first);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
async function disposeSession(session) {
|
|
415
|
-
if (session.heartbeatTimer) {
|
|
416
|
-
clearInterval(session.heartbeatTimer);
|
|
417
|
-
session.heartbeatTimer = null;
|
|
418
|
-
}
|
|
419
|
-
session.adapter.removeAllListeners();
|
|
420
|
-
await session.adapter.stop().catch(() => { });
|
|
421
|
-
}
|
|
422
|
-
async function createActiveSession(runtime, sessionId, agentType, resolvedWorkDir, incomingAgentSid, externalChannel, externalReplyTarget) {
|
|
423
|
-
runtime.evictIdleSessions();
|
|
424
|
-
const adapter = createAgent(agentType);
|
|
425
|
-
if (!adapter)
|
|
426
|
-
throw new Error(`Unsupported agent: ${agentType}`);
|
|
427
|
-
configureAdapterForSession(adapter, sessionId, agentType, externalChannel, externalReplyTarget);
|
|
428
|
-
await adapter.start(sessionId, resolvedWorkDir, incomingAgentSid);
|
|
429
|
-
const session = {
|
|
430
|
-
adapter,
|
|
431
|
-
workDir: resolvedWorkDir,
|
|
432
|
-
agentType,
|
|
433
|
-
agentSessionId: incomingAgentSid ?? null,
|
|
434
|
-
lastActiveAt: Date.now(),
|
|
435
|
-
currentRunId: null,
|
|
436
|
-
currentRunPhase: null,
|
|
437
|
-
nextEventSeq: 0,
|
|
438
|
-
heartbeatSeq: 0,
|
|
439
|
-
heartbeatTimer: null,
|
|
440
|
-
pendingTextEvent: null,
|
|
441
|
-
externalChannel: externalChannel ?? null,
|
|
442
|
-
externalReplyTarget: externalReplyTarget ?? null,
|
|
443
|
-
externalChannelEnv: {
|
|
444
|
-
...managedProviderEnv(agentType),
|
|
445
|
-
...externalChannelEnv(sessionId, externalChannel, externalReplyTarget),
|
|
446
|
-
},
|
|
447
|
-
};
|
|
448
|
-
runtime.sessions.set(sessionId, session);
|
|
449
|
-
bindAdapterEvents(runtime, sessionId, agentType, adapter);
|
|
450
|
-
return session;
|
|
451
|
-
}
|
|
452
|
-
function emitSyntheticAbort(runtime, sessionId) {
|
|
453
|
-
const session = runtime.sessions.get(sessionId);
|
|
454
|
-
const runId = session?.currentRunId;
|
|
455
|
-
if (!session || !runId)
|
|
456
|
-
return;
|
|
457
|
-
const seq = session.nextEventSeq;
|
|
458
|
-
runtime.runTextAcc.delete(`${sessionId}:${runId}`);
|
|
459
|
-
session.pendingTextEvent = null;
|
|
460
|
-
session.currentRunId = null;
|
|
461
|
-
session.currentRunPhase = null;
|
|
462
|
-
session.nextEventSeq = 0;
|
|
463
|
-
if (session.heartbeatTimer) {
|
|
464
|
-
clearInterval(session.heartbeatTimer);
|
|
465
|
-
session.heartbeatTimer = null;
|
|
466
|
-
}
|
|
467
|
-
runtime.client.sendAgentEvent({
|
|
468
|
-
type: 'event',
|
|
469
|
-
event: 'agent',
|
|
470
|
-
payload: { state: 'aborted', sessionId, runId, seq },
|
|
471
|
-
seq,
|
|
472
|
-
id: `agent-evt-${runId}-${seq}`,
|
|
473
|
-
});
|
|
474
|
-
runtime.chatQueue?.noteTerminal(sessionId);
|
|
475
|
-
}
|
|
476
|
-
export async function handleChatSend(runtime, req) {
|
|
477
|
-
if (runtime.processedReqIds.has(req.id)) {
|
|
478
|
-
runtime.client.sendRes({ type: 'res', id: req.id, ok: true });
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
rememberProcessedReqId(runtime, req.id);
|
|
482
|
-
const { sessionId, text, agentType, workDir, agentSessionId: incomingAgentSid, modelId, managerDefaultWorkerAgentType, managerDefaultWorkerModelId, reasoningEffort, clientMessageId, sessionListProjection, waitForDispatch, responseId, } = req.params;
|
|
483
|
-
const replyId = responseId || req.id;
|
|
484
|
-
mergeProjectedSessions(sessionListProjection);
|
|
485
|
-
const incomingExternalChannel = normalizeExternalChannel(req.params.externalChannel);
|
|
486
|
-
const incomingReplyTarget = typeof req.params.replyTarget === 'string'
|
|
487
|
-
? req.params.replyTarget.trim()
|
|
488
|
-
: '';
|
|
489
|
-
if (!sessionId || !text) {
|
|
490
|
-
runtime.processedReqIds.delete(req.id);
|
|
491
|
-
runtime.client.sendRes({ type: 'res', id: replyId, ok: false, error: 'sessionId and text are required' });
|
|
492
|
-
return;
|
|
493
|
-
}
|
|
494
|
-
const requestedAgentType = agentType;
|
|
495
|
-
if (requestedAgentType === 'manager') {
|
|
496
|
-
runtime.managerRuntime?.setManagerWorkerDefaults(sessionId, managerDefaultWorkerAgentType ?? null, managerDefaultWorkerModelId ?? null);
|
|
497
|
-
}
|
|
498
|
-
const resolvedReasoningEffort = (requestedAgentType === 'claude' || requestedAgentType === 'codex') &&
|
|
499
|
-
typeof reasoningEffort === 'string' &&
|
|
500
|
-
reasoningEffort.trim()
|
|
501
|
-
? reasoningEffort.trim()
|
|
502
|
-
: undefined;
|
|
503
|
-
const resolvedWorkDir = runtime.resolvePath(maybeResolveClaudeImportedWorkDir(requestedAgentType, workDir || os.homedir(), incomingAgentSid) || os.homedir());
|
|
504
|
-
const displayWorkDir = resolvedWorkDir;
|
|
505
|
-
const normalizedAttachments = normalizeChatAttachments(req.params.attachments);
|
|
506
|
-
const materializedAttachments = normalizedAttachments?.length
|
|
507
|
-
? await materializeRemoteChatAttachments({ text, attachments: normalizedAttachments, workDir: resolvedWorkDir })
|
|
508
|
-
: { text, attachments: normalizedAttachments, localized: false };
|
|
509
|
-
let session = runtime.sessions.get(sessionId);
|
|
510
|
-
if (session) {
|
|
511
|
-
session.lastActiveAt = Date.now();
|
|
512
|
-
const sessionDrifted = session.agentType !== requestedAgentType ||
|
|
513
|
-
session.workDir !== resolvedWorkDir ||
|
|
514
|
-
JSON.stringify(session.externalChannel ?? null) !== JSON.stringify(incomingExternalChannel ?? null);
|
|
515
|
-
if (sessionDrifted) {
|
|
516
|
-
runtime.sessions.delete(sessionId);
|
|
517
|
-
try {
|
|
518
|
-
await disposeSession(session);
|
|
519
|
-
}
|
|
520
|
-
catch {
|
|
521
|
-
runtime.processedReqIds.delete(req.id);
|
|
522
|
-
}
|
|
523
|
-
session = undefined;
|
|
524
|
-
}
|
|
525
|
-
else if (incomingAgentSid && session.agentSessionId !== incomingAgentSid) {
|
|
526
|
-
try {
|
|
527
|
-
await session.adapter.resume(incomingAgentSid);
|
|
528
|
-
session.agentSessionId = incomingAgentSid;
|
|
529
|
-
}
|
|
530
|
-
catch {
|
|
531
|
-
runtime.sessions.delete(sessionId);
|
|
532
|
-
try {
|
|
533
|
-
await disposeSession(session);
|
|
534
|
-
}
|
|
535
|
-
catch {
|
|
536
|
-
runtime.processedReqIds.delete(req.id);
|
|
537
|
-
}
|
|
538
|
-
session = undefined;
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
if (!session) {
|
|
543
|
-
try {
|
|
544
|
-
session = await createActiveSession(runtime, sessionId, requestedAgentType, resolvedWorkDir, incomingAgentSid, incomingExternalChannel, incomingReplyTarget);
|
|
545
|
-
}
|
|
546
|
-
catch (err) {
|
|
547
|
-
const message = err instanceof Error && err.message.startsWith('Unsupported agent:')
|
|
548
|
-
? err.message
|
|
549
|
-
: `Failed to start ${agentType}: ${err instanceof Error ? err.message : String(err)}`;
|
|
550
|
-
console.error(`[chat.send] start failed reqId=${req.id} sessionId=${sessionId} agentType=${agentType} workDir=${resolvedWorkDir} agentSessionId=${incomingAgentSid ?? ''}: ${message}`);
|
|
551
|
-
runtime.client.sendEvent({
|
|
552
|
-
type: 'event',
|
|
553
|
-
event: 'agent',
|
|
554
|
-
payload: {
|
|
555
|
-
state: 'error',
|
|
556
|
-
sessionId,
|
|
557
|
-
message,
|
|
558
|
-
runId: '',
|
|
559
|
-
seq: 0,
|
|
560
|
-
},
|
|
561
|
-
});
|
|
562
|
-
runtime.processedReqIds.delete(req.id);
|
|
563
|
-
runtime.client.sendRes({
|
|
564
|
-
type: 'res',
|
|
565
|
-
id: replyId,
|
|
566
|
-
ok: false,
|
|
567
|
-
error: message,
|
|
568
|
-
});
|
|
569
|
-
return;
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
const userEnvelope = {
|
|
573
|
-
id: clientMessageId ?? `user-${req.id}`,
|
|
574
|
-
sessionId,
|
|
575
|
-
role: 'user',
|
|
576
|
-
ts: Date.now(),
|
|
577
|
-
payload: buildUserMessagePayload(materializedAttachments.text, materializedAttachments.attachments),
|
|
578
|
-
};
|
|
579
|
-
reportLog({
|
|
580
|
-
level: 'info',
|
|
581
|
-
sessionId,
|
|
582
|
-
wsEvent: 'chat.send.start',
|
|
583
|
-
metadata: { reqId: req.id, agentType: requestedAgentType, modelId, reasoningEffort: resolvedReasoningEffort },
|
|
584
|
-
});
|
|
585
|
-
const markAccepted = () => {
|
|
586
|
-
sendSessionUpdateEvent(runtime, {
|
|
587
|
-
sessionId,
|
|
588
|
-
agentType: requestedAgentType,
|
|
589
|
-
workDir: displayWorkDir,
|
|
590
|
-
agentSessionId: session.agentSessionId ?? incomingAgentSid ?? null,
|
|
591
|
-
modelId,
|
|
592
|
-
});
|
|
593
|
-
// Some adapters (notably Codex app-server) emit `start` synchronously
|
|
594
|
-
// during `send()` before chat.send has acknowledged dispatch. Do not wipe
|
|
595
|
-
// that live run state here, or later chat.enqueue calls will think the
|
|
596
|
-
// session is idle and bypass the daemon queue.
|
|
597
|
-
if (!session.currentRunId) {
|
|
598
|
-
session.nextEventSeq = 0;
|
|
599
|
-
}
|
|
600
|
-
runtime.nativeFusion?.registerManagedSend({
|
|
601
|
-
sessionId,
|
|
602
|
-
agentType: requestedAgentType,
|
|
603
|
-
sourceAgentType: getNativeSourceAgentType(requestedAgentType, modelId),
|
|
604
|
-
canonicalMessageId: clientMessageId ?? null,
|
|
605
|
-
sourceSessionKey: session.agentSessionId ?? incomingAgentSid ?? null,
|
|
606
|
-
text: materializedAttachments.text,
|
|
607
|
-
});
|
|
608
|
-
appendMessage(sessionId, userEnvelope);
|
|
609
|
-
sendSessionMessageEvent(runtime, userEnvelope, {
|
|
610
|
-
agentType: requestedAgentType,
|
|
611
|
-
workDir: displayWorkDir,
|
|
612
|
-
agentSessionId: session.agentSessionId ?? incomingAgentSid ?? null,
|
|
613
|
-
modelId,
|
|
614
|
-
});
|
|
615
|
-
};
|
|
616
|
-
const handleSendFailure = async (err, respondToReq) => {
|
|
617
|
-
const message = formatAgentSendFailure(requestedAgentType, err);
|
|
618
|
-
console.error(`[chat.send] send failed reqId=${req.id} sessionId=${sessionId} agentType=${agentType} workDir=${resolvedWorkDir} agentSessionId=${session.agentSessionId ?? incomingAgentSid ?? ''}: ${message}`);
|
|
619
|
-
runtime.sessions.delete(sessionId);
|
|
620
|
-
try {
|
|
621
|
-
await disposeSession(session);
|
|
622
|
-
}
|
|
623
|
-
catch { /* best-effort cleanup */ }
|
|
624
|
-
runtime.client.sendEvent({
|
|
625
|
-
type: 'event',
|
|
626
|
-
event: 'agent',
|
|
627
|
-
payload: {
|
|
628
|
-
state: 'error',
|
|
629
|
-
sessionId,
|
|
630
|
-
message,
|
|
631
|
-
runId: '',
|
|
632
|
-
seq: 0,
|
|
633
|
-
},
|
|
634
|
-
});
|
|
635
|
-
if (!respondToReq) {
|
|
636
|
-
const errorEnvelope = {
|
|
637
|
-
id: `agent-error-${req.id}-${Date.now()}`,
|
|
638
|
-
sessionId,
|
|
639
|
-
role: 'agent',
|
|
640
|
-
ts: Date.now(),
|
|
641
|
-
payload: message,
|
|
642
|
-
};
|
|
643
|
-
appendMessage(sessionId, errorEnvelope);
|
|
644
|
-
sendSessionMessageEvent(runtime, errorEnvelope, {
|
|
645
|
-
agentType: requestedAgentType,
|
|
646
|
-
workDir: displayWorkDir,
|
|
647
|
-
agentSessionId: session.agentSessionId ?? incomingAgentSid ?? null,
|
|
648
|
-
modelId,
|
|
649
|
-
});
|
|
650
|
-
}
|
|
651
|
-
if (respondToReq) {
|
|
652
|
-
runtime.processedReqIds.delete(req.id);
|
|
653
|
-
runtime.client.sendRes({
|
|
654
|
-
type: 'res',
|
|
655
|
-
id: replyId,
|
|
656
|
-
ok: false,
|
|
657
|
-
error: message,
|
|
658
|
-
});
|
|
659
|
-
}
|
|
660
|
-
};
|
|
661
|
-
if (waitForDispatch) {
|
|
662
|
-
try {
|
|
663
|
-
const attachments = materializedAttachments.attachments;
|
|
664
|
-
if (attachments?.length)
|
|
665
|
-
await session.adapter.send(materializedAttachments.text, modelId, resolvedReasoningEffort, attachments);
|
|
666
|
-
else if (resolvedReasoningEffort)
|
|
667
|
-
await session.adapter.send(materializedAttachments.text, modelId, resolvedReasoningEffort);
|
|
668
|
-
else
|
|
669
|
-
await session.adapter.send(materializedAttachments.text, modelId);
|
|
670
|
-
reportLog({
|
|
671
|
-
level: 'info',
|
|
672
|
-
sessionId,
|
|
673
|
-
wsEvent: 'chat.send.done',
|
|
674
|
-
metadata: { reqId: req.id },
|
|
675
|
-
});
|
|
676
|
-
}
|
|
677
|
-
catch (err) {
|
|
678
|
-
await handleSendFailure(err, true);
|
|
679
|
-
return;
|
|
680
|
-
}
|
|
681
|
-
markAccepted();
|
|
682
|
-
runtime.client.sendRes({
|
|
683
|
-
type: 'res',
|
|
684
|
-
id: replyId,
|
|
685
|
-
ok: true,
|
|
686
|
-
...(materializedAttachments.localized ? { payload: { localizedAttachments: true } } : {}),
|
|
687
|
-
});
|
|
688
|
-
reportLog({
|
|
689
|
-
level: 'info',
|
|
690
|
-
sessionId,
|
|
691
|
-
wsEvent: 'chat.send.res',
|
|
692
|
-
metadata: { reqId: req.id, ok: true },
|
|
693
|
-
});
|
|
694
|
-
return;
|
|
695
|
-
}
|
|
696
|
-
markAccepted();
|
|
697
|
-
runtime.client.sendRes({
|
|
698
|
-
type: 'res',
|
|
699
|
-
id: replyId,
|
|
700
|
-
ok: true,
|
|
701
|
-
...(materializedAttachments.localized ? { payload: { localizedAttachments: true } } : {}),
|
|
702
|
-
});
|
|
703
|
-
reportLog({
|
|
704
|
-
level: 'info',
|
|
705
|
-
sessionId,
|
|
706
|
-
wsEvent: 'chat.send.res',
|
|
707
|
-
metadata: { reqId: req.id, ok: true },
|
|
708
|
-
});
|
|
709
|
-
const asyncAttachments = materializedAttachments.attachments;
|
|
710
|
-
const sendPromise = asyncAttachments?.length
|
|
711
|
-
? session.adapter.send(materializedAttachments.text, modelId, resolvedReasoningEffort, asyncAttachments)
|
|
712
|
-
: resolvedReasoningEffort
|
|
713
|
-
? session.adapter.send(materializedAttachments.text, modelId, resolvedReasoningEffort)
|
|
714
|
-
: session.adapter.send(materializedAttachments.text, modelId);
|
|
715
|
-
void sendPromise
|
|
716
|
-
.then(() => {
|
|
717
|
-
reportLog({
|
|
718
|
-
level: 'info',
|
|
719
|
-
sessionId,
|
|
720
|
-
wsEvent: 'chat.send.done',
|
|
721
|
-
metadata: { reqId: req.id },
|
|
722
|
-
});
|
|
723
|
-
})
|
|
724
|
-
.catch((err) => {
|
|
725
|
-
void handleSendFailure(err, false);
|
|
726
|
-
});
|
|
727
|
-
}
|
|
728
|
-
export async function handleChatAbort(runtime, req) {
|
|
729
|
-
const { sessionId } = req.params;
|
|
730
|
-
const session = runtime.sessions.get(sessionId);
|
|
731
|
-
if (session) {
|
|
732
|
-
try {
|
|
733
|
-
await session.adapter.stop();
|
|
734
|
-
}
|
|
735
|
-
catch { /* best-effort: still emit synthetic abort below */ }
|
|
736
|
-
emitSyntheticAbort(runtime, sessionId);
|
|
737
|
-
runtime.activityPublisher?.publish(sessionId, null);
|
|
738
|
-
}
|
|
739
|
-
else {
|
|
740
|
-
const params = req.params;
|
|
741
|
-
if (params.agentType === 'codex' && params.agentSessionId && runtime.nativeFusion) {
|
|
742
|
-
const ok = await runtime.nativeFusion.interruptCodexThread({
|
|
743
|
-
threadId: params.agentSessionId,
|
|
744
|
-
workDir: params.workDir,
|
|
745
|
-
});
|
|
746
|
-
if (ok)
|
|
747
|
-
runtime.activityPublisher?.publish(sessionId, null);
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
runtime.client.sendRes({ type: 'res', id: req.id, ok: true });
|
|
751
|
-
}
|
|
1
|
+
import F from"node:os";import{createAgent as H}from"../../agents/adapter.js";import{buildApprovalPendingPayload as O,buildUserMessagePayload as G}from"@shennian/wire";import{reportLog as x}from"../../log-reporter.js";import{lookupClaudeTranscriptCwd as U}from"../../native-fusion/parsers.js";import{appendMessage as A,recordSession as v}from"../store.js";import{mergeProjectedSessions as J}from"../projection.js";import{getManagerRuntimeService as Q}from"../../manager/runtime.js";import{buildManagedAgentEnv as K}from"../../agents/config-status.js";import{materializeRemoteChatAttachments as V}from"../remote-attachments.js";function X(t){if(!Array.isArray(t))return;const e=t.map(a=>{if(!a||typeof a!="object")return null;const s=a,i=typeof s.path=="string"?s.path:"",f=typeof s.name=="string"?s.name:"",l=typeof s.mimeType=="string"?s.mimeType:"";if(!i||!f||!l)return null;const d=typeof s.previewData=="string"&&s.previewData.trim()?s.previewData.trim():void 0;return{path:i,name:f,mimeType:l,kind:l.startsWith("image/")?"image":"file",...d?{previewData:d}:{}}}).filter(a=>a!=null);return e.length?e:void 0}function Y(t){const e=t.indexOf(`
|
|
2
|
+
`),a=e>0?Math.min(e,80):Math.min(t.length,80);return t.slice(0,a)}function Z(t,e,a={}){if(t.state==="tool-call"||t.state==="tool-result"){const s={runId:t.runId,sourceSeq:t.seq};return{state:t.state,runId:t.runId,seq:t.seq,sessionId:e,detailRef:s,...t.name?{name:t.name}:{},...t.source?{source:t.source}:{},...t.agentSessionId?{agentSessionId:t.agentSessionId}:{},...a}}return{...t,sessionId:e,...a}}const ee=3e4;function te(t){return t.state==="heartbeat"?t.runPhase??null:t.state==="tool-call"||t.state==="tool-result"?"tool_running":t.state==="approval-pending"?"waiting_approval":t.state==="delta"?t.thinking?"thinking":"streaming_text":t.state==="init"||t.state==="start"?"thinking":null}function ne(t,e){const a=e instanceof Error?e.message:String(e);return t==="pi"&&(a.includes("429")||a.includes("daily_quota_exceeded")||a.includes("nian_quota_exceeded"))?a.includes("too quickly")||a.includes("per minute")?"Nian \u8BF7\u6C42\u8FC7\u4E8E\u9891\u7E41\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002":"Nian \u4ECA\u65E5\u989D\u5EA6\u5DF2\u7528\u5B8C\uFF0C\u6B21\u65E5\u81EA\u52A8\u6062\u590D\u3002":`Agent send failed: ${a}`}function L(t,e){return t!=="manager"?t:e==="claude"?"claude":"codex"}function z(t,e,a){t.client.sendEvent({type:"event",event:"session.message",payload:{sessionId:e.sessionId,message:e,session:{id:e.sessionId,agentType:a.agentType,agentSessionId:a.agentSessionId??null,modelId:a.modelId??null,workDir:a.workDir,status:"active",externalChannel:j(t,e.sessionId,a.agentType)}}})}function ae(t,e){v({sessionId:e.sessionId,agentType:e.agentType,workDir:e.workDir,agentSessionId:e.agentSessionId??null,modelId:e.modelId??null}),t.client.sendEvent({type:"event",event:"session.update",payload:{session:{id:e.sessionId,agentType:e.agentType,agentSessionId:e.agentSessionId??null,modelId:e.modelId??null,workDir:e.workDir,status:"active",externalChannel:j(t,e.sessionId,e.agentType)}}})}function re(t){if(!t||typeof t!="object")return null;const e=t;return{configured:e.configured===void 0?void 0:!!e.configured,connected:!!e.connected,type:typeof e.type=="string"?e.type:null,channelId:typeof e.channelId=="string"?e.channelId:null,name:typeof e.name=="string"?e.name:null,canReply:e.canReply===void 0||e.canReply===null?null:!!e.canReply,systemPrompt:typeof e.systemPrompt=="string"?e.systemPrompt:null,wechatRpaSource:typeof e.wechatRpaSource=="string"?e.wechatRpaSource:null,wechatRpaGroups:Array.isArray(e.wechatRpaGroups)?e.wechatRpaGroups.map(a=>({name:String(a?.name||"").trim()})).filter(a=>a.name):null,pollIntervalMs:Number.isFinite(e.pollIntervalMs)?Number(e.pollIntervalMs):null,recentLimit:Number.isFinite(e.recentLimit)?Number(e.recentLimit):null,idleSeconds:Number.isFinite(e.idleSeconds)?Number(e.idleSeconds):null,forceForeground:e.forceForeground===void 0||e.forceForeground===null?null:!!e.forceForeground,noRestore:e.noRestore===void 0||e.noRestore===null?null:!!e.noRestore,downloadAttachments:e.downloadAttachments===void 0||e.downloadAttachments===null?null:!!e.downloadAttachments,downloadAttachmentsDir:typeof e.downloadAttachmentsDir=="string"?e.downloadAttachmentsDir:null}}function se(t){return!!(t?.configured??t?.connected)}function B(t){return K(t)}function W(t,e,a){return se(e)?{...Q()?.getInjectedEnv(t,null,process.cwd(),"external")??{},SHENNIAN_EXTERNAL_SESSION_ID:t,SHENNIAN_MANAGER_SESSION_ID:t,...a?.trim()?{SHENNIAN_EXTERNAL_REPLY_TARGET:a.trim()}:{}}:{}}function oe(t,e,a,s,i){t.configure?.({sessionId:e,externalChannel:s??null,env:{...B(a),...W(e,s,i)}})}function j(t,e,a){const s=t.sessions.get(e);return s?.externalChannel?s.externalChannel:a==="manager"?t.managerRuntime?.getExternalChannelStatus(e)??null:null}function ie(t,e,a){if(t!=="claude"||!a)return e;const s=e.trim();return!!s&&s.startsWith("-")&&!s.includes("/")?U(a)??e:e}function le(t,e,a,s){let i=null;function f(n,r={}){t.client.sendAgentEvent({type:"event",event:"agent",payload:Z(n,e,r),seq:n.seq,id:`agent-evt-${n.runId}-${n.seq}`})}function l(n){n?.heartbeatTimer&&(clearInterval(n.heartbeatTimer),n.heartbeatTimer=null)}function d(n){if(!n.currentRunId||!n.currentRunPhase)return;const r=n.heartbeatSeq++;t.client.sendAgentEvent({type:"event",event:"agent",payload:{state:"heartbeat",sessionId:e,runId:n.currentRunId,seq:r,runPhase:n.currentRunPhase},seq:r,id:`agent-heartbeat-${n.currentRunId}-${r}-${Date.now()}`})}function I(n){!n||n.heartbeatTimer||(n.heartbeatTimer=setInterval(()=>{d(n)},ee),n.heartbeatTimer.unref?.())}function S(n){const r=n?.pendingTextEvent;!n||!r||!r.text||(f({state:"delta",runId:r.runId,seq:r.seq,text:r.text,thinking:r.thinking||void 0}),n.pendingTextEvent=null)}s.on("agentEvent",n=>{const r=t.sessions.get(e),R=te(n),T=n.state==="final"||n.state==="error"||n.state==="aborted";r&&(r.nextEventSeq=n.seq+1,T||(r.currentRunId=n.runId),!T&&R&&(r.currentRunPhase=R,I(r)),n.agentSessionId&&(r.agentSessionId=n.agentSessionId)),t.managerRuntime?.noteAgentEvent(e,n),n.state!=="delta"&&x({level:"info",sessionId:e,wsEvent:`agent.${n.state}`,wsDirection:"out",metadata:{runId:n.runId,seq:n.seq,agentType:a}}),n.state==="delta"&&n.text&&!n.thinking?A(e,{id:`agent-${n.runId}-${n.seq}`,sessionId:e,role:"agent",ts:Date.now(),payload:n.text}):n.state==="tool-call"||n.state==="tool-result"?A(e,{id:`agent-${n.runId}-${n.seq}`,sessionId:e,role:"agent",ts:Date.now(),payload:JSON.stringify({v:1,type:n.state==="tool-call"?"tool_use":"tool_result",name:n.name,status:n.state==="tool-call"?"running":"completed",detailRef:{runId:n.runId,sourceSeq:n.seq},args:n.args,result:n.result})}):n.state==="approval-pending"?A(e,{id:`agent-${n.runId||"run"}-${n.seq}`,sessionId:e,role:"agent",ts:Date.now(),payload:O(n.approval)}):(n.state==="error"||n.state==="aborted")&&n.message&&A(e,{id:`agent-${n.runId||"run"}-${n.seq}`,sessionId:e,role:"agent",ts:Date.now(),payload:n.message});const m=`${e}:${n.runId}`;if(n.state==="delta"&&!n.thinking&&n.text&&t.runTextAcc.set(m,(t.runTextAcc.get(m)??"")+n.text),n.agentSessionId&&n.agentSessionId!==i&&(i=n.agentSessionId,t.nativeFusion?.noteManagedSourceSession(e,L(a,n.source),n.agentSessionId),r&&v({sessionId:e,agentType:a,workDir:r.workDir,agentSessionId:n.agentSessionId}),t.client.sendEvent({type:"event",event:"session.update",payload:{session:{id:e,agentType:a,agentSessionId:n.agentSessionId}}})),n.state==="delta"){const h=n.text??"";if(!h)return;const k=!!n.thinking;r?.pendingTextEvent&&(r.pendingTextEvent.runId!==n.runId||r.pendingTextEvent.thinking!==k)&&S(r),r&&!r.pendingTextEvent&&(r.pendingTextEvent={runId:n.runId,seq:n.seq,text:"",thinking:k}),r?.pendingTextEvent&&(r.pendingTextEvent.text+=h,r.pendingTextEvent.seq=n.seq);return}let p={};if(n.state==="final"){S(r);const h=t.runTextAcc.get(m)??"";h&&(p={messageSummary:Y(h)}),t.runTextAcc.delete(m)}else n.state==="error"||n.state==="aborted"?(S(r),t.runTextAcc.delete(m)):(n.state==="tool-call"||n.state==="tool-result"||n.state==="approval-pending")&&S(r);T&&r?.currentRunId===n.runId&&(r.currentRunId=null,r.currentRunPhase=null,r.nextEventSeq=0,l(r),t.chatQueue?.noteTerminal(e)),f(n,p)}),s.on("error",n=>{console.error(`[chat.send] adapter error sessionId=${e} agentType=${a}: ${n.message}`),t.sessions.delete(e),t.chatQueue?.noteTerminal(e),t.client.sendEvent({type:"event",event:"agent",payload:{state:"error",sessionId:e,message:n.message,runId:"",seq:0}})})}function de(t,e){if(t.processedReqIds.add(e),t.processedReqIds.size>1e3){const a=t.processedReqIds.values().next().value;t.processedReqIds.delete(a)}}async function q(t){t.heartbeatTimer&&(clearInterval(t.heartbeatTimer),t.heartbeatTimer=null),t.adapter.removeAllListeners(),await t.adapter.stop().catch(()=>{})}async function ce(t,e,a,s,i,f,l){t.evictIdleSessions();const d=H(a);if(!d)throw new Error(`Unsupported agent: ${a}`);oe(d,e,a,f,l),await d.start(e,s,i);const I={adapter:d,workDir:s,agentType:a,agentSessionId:i??null,lastActiveAt:Date.now(),currentRunId:null,currentRunPhase:null,nextEventSeq:0,heartbeatSeq:0,heartbeatTimer:null,pendingTextEvent:null,externalChannel:f??null,externalReplyTarget:l??null,externalChannelEnv:{...B(a),...W(e,f,l)}};return t.sessions.set(e,I),le(t,e,a,d),I}function ue(t,e){const a=t.sessions.get(e),s=a?.currentRunId;if(!a||!s)return;const i=a.nextEventSeq;t.runTextAcc.delete(`${e}:${s}`),a.pendingTextEvent=null,a.currentRunId=null,a.currentRunPhase=null,a.nextEventSeq=0,a.heartbeatTimer&&(clearInterval(a.heartbeatTimer),a.heartbeatTimer=null),t.client.sendAgentEvent({type:"event",event:"agent",payload:{state:"aborted",sessionId:e,runId:s,seq:i},seq:i,id:`agent-evt-${s}-${i}`}),t.chatQueue?.noteTerminal(e)}async function Ae(t,e){if(t.processedReqIds.has(e.id)){t.client.sendRes({type:"res",id:e.id,ok:!0});return}de(t,e.id);const{sessionId:a,text:s,agentType:i,workDir:f,agentSessionId:l,modelId:d,managerDefaultWorkerAgentType:I,managerDefaultWorkerModelId:S,reasoningEffort:n,clientMessageId:r,sessionListProjection:R,waitForDispatch:T,responseId:m}=e.params,p=m||e.id;J(R);const h=re(e.params.externalChannel),k=typeof e.params.replyTarget=="string"?e.params.replyTarget.trim():"";if(!a||!s){t.processedReqIds.delete(e.id),t.client.sendRes({type:"res",id:p,ok:!1,error:"sessionId and text are required"});return}const u=i;u==="manager"&&t.managerRuntime?.setManagerWorkerDefaults(a,I??null,S??null);const y=(u==="claude"||u==="codex")&&typeof n=="string"&&n.trim()?n.trim():void 0,E=t.resolvePath(ie(u,f||F.homedir(),l)||F.homedir()),D=E,$=X(e.params.attachments),g=$?.length?await V({text:s,attachments:$,workDir:E}):{text:s,attachments:$,localized:!1};let o=t.sessions.get(a);if(o){if(o.lastActiveAt=Date.now(),o.agentType!==u||o.workDir!==E||JSON.stringify(o.externalChannel??null)!==JSON.stringify(h??null)){t.sessions.delete(a);try{await q(o)}catch{t.processedReqIds.delete(e.id)}o=void 0}else if(l&&o.agentSessionId!==l)try{await o.adapter.resume(l),o.agentSessionId=l}catch{t.sessions.delete(a);try{await q(o)}catch{t.processedReqIds.delete(e.id)}o=void 0}}if(!o)try{o=await ce(t,a,u,E,l,h,k)}catch(c){const w=c instanceof Error&&c.message.startsWith("Unsupported agent:")?c.message:`Failed to start ${i}: ${c instanceof Error?c.message:String(c)}`;console.error(`[chat.send] start failed reqId=${e.id} sessionId=${a} agentType=${i} workDir=${E} agentSessionId=${l??""}: ${w}`),t.client.sendEvent({type:"event",event:"agent",payload:{state:"error",sessionId:a,message:w,runId:"",seq:0}}),t.processedReqIds.delete(e.id),t.client.sendRes({type:"res",id:p,ok:!1,error:w});return}const N={id:r??`user-${e.id}`,sessionId:a,role:"user",ts:Date.now(),payload:G(g.text,g.attachments)};x({level:"info",sessionId:a,wsEvent:"chat.send.start",metadata:{reqId:e.id,agentType:u,modelId:d,reasoningEffort:y}});const P=()=>{ae(t,{sessionId:a,agentType:u,workDir:D,agentSessionId:o.agentSessionId??l??null,modelId:d}),o.currentRunId||(o.nextEventSeq=0),t.nativeFusion?.registerManagedSend({sessionId:a,agentType:u,sourceAgentType:L(u,d),canonicalMessageId:r??null,sourceSessionKey:o.agentSessionId??l??null,text:g.text}),A(a,N),z(t,N,{agentType:u,workDir:D,agentSessionId:o.agentSessionId??l??null,modelId:d})},_=async(c,w)=>{const b=ne(u,c);console.error(`[chat.send] send failed reqId=${e.id} sessionId=${a} agentType=${i} workDir=${E} agentSessionId=${o.agentSessionId??l??""}: ${b}`),t.sessions.delete(a);try{await q(o)}catch{}if(t.client.sendEvent({type:"event",event:"agent",payload:{state:"error",sessionId:a,message:b,runId:"",seq:0}}),!w){const M={id:`agent-error-${e.id}-${Date.now()}`,sessionId:a,role:"agent",ts:Date.now(),payload:b};A(a,M),z(t,M,{agentType:u,workDir:D,agentSessionId:o.agentSessionId??l??null,modelId:d})}w&&(t.processedReqIds.delete(e.id),t.client.sendRes({type:"res",id:p,ok:!1,error:b}))};if(T){try{const c=g.attachments;c?.length?await o.adapter.send(g.text,d,y,c):y?await o.adapter.send(g.text,d,y):await o.adapter.send(g.text,d),x({level:"info",sessionId:a,wsEvent:"chat.send.done",metadata:{reqId:e.id}})}catch(c){await _(c,!0);return}P(),t.client.sendRes({type:"res",id:p,ok:!0,...g.localized?{payload:{localizedAttachments:!0}}:{}}),x({level:"info",sessionId:a,wsEvent:"chat.send.res",metadata:{reqId:e.id,ok:!0}});return}P(),t.client.sendRes({type:"res",id:p,ok:!0,...g.localized?{payload:{localizedAttachments:!0}}:{}}),x({level:"info",sessionId:a,wsEvent:"chat.send.res",metadata:{reqId:e.id,ok:!0}});const C=g.attachments;(C?.length?o.adapter.send(g.text,d,y,C):y?o.adapter.send(g.text,d,y):o.adapter.send(g.text,d)).then(()=>{x({level:"info",sessionId:a,wsEvent:"chat.send.done",metadata:{reqId:e.id}})}).catch(c=>{_(c,!1)})}async function Te(t,e){const{sessionId:a}=e.params,s=t.sessions.get(a);if(s){try{await s.adapter.stop()}catch{}ue(t,a),t.activityPublisher?.publish(a,null)}else{const i=e.params;i.agentType==="codex"&&i.agentSessionId&&t.nativeFusion&&await t.nativeFusion.interruptCodexThread({threadId:i.agentSessionId,workDir:i.workDir})&&t.activityPublisher?.publish(a,null)}t.client.sendRes({type:"res",id:e.id,ok:!0})}export{Te as handleChatAbort,Ae as handleChatSend};
|