zhuye 0.1.5 → 0.1.6

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/agent.d.ts CHANGED
@@ -19,6 +19,12 @@ export interface ZhuyeAgentOptions {
19
19
  spawnOptions?: Omit<SpawnOptions, 'bin' | 'args' | 'env' | 'cwd'>;
20
20
  /** `session_configured` 握手超时(ms)。默认 30s。 */
21
21
  handshakeTimeoutMs?: number;
22
+ /**
23
+ * 子进程 stderr 接管回调。提供后每个原始块交给回调(落文件 /
24
+ * 送日志系统 / 过滤降噪),不再硬编码直通宿主 stderr。
25
+ * 未提供时维持旧行为(透传宿主 stderr)。
26
+ */
27
+ onStderr?: (chunk: Buffer) => void;
22
28
  }
23
29
  interface SpawnOptions {
24
30
  bin: string;
package/dist/agent.js CHANGED
@@ -18,6 +18,15 @@ const ZHUYE_BIN_ENV = 'ZHUYE_BIN';
18
18
  const DEFAULT_BIN = 'zhuye';
19
19
  const READLINE_LIMIT = 16 * 1024 * 1024;
20
20
  const STDIN_HANDSHAKE_TIMEOUT_MS = 30_000;
21
+ /** 握手失败时随异常携带的 stderr 尾部块数上限。 */
22
+ const STDERR_TAIL_LINES = 32;
23
+ /** 把 stderr 尾部 ring buffer 拼成诊断文本(解码容错)。 */
24
+ function stderrTailText(tail) {
25
+ return tail
26
+ .map((c) => c.toString('utf-8').trim())
27
+ .filter((s) => s.length > 0)
28
+ .join(' | ');
29
+ }
21
30
  /**
22
31
  * 主类:启动并维护一个常驻 `zhuye serve` 子进程,把 JSONL 协议包装
23
32
  * 成 Promise / AsyncIterable 给 TS 用户。
@@ -62,9 +71,20 @@ export class ZhuyeAgent {
62
71
  env: spawnOpts.env,
63
72
  stdio: ['pipe', 'pipe', 'pipe'],
64
73
  });
65
- // 子进程 stderr → 当前进程 stderr(让 tracing 日志透传)。
74
+ // 子进程 stderr → 提供 onStderr 回调时交给回调;否则透传
75
+ // 当前进程 stderr。无论哪种模式,尾部都保留在 ring buffer 里,
76
+ // 握手期子进程死亡时把真实原因带进异常。
77
+ const stderrTail = [];
66
78
  child.stderr.on('data', (chunk) => {
67
- process.stderr.write(chunk);
79
+ stderrTail.push(chunk);
80
+ if (stderrTail.length > STDERR_TAIL_LINES)
81
+ stderrTail.shift();
82
+ if (options.onStderr) {
83
+ options.onStderr(chunk);
84
+ }
85
+ else {
86
+ process.stderr.write(chunk);
87
+ }
68
88
  });
69
89
  const proc = {
70
90
  child,
@@ -78,29 +98,83 @@ export class ZhuyeAgent {
78
98
  readyResolve = res;
79
99
  readyReject = rej;
80
100
  });
101
+ // 握手期死亡时 ready 被拒绝;若调用方从未 await 它(spawn 后
102
+ // 直接 close / 只用全局事件),挂一个 no-op 兜底标记已处理,
103
+ // 避免 unhandled rejection 打爆宿主。后续 await 仍能拿到该错误。
104
+ ready.catch(() => { });
81
105
  const agent = new ZhuyeAgent(proc, ready);
106
+ // 握手是否仍在进行 —— 子进程在握手期退出时,ready 立刻拒绝
107
+ // (带 stderr 尾部),不再傻等满握手超时后报泛化的 timed out。
108
+ let handshakeSettled = false;
82
109
  // 子进程意外退出 → 通知所有 close 监听者(submit / prompt 等
83
110
  // 仍 await 中的 promise 借此抛错 / 收尾)。
84
111
  child.on('exit', (code, signal) => {
85
- agent.handleServeExit(new Error(`zhuye serve exited unexpectedly (code=${code} signal=${signal ?? 'none'})`));
112
+ // 阶段标记必须在 exit 时刻捕获:等待 stderr 排空期间握手状态
113
+ // 可能被翻转,措辞判定要以死亡时的阶段为准。
114
+ const wasHandshake = !handshakeSettled;
115
+ // 握手期死亡与运行期死亡措辞区分,方便排障定位。
116
+ const buildErr = () => {
117
+ const tail = stderrTail.length
118
+ ? ` stderr tail: ${stderrTailText(stderrTail)}`
119
+ : '';
120
+ return wasHandshake
121
+ ? new Error(`zhuye serve exited during handshake (code=${code} signal=${signal ?? 'none'})${tail}`)
122
+ : new Error(`zhuye serve exited unexpectedly (code=${code} signal=${signal ?? 'none'})${tail}`);
123
+ };
124
+ if (!wasHandshake) {
125
+ agent.handleServeExit(buildErr());
126
+ return;
127
+ }
128
+ // 竞态:子进程写 stderr 后立刻退出时,`exit` 事件可能先于
129
+ // stderr 的 `data` 事件投递(管道排空晚于进程退出),此刻同步读
130
+ // stderrTail 会拿到空尾、丢失真实原因。握手失败不在毫秒级关键
131
+ // 路径上 —— 等 `close`(stdio 全部关闭,stderr 必已投递)或至多
132
+ // 500ms 兜底,再构建错误。
133
+ let drainSettled = false;
134
+ const finish = () => {
135
+ if (drainSettled)
136
+ return;
137
+ drainSettled = true;
138
+ // 等待期间 stdout 缓冲可能投递了 session_configured:握手实际
139
+ // 成功则让路,不再以失败拒绝。
140
+ if (handshakeSettled)
141
+ return;
142
+ handshakeSettled = true;
143
+ const err = buildErr();
144
+ readyReject(err);
145
+ agent.handleServeExit(err);
146
+ };
147
+ child.once('close', finish);
148
+ const drainTimer = setTimeout(finish, 500);
149
+ drainTimer.unref?.();
86
150
  });
87
151
  // spawn 失败(典型:二进制路径不存在,ENOENT):ChildProcess 的
88
152
  // error 事件没有监听者会以 uncaught exception 打爆宿主进程;这里
89
153
  // 转为握手失败(ready 尚未 settle 时立即拒绝,否则等握手超时)。
90
154
  child.on('error', (err) => {
91
155
  const failure = new Error(`zhuye serve failed to start: ${err.message}`);
92
- readyReject(failure);
156
+ if (!handshakeSettled) {
157
+ handshakeSettled = true;
158
+ readyReject(failure);
159
+ }
93
160
  agent.handleServeExit(failure);
94
161
  });
95
162
  const handshakeMs = options.handshakeTimeoutMs ?? STDIN_HANDSHAKE_TIMEOUT_MS;
96
163
  const timer = setTimeout(() => {
97
- readyReject(new Error(`zhuye serve handshake timed out after ${handshakeMs}ms`));
164
+ if (handshakeSettled)
165
+ return;
166
+ handshakeSettled = true;
167
+ readyReject(new Error(`zhuye serve handshake timed out after ${handshakeMs}ms` +
168
+ (stderrTail.length
169
+ ? ` stderr tail: ${stderrTailText(stderrTail)}`
170
+ : '')));
98
171
  }, handshakeMs);
99
172
  timer.unref?.();
100
173
  // 在握手阶段消费 stdout,直到见到 session_configured 后再切给
101
174
  // 业务消费者 —— 简单做法:让后台 consumer 自己处理,ready 在
102
175
  // session_configured 时 resolve。
103
176
  agent.onceSessionConfigured = () => {
177
+ handshakeSettled = true;
104
178
  clearTimeout(timer);
105
179
  readyResolve();
106
180
  };
@@ -120,6 +194,9 @@ export class ZhuyeAgent {
120
194
  readyResolve = res;
121
195
  readyReject = rej;
122
196
  });
197
+ // 与 spawn 同款兜底:握手超时拒绝而调用方从未 await 时,
198
+ // 不让 unhandled rejection 打爆宿主;后续 await 仍能拿到该错误。
199
+ ready.catch(() => { });
123
200
  const agent = new ZhuyeAgent(proc, ready);
124
201
  const timer = setTimeout(() => {
125
202
  readyReject(new Error(`fromStdio handshake timed out after ${handshakeTimeoutMs}ms`));
@@ -162,8 +239,14 @@ export class ZhuyeAgent {
162
239
  const msg = ev.msg;
163
240
  if (msg.type === 'agent_message_delta')
164
241
  result += msg.delta;
165
- else if (msg.type === 'agent_message')
166
- result += msg.text;
242
+ else if (msg.type === 'agent_message') {
243
+ // 全量消息是权威快照 —— 覆盖(而非追加)已累计的 delta,
244
+ // 协议允许同轮混合下发时不会产生重复文本。text 经 typeof 守卫:
245
+ // EventMsg 联合含开放成员(UnknownEventMsg),窄化后仍是 unknown。
246
+ const { text } = msg;
247
+ if (typeof text === 'string')
248
+ result = text;
249
+ }
167
250
  else if (msg.type === 'turn_complete')
168
251
  break;
169
252
  else if (msg.type === 'turn_aborted') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zhuye",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "ZhuYe Agent SDK for TypeScript: spawn `zhuye serve` and drive it over JSONL stdio.",
5
5
  "author": "ZhuYe Agent Contributors",
6
6
  "homepage": "https://github.com/Asketisch/ZhuYe#readme",
@@ -37,10 +37,10 @@
37
37
  "vitest": "^1.6.0"
38
38
  },
39
39
  "optionalDependencies": {
40
- "zhuye-darwin-arm64": "0.1.5",
41
- "zhuye-darwin-x64": "0.1.5",
42
- "zhuye-linux-x64": "0.1.5",
43
- "zhuye-linux-arm64": "0.1.5",
44
- "zhuye-win32-x64": "0.1.5"
40
+ "zhuye-darwin-arm64": "0.1.6",
41
+ "zhuye-darwin-x64": "0.1.6",
42
+ "zhuye-linux-x64": "0.1.6",
43
+ "zhuye-linux-arm64": "0.1.6",
44
+ "zhuye-win32-x64": "0.1.6"
45
45
  }
46
46
  }