shennian 0.2.49 → 0.2.51
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.
|
@@ -13,6 +13,8 @@ export declare class CustomAgentAdapter extends AgentAdapter {
|
|
|
13
13
|
private seq;
|
|
14
14
|
private stdioProcess;
|
|
15
15
|
private spawnProcess;
|
|
16
|
+
private dispatchGuard;
|
|
17
|
+
private dispatchObservedEvent;
|
|
16
18
|
constructor(name: string, entry: CustomAgentEntry);
|
|
17
19
|
start(sessionId: string, workDir: string, agentSessionId?: string | null): Promise<void>;
|
|
18
20
|
send(text: string, modelId?: string): Promise<void>;
|
|
@@ -25,6 +27,9 @@ export declare class CustomAgentAdapter extends AgentAdapter {
|
|
|
25
27
|
private stopStdioProcess;
|
|
26
28
|
private attachOutputHandlers;
|
|
27
29
|
private handleProtocolEvent;
|
|
30
|
+
private startDispatchGuard;
|
|
31
|
+
private acceptDispatch;
|
|
32
|
+
private rejectDispatch;
|
|
28
33
|
private emitEvent;
|
|
29
34
|
}
|
|
30
35
|
export declare function registerCustomAgent(name: string, entry: CustomAgentEntry): void;
|
|
@@ -4,6 +4,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
4
4
|
import { AgentAdapter, registerAgent } from './adapter.js';
|
|
5
5
|
import { spawnCommandString } from './command-spec.js';
|
|
6
6
|
import { buildAgentProcessEnv } from '../agent-env.js';
|
|
7
|
+
const DISPATCH_GUARD_MS = 3000;
|
|
7
8
|
export class CustomAgentAdapter extends AgentAdapter {
|
|
8
9
|
type;
|
|
9
10
|
command;
|
|
@@ -18,6 +19,8 @@ export class CustomAgentAdapter extends AgentAdapter {
|
|
|
18
19
|
stdioProcess = null;
|
|
19
20
|
// spawn mode: current single-turn process
|
|
20
21
|
spawnProcess = null;
|
|
22
|
+
dispatchGuard = null;
|
|
23
|
+
dispatchObservedEvent = false;
|
|
21
24
|
constructor(name, entry) {
|
|
22
25
|
super();
|
|
23
26
|
this.type = `custom:${name}`;
|
|
@@ -66,6 +69,8 @@ export class CustomAgentAdapter extends AgentAdapter {
|
|
|
66
69
|
// ─── Spawn mode ───────────────────────────────────────────────────────────
|
|
67
70
|
async runSpawn(text, modelId) {
|
|
68
71
|
await this.killSpawnProcess();
|
|
72
|
+
const guard = this.startDispatchGuard();
|
|
73
|
+
this.dispatchObservedEvent = false;
|
|
69
74
|
const args = ['/run', '--workdir', this.workDir ?? process.cwd()];
|
|
70
75
|
if (this.sessionId)
|
|
71
76
|
args.push('--session', this.sessionId);
|
|
@@ -83,8 +88,10 @@ export class CustomAgentAdapter extends AgentAdapter {
|
|
|
83
88
|
if (this.spawnProcess === proc)
|
|
84
89
|
this.spawnProcess = null;
|
|
85
90
|
});
|
|
91
|
+
proc.stdin?.once('error', (error) => this.rejectDispatch(error));
|
|
86
92
|
proc.stdin?.write(text);
|
|
87
93
|
proc.stdin?.end();
|
|
94
|
+
await guard.promise;
|
|
88
95
|
}
|
|
89
96
|
async killSpawnProcess() {
|
|
90
97
|
const proc = this.spawnProcess;
|
|
@@ -116,6 +123,8 @@ export class CustomAgentAdapter extends AgentAdapter {
|
|
|
116
123
|
if (!this.stdioProcess) {
|
|
117
124
|
await this.startStdioProcess();
|
|
118
125
|
}
|
|
126
|
+
const guard = this.startDispatchGuard();
|
|
127
|
+
this.dispatchObservedEvent = false;
|
|
119
128
|
const msg = JSON.stringify({
|
|
120
129
|
method: 'send',
|
|
121
130
|
id: randomUUID(),
|
|
@@ -125,7 +134,9 @@ export class CustomAgentAdapter extends AgentAdapter {
|
|
|
125
134
|
modelId,
|
|
126
135
|
},
|
|
127
136
|
});
|
|
137
|
+
this.stdioProcess?.stdin?.once('error', (error) => this.rejectDispatch(error));
|
|
128
138
|
this.stdioProcess?.stdin?.write(msg + '\n');
|
|
139
|
+
await guard.promise;
|
|
129
140
|
}
|
|
130
141
|
async stopStdioProcess() {
|
|
131
142
|
const proc = this.stdioProcess;
|
|
@@ -165,11 +176,19 @@ export class CustomAgentAdapter extends AgentAdapter {
|
|
|
165
176
|
onClose();
|
|
166
177
|
if (code !== 0 && code !== null) {
|
|
167
178
|
const msg = stderrBuf.trim() || `Agent exited with code ${code}`;
|
|
179
|
+
this.rejectDispatch(new Error(msg));
|
|
168
180
|
this.emitEvent({ state: 'error', message: msg });
|
|
169
181
|
}
|
|
182
|
+
else if (!this.dispatchObservedEvent) {
|
|
183
|
+
this.rejectDispatch(new Error('Custom agent exited without protocol events'));
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
this.acceptDispatch();
|
|
187
|
+
}
|
|
170
188
|
});
|
|
171
189
|
proc.on('error', (err) => {
|
|
172
190
|
onClose();
|
|
191
|
+
this.rejectDispatch(err);
|
|
173
192
|
this.emit('error', err);
|
|
174
193
|
});
|
|
175
194
|
}
|
|
@@ -177,29 +196,70 @@ export class CustomAgentAdapter extends AgentAdapter {
|
|
|
177
196
|
const { state } = event;
|
|
178
197
|
if (!state)
|
|
179
198
|
return;
|
|
199
|
+
this.dispatchObservedEvent = true;
|
|
180
200
|
switch (state) {
|
|
181
201
|
case 'delta':
|
|
202
|
+
this.acceptDispatch();
|
|
182
203
|
this.emitEvent({ state: 'delta', text: event.text ?? '', thinking: event.thinking });
|
|
183
204
|
break;
|
|
184
205
|
case 'final':
|
|
206
|
+
this.acceptDispatch();
|
|
185
207
|
if (event.agentSessionId)
|
|
186
208
|
this.agentSessionId = event.agentSessionId;
|
|
187
209
|
this.emitEvent({ state: 'final', usage: event.usage, agentSessionId: this.agentSessionId ?? undefined });
|
|
188
210
|
break;
|
|
189
211
|
case 'error':
|
|
212
|
+
this.rejectDispatch(new Error(event.message ?? 'unknown error'));
|
|
190
213
|
this.emitEvent({ state: 'error', message: event.message ?? 'unknown error' });
|
|
191
214
|
break;
|
|
192
215
|
case 'tool-call':
|
|
216
|
+
this.acceptDispatch();
|
|
193
217
|
this.emitEvent({ state: 'tool-call', name: event.name ?? '', args: event.args });
|
|
194
218
|
break;
|
|
195
219
|
case 'tool-result':
|
|
220
|
+
this.acceptDispatch();
|
|
196
221
|
this.emitEvent({ state: 'tool-result', name: event.name ?? '', result: event.result ?? '' });
|
|
197
222
|
break;
|
|
198
223
|
case 'notify':
|
|
224
|
+
this.acceptDispatch();
|
|
199
225
|
this.emitEvent({ state: 'notify', text: event.text ?? '', title: event.title, source: event.source });
|
|
200
226
|
break;
|
|
201
227
|
}
|
|
202
228
|
}
|
|
229
|
+
startDispatchGuard() {
|
|
230
|
+
this.acceptDispatch();
|
|
231
|
+
let guard;
|
|
232
|
+
const promise = new Promise((resolve, reject) => {
|
|
233
|
+
guard = {
|
|
234
|
+
settled: false,
|
|
235
|
+
timer: setTimeout(() => this.acceptDispatch(guard), DISPATCH_GUARD_MS),
|
|
236
|
+
resolve,
|
|
237
|
+
reject,
|
|
238
|
+
promise: Promise.resolve(),
|
|
239
|
+
};
|
|
240
|
+
});
|
|
241
|
+
guard.promise = promise;
|
|
242
|
+
this.dispatchGuard = guard;
|
|
243
|
+
return guard;
|
|
244
|
+
}
|
|
245
|
+
acceptDispatch(guard = this.dispatchGuard) {
|
|
246
|
+
if (!guard || guard.settled)
|
|
247
|
+
return;
|
|
248
|
+
guard.settled = true;
|
|
249
|
+
clearTimeout(guard.timer);
|
|
250
|
+
if (this.dispatchGuard === guard)
|
|
251
|
+
this.dispatchGuard = null;
|
|
252
|
+
guard.resolve();
|
|
253
|
+
}
|
|
254
|
+
rejectDispatch(error, guard = this.dispatchGuard) {
|
|
255
|
+
if (!guard || guard.settled)
|
|
256
|
+
return;
|
|
257
|
+
guard.settled = true;
|
|
258
|
+
clearTimeout(guard.timer);
|
|
259
|
+
if (this.dispatchGuard === guard)
|
|
260
|
+
this.dispatchGuard = null;
|
|
261
|
+
guard.reject(error);
|
|
262
|
+
}
|
|
203
263
|
emitEvent(partial) {
|
|
204
264
|
const event = { ...partial, runId: this.runId, seq: this.seq++ };
|
|
205
265
|
this.emit('agentEvent', event);
|
|
@@ -283,7 +283,7 @@ export async function handleChatSend(runtime, req) {
|
|
|
283
283
|
return;
|
|
284
284
|
}
|
|
285
285
|
rememberProcessedReqId(runtime, req.id);
|
|
286
|
-
const { sessionId, text, agentType, workDir, agentSessionId: incomingAgentSid, modelId, clientMessageId, sessionListProjection } = req.params;
|
|
286
|
+
const { sessionId, text, agentType, workDir, agentSessionId: incomingAgentSid, modelId, clientMessageId, sessionListProjection, waitForDispatch } = req.params;
|
|
287
287
|
mergeProjectedSessions(sessionListProjection);
|
|
288
288
|
if (!sessionId || !text) {
|
|
289
289
|
runtime.processedReqIds.delete(req.id);
|
|
@@ -354,23 +354,6 @@ export async function handleChatSend(runtime, req) {
|
|
|
354
354
|
return;
|
|
355
355
|
}
|
|
356
356
|
}
|
|
357
|
-
sendSessionUpdateEvent(runtime, {
|
|
358
|
-
sessionId,
|
|
359
|
-
agentType: requestedAgentType,
|
|
360
|
-
workDir: resolvedWorkDir,
|
|
361
|
-
agentSessionId: session.agentSessionId ?? incomingAgentSid ?? null,
|
|
362
|
-
modelId,
|
|
363
|
-
});
|
|
364
|
-
session.currentRunId = null;
|
|
365
|
-
session.nextEventSeq = 0;
|
|
366
|
-
runtime.nativeFusion?.registerManagedSend({
|
|
367
|
-
sessionId,
|
|
368
|
-
agentType: requestedAgentType,
|
|
369
|
-
sourceAgentType: getNativeSourceAgentType(requestedAgentType, modelId),
|
|
370
|
-
canonicalMessageId: clientMessageId ?? null,
|
|
371
|
-
sourceSessionKey: session.agentSessionId ?? incomingAgentSid ?? null,
|
|
372
|
-
text,
|
|
373
|
-
});
|
|
374
357
|
const userEnvelope = {
|
|
375
358
|
id: clientMessageId ?? `user-${req.id}`,
|
|
376
359
|
sessionId,
|
|
@@ -378,36 +361,39 @@ export async function handleChatSend(runtime, req) {
|
|
|
378
361
|
ts: Date.now(),
|
|
379
362
|
payload: text,
|
|
380
363
|
};
|
|
381
|
-
appendMessage(sessionId, userEnvelope);
|
|
382
|
-
sendSessionMessageEvent(runtime, userEnvelope, {
|
|
383
|
-
agentType: requestedAgentType,
|
|
384
|
-
workDir: resolvedWorkDir,
|
|
385
|
-
agentSessionId: session.agentSessionId ?? incomingAgentSid ?? null,
|
|
386
|
-
modelId,
|
|
387
|
-
});
|
|
388
364
|
reportLog({
|
|
389
365
|
level: 'info',
|
|
390
366
|
sessionId,
|
|
391
367
|
wsEvent: 'chat.send.start',
|
|
392
368
|
metadata: { reqId: req.id, agentType: requestedAgentType, modelId },
|
|
393
369
|
});
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
level: 'info',
|
|
397
|
-
sessionId,
|
|
398
|
-
wsEvent: 'chat.send.res',
|
|
399
|
-
metadata: { reqId: req.id, ok: true },
|
|
400
|
-
});
|
|
401
|
-
void session.adapter.send(text, modelId)
|
|
402
|
-
.then(() => {
|
|
403
|
-
reportLog({
|
|
404
|
-
level: 'info',
|
|
370
|
+
const markAccepted = () => {
|
|
371
|
+
sendSessionUpdateEvent(runtime, {
|
|
405
372
|
sessionId,
|
|
406
|
-
|
|
407
|
-
|
|
373
|
+
agentType: requestedAgentType,
|
|
374
|
+
workDir: resolvedWorkDir,
|
|
375
|
+
agentSessionId: session.agentSessionId ?? incomingAgentSid ?? null,
|
|
376
|
+
modelId,
|
|
408
377
|
});
|
|
409
|
-
|
|
410
|
-
.
|
|
378
|
+
session.currentRunId = null;
|
|
379
|
+
session.nextEventSeq = 0;
|
|
380
|
+
runtime.nativeFusion?.registerManagedSend({
|
|
381
|
+
sessionId,
|
|
382
|
+
agentType: requestedAgentType,
|
|
383
|
+
sourceAgentType: getNativeSourceAgentType(requestedAgentType, modelId),
|
|
384
|
+
canonicalMessageId: clientMessageId ?? null,
|
|
385
|
+
sourceSessionKey: session.agentSessionId ?? incomingAgentSid ?? null,
|
|
386
|
+
text,
|
|
387
|
+
});
|
|
388
|
+
appendMessage(sessionId, userEnvelope);
|
|
389
|
+
sendSessionMessageEvent(runtime, userEnvelope, {
|
|
390
|
+
agentType: requestedAgentType,
|
|
391
|
+
workDir: resolvedWorkDir,
|
|
392
|
+
agentSessionId: session.agentSessionId ?? incomingAgentSid ?? null,
|
|
393
|
+
modelId,
|
|
394
|
+
});
|
|
395
|
+
};
|
|
396
|
+
const handleSendFailure = async (err, respondToReq) => {
|
|
411
397
|
const message = `Agent send failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
412
398
|
console.error(`[chat.send] send failed reqId=${req.id} sessionId=${sessionId} agentType=${agentType} workDir=${resolvedWorkDir} agentSessionId=${session.agentSessionId ?? incomingAgentSid ?? ''}: ${message}`);
|
|
413
399
|
runtime.sessions.delete(sessionId);
|
|
@@ -426,6 +412,59 @@ export async function handleChatSend(runtime, req) {
|
|
|
426
412
|
seq: 0,
|
|
427
413
|
},
|
|
428
414
|
});
|
|
415
|
+
if (respondToReq) {
|
|
416
|
+
runtime.processedReqIds.delete(req.id);
|
|
417
|
+
runtime.client.sendRes({
|
|
418
|
+
type: 'res',
|
|
419
|
+
id: req.id,
|
|
420
|
+
ok: false,
|
|
421
|
+
error: message,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
if (waitForDispatch) {
|
|
426
|
+
try {
|
|
427
|
+
await session.adapter.send(text, modelId);
|
|
428
|
+
reportLog({
|
|
429
|
+
level: 'info',
|
|
430
|
+
sessionId,
|
|
431
|
+
wsEvent: 'chat.send.done',
|
|
432
|
+
metadata: { reqId: req.id },
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
catch (err) {
|
|
436
|
+
await handleSendFailure(err, true);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
markAccepted();
|
|
440
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: true });
|
|
441
|
+
reportLog({
|
|
442
|
+
level: 'info',
|
|
443
|
+
sessionId,
|
|
444
|
+
wsEvent: 'chat.send.res',
|
|
445
|
+
metadata: { reqId: req.id, ok: true },
|
|
446
|
+
});
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
markAccepted();
|
|
450
|
+
runtime.client.sendRes({ type: 'res', id: req.id, ok: true });
|
|
451
|
+
reportLog({
|
|
452
|
+
level: 'info',
|
|
453
|
+
sessionId,
|
|
454
|
+
wsEvent: 'chat.send.res',
|
|
455
|
+
metadata: { reqId: req.id, ok: true },
|
|
456
|
+
});
|
|
457
|
+
void session.adapter.send(text, modelId)
|
|
458
|
+
.then(() => {
|
|
459
|
+
reportLog({
|
|
460
|
+
level: 'info',
|
|
461
|
+
sessionId,
|
|
462
|
+
wsEvent: 'chat.send.done',
|
|
463
|
+
metadata: { reqId: req.id },
|
|
464
|
+
});
|
|
465
|
+
})
|
|
466
|
+
.catch((err) => {
|
|
467
|
+
void handleSendFailure(err, false);
|
|
429
468
|
});
|
|
430
469
|
}
|
|
431
470
|
export async function handleChatAbort(runtime, req) {
|
|
@@ -87,12 +87,14 @@ export class ChatQueueManager {
|
|
|
87
87
|
const active = runtime.sessions.get(params.sessionId);
|
|
88
88
|
const isBusy = Boolean(active?.currentRunId);
|
|
89
89
|
if (!isBusy && !(readQueue().sessions[params.sessionId]?.length)) {
|
|
90
|
-
await this.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
90
|
+
await this.opts.dispatchReq({
|
|
91
|
+
...req,
|
|
92
|
+
method: 'chat.send',
|
|
93
|
+
params: {
|
|
94
|
+
...params,
|
|
95
|
+
clientMessageId: params.clientMessageId ?? params.queueMessageId,
|
|
96
|
+
waitForDispatch: true,
|
|
97
|
+
},
|
|
96
98
|
});
|
|
97
99
|
return;
|
|
98
100
|
}
|