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
- runtime.client.sendRes({ type: 'res', id: req.id, ok: true });
395
- reportLog({
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
- wsEvent: 'chat.send.done',
407
- metadata: { reqId: req.id },
373
+ agentType: requestedAgentType,
374
+ workDir: resolvedWorkDir,
375
+ agentSessionId: session.agentSessionId ?? incomingAgentSid ?? null,
376
+ modelId,
408
377
  });
409
- })
410
- .catch(async (err) => {
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.dispatchQueuedMessage(queueMessageFromParams(params));
91
- runtime.client.sendRes({
92
- type: 'res',
93
- id: req.id,
94
- ok: true,
95
- payload: { queued: false, queue: this.getSnapshot(params.sessionId) },
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shennian",
3
- "version": "0.2.49",
3
+ "version": "0.2.51",
4
4
  "description": "Shennian — AI Agent Control Plane CLI",
5
5
  "type": "module",
6
6
  "bin": {