pluriply 0.5.2 → 0.5.4
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/package.json +1 -1
- package/src/connector/tools.js +177 -50
package/package.json
CHANGED
package/src/connector/tools.js
CHANGED
|
@@ -51,6 +51,31 @@ function staleHubMessage(stale) {
|
|
|
51
51
|
);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/** 허브가 hello 하지 않은 연결의 요청을 거절할 때 쓰는 문구(server.js #conn) */
|
|
55
|
+
const UNIDENTIFIED = "say hello first";
|
|
56
|
+
|
|
57
|
+
/** 채널 문서가 없을 때의 허브 오류(channels.js ChannelNotFound) */
|
|
58
|
+
const CHANNEL_GONE = /^channel not found: /;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* hub-client 자신이 내는 연결 오류(허브의 거절이 아니다). 원인이 정체성이 아니므로 "정체성을
|
|
62
|
+
* 잃었다"로 감싸지 않고 원래 오류를 그대로 알린다: 끊김·시간 초과는 재접속 리스너가 곧
|
|
63
|
+
* 되살리고, unreachable(dead)은 이미 원인(예: unauthorized)과 재시작 안내를 담고 있다.
|
|
64
|
+
*/
|
|
65
|
+
const HUB_CLIENT_ERROR =
|
|
66
|
+
/^hub (connection closed|connection error|request timed out|unreachable)/;
|
|
67
|
+
|
|
68
|
+
/** 정체성을 되살리지 못했다 — 도구를 다시 시작해야 한다(안내 문구를 다른 안내로 감싸지 않게 구분한다) */
|
|
69
|
+
class IdentityLostError extends Error {
|
|
70
|
+
/** @param {string} reason */
|
|
71
|
+
constructor(reason) {
|
|
72
|
+
super(
|
|
73
|
+
`Pluriply lost this session's identity on the hub connection and could not restore it (${reason}). ` +
|
|
74
|
+
"Restart this AI tool (or reload its MCP server) to reconnect.",
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
54
79
|
/**
|
|
55
80
|
* 허브에 정체성을 알리고 인스턴스 ID를 받는다. 재접속 때는 알고 있는 ID를 실어 그대로 인정받는다.
|
|
56
81
|
* @param {import('./hub-client.js').HubClient} hub
|
|
@@ -90,12 +115,122 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
90
115
|
const delegationDepth = () =>
|
|
91
116
|
worker ? Number(process.env.PLURIPLY_DEPTH ?? 0) + 1 : 0;
|
|
92
117
|
|
|
118
|
+
/** 이 소켓에 알고 있는 instanceId 로 정체성을 다시 알린다 @param {{duringReconnect?: boolean}} [opts] */
|
|
119
|
+
async function helloAgain(opts = {}) {
|
|
120
|
+
state.instanceId = await hello(hub, {
|
|
121
|
+
agent,
|
|
122
|
+
worker,
|
|
123
|
+
instanceId: state.instanceId,
|
|
124
|
+
...opts,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 참여 중이던 채널에 이 소켓으로 다시 들어간다.
|
|
130
|
+
* @param {{duringReconnect?: boolean}} [opts] @returns {Promise<boolean>} 채널이 없으면 false
|
|
131
|
+
*/
|
|
132
|
+
async function rejoin(opts = {}) {
|
|
133
|
+
if (!state.currentChannel) return false;
|
|
134
|
+
await hub.request(
|
|
135
|
+
"channel.join",
|
|
136
|
+
{ channelCode: state.currentChannel },
|
|
137
|
+
opts,
|
|
138
|
+
);
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** 진행 중인 정체성 복구. 동시에 거절된 요청들이 hello 하나를 같이 기다린다. */
|
|
143
|
+
let recovering = null;
|
|
144
|
+
/**
|
|
145
|
+
* 이 소켓의 정체성이나 채널 참여가 빠졌다고 알고 있는 상태(재접속·복구의 hello 또는 재참여가
|
|
146
|
+
* 실패했다). 조회 도구가 쓰는 허브 요청(channel.peers·task.list 등)은 정체성·참여 없이도
|
|
147
|
+
* 통과하므로, 거절을 기다리면 복구가 일어나지 않고 이 인스턴스가 동료에게 offline 으로 남는다
|
|
148
|
+
* (이 도구로 오는 태스크도 대화형 세션 대신 워커로 갈 수 있다) — 그래서 요청 전에 먼저 복구한다.
|
|
149
|
+
* hello 와 재참여가 모두 끝나야 내린다: 끝나 가는 복구를 재사용하는 사이 재접속이 끼어도
|
|
150
|
+
* 표시가 잘못 내려가지 않는다.
|
|
151
|
+
*/
|
|
152
|
+
let needsRecover = false;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 참여 중이던 채널에 다시 들어가고 결과를 상태에 반영한다.
|
|
156
|
+
* - 채널이 사라졌으면 currentChannel 을 비워 다음 호출이 자동 복귀·"Join a channel first" 로
|
|
157
|
+
* 알리게 한다(기다리는 사이 join_channel 이 다른 채널로 바꿨으면 그대로 둔다).
|
|
158
|
+
* - 연결 오류(끊김 등)나, 복구 도중 재접속이 끼어 아직 정체성 없는 새 소켓으로 나간 경우
|
|
159
|
+
* ("say hello first")는 다시 보내면 되므로 needsRecover 로 남겨 다음 호출이 다시 시도한다.
|
|
160
|
+
* - 그 밖의 거절(예: 손상된 채널 문서)은 다시 보내도 같으니 표시를 내린다 — 남기면 세션 내내
|
|
161
|
+
* 요청마다 왕복이 붙는다. 채널은 유지해 이후 요청이 실제 오류를 보이게 한다.
|
|
162
|
+
* @param {{duringReconnect?: boolean}} [opts] @returns {Promise<boolean>} 다시 들어갔으면 true
|
|
163
|
+
*/
|
|
164
|
+
async function restoreChannel(opts) {
|
|
165
|
+
const code = state.currentChannel;
|
|
166
|
+
try {
|
|
167
|
+
const joined = await rejoin(opts);
|
|
168
|
+
needsRecover = false;
|
|
169
|
+
return joined;
|
|
170
|
+
} catch (err) {
|
|
171
|
+
if (CHANNEL_GONE.test(err.message)) {
|
|
172
|
+
if (state.currentChannel === code) state.currentChannel = null;
|
|
173
|
+
needsRecover = false;
|
|
174
|
+
} else
|
|
175
|
+
needsRecover =
|
|
176
|
+
HUB_CLIENT_ERROR.test(err.message) || err.message === UNIDENTIFIED;
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 정체성을 되살리고 참여 중이던 채널에 다시 들어간다(hello 는 연결당 멱등).
|
|
183
|
+
* 재접속 리스너는 이 공유 promise 를 쓰면 안 된다 — 여기서 나가는 요청은 재접속 배리어를
|
|
184
|
+
* 기다리고, 배리어는 리스너가 끝나기를 기다리므로 서로를 기다리는 교착이 된다.
|
|
185
|
+
* @returns {Promise<void>} hello 가 허브에 거절되면 IdentityLostError, 연결 오류면 그 오류 그대로
|
|
186
|
+
*/
|
|
187
|
+
function recover() {
|
|
188
|
+
recovering ??= (async () => {
|
|
189
|
+
try {
|
|
190
|
+
await helloAgain();
|
|
191
|
+
} catch (err) {
|
|
192
|
+
needsRecover = true;
|
|
193
|
+
if (HUB_CLIENT_ERROR.test(err.message)) throw err;
|
|
194
|
+
throw new IdentityLostError(err.message);
|
|
195
|
+
}
|
|
196
|
+
await restoreChannel();
|
|
197
|
+
})().finally(() => {
|
|
198
|
+
recovering = null;
|
|
199
|
+
});
|
|
200
|
+
return recovering;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* hub.request 와 같되 이 소켓의 정체성을 지킨다. 정체성·참여가 빠진 것을 알면 보내기 전에
|
|
205
|
+
* 복구하고(재참여가 계속 실패하면 두 번까지만 — 요청은 그대로 보내고 다음 호출이 다시 시도한다),
|
|
206
|
+
* 허브가 "say hello first" 로 거절하면 복구한 뒤 한 번만 다시 보낸다. 허브는 정체성이 필요한
|
|
207
|
+
* 요청을 첫 줄(#conn)에서 거절하므로 다시 보내도 중복 부작용이 없다.
|
|
208
|
+
* 재접속 리스너의 duringReconnect 요청은 이 경로를 타지 않는다.
|
|
209
|
+
* @type {typeof hub.request}
|
|
210
|
+
*/
|
|
211
|
+
async function hubRequest(type, payload, opts) {
|
|
212
|
+
for (let i = 0; needsRecover && i < 2; i++) await recover();
|
|
213
|
+
try {
|
|
214
|
+
return await hub.request(type, payload, opts);
|
|
215
|
+
} catch (err) {
|
|
216
|
+
if (err.message !== UNIDENTIFIED) throw err;
|
|
217
|
+
}
|
|
218
|
+
await recover();
|
|
219
|
+
try {
|
|
220
|
+
return await hub.request(type, payload, opts);
|
|
221
|
+
} catch (err) {
|
|
222
|
+
if (err.message === UNIDENTIFIED)
|
|
223
|
+
throw new IdentityLostError(err.message);
|
|
224
|
+
throw err;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
93
228
|
/**
|
|
94
229
|
* 채널이 없을 때 허브에 직전 채널 복귀를 요청한다.
|
|
95
230
|
* @returns {Promise<string|null>} 복귀한 채널 코드
|
|
96
231
|
*/
|
|
97
232
|
async function resumeChannel() {
|
|
98
|
-
const { channelCode } = await
|
|
233
|
+
const { channelCode } = await hubRequest("agent.resume", {});
|
|
99
234
|
if (!channelCode) return null;
|
|
100
235
|
if (state.currentChannel) return null; // join_channel이 경합에서 이겼으니 그대로 둔다
|
|
101
236
|
state.currentChannel = channelCode;
|
|
@@ -113,6 +248,8 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
113
248
|
try {
|
|
114
249
|
resumed = await resumeChannel();
|
|
115
250
|
} catch (err) {
|
|
251
|
+
// join_channel 도 같은 이유로 실패하므로 그쪽을 권하지 않고 재시작 안내만 낸다
|
|
252
|
+
if (err instanceof IdentityLostError) return fail(err.message);
|
|
116
253
|
return fail(
|
|
117
254
|
`Join a channel first with join_channel. (auto-resume failed: ${err.message})`,
|
|
118
255
|
);
|
|
@@ -147,7 +284,7 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
147
284
|
const remaining = deadline - Date.now();
|
|
148
285
|
if (remaining <= 0) break;
|
|
149
286
|
const timeoutMs = Math.min(ASK_CHUNK_MS, remaining);
|
|
150
|
-
({ task } = await
|
|
287
|
+
({ task } = await hubRequest(
|
|
151
288
|
"task.wait",
|
|
152
289
|
{ channelCode: code, taskId, timeoutMs },
|
|
153
290
|
{ timeoutMs: timeoutMs + 5000 },
|
|
@@ -200,22 +337,16 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
200
337
|
// join_channel이 도구를 다시 시작할 때까지 "say hello first"로 막힌다.
|
|
201
338
|
// (hello는 인증이 필요한 요청이라, 토큰이 틀린 연결은 여기서 unauthorized를
|
|
202
339
|
// 받아 hub-client의 재접속 판정이 그것을 본다.)
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
worker,
|
|
206
|
-
instanceId: state.instanceId,
|
|
207
|
-
duringReconnect: true,
|
|
208
|
-
});
|
|
209
|
-
if (!state.currentChannel) return;
|
|
210
|
-
await hub.request(
|
|
211
|
-
"channel.join",
|
|
212
|
-
{ channelCode: state.currentChannel },
|
|
213
|
-
{ duringReconnect: true },
|
|
214
|
-
);
|
|
215
|
-
hub.emit("rejoined", state.currentChannel);
|
|
340
|
+
// recover() 의 공유 promise 는 쓰지 않는다(교착 — recover 주석 참고).
|
|
341
|
+
await helloAgain({ duringReconnect: true });
|
|
216
342
|
} catch {
|
|
217
|
-
//
|
|
343
|
+
// 다음 도구 호출이 요청을 보내기 전에 hubRequest() 에서 복구한다
|
|
344
|
+
needsRecover = true;
|
|
345
|
+
return;
|
|
218
346
|
}
|
|
347
|
+
// 재참여 실패도 needsRecover 로 남기고, 채널이 사라졌으면 currentChannel 을 비운다
|
|
348
|
+
if (await restoreChannel({ duringReconnect: true }))
|
|
349
|
+
hub.emit("rejoined", state.currentChannel);
|
|
219
350
|
});
|
|
220
351
|
}
|
|
221
352
|
|
|
@@ -234,8 +365,8 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
234
365
|
if (hub.stale) return fail(staleHubMessage(hub.stale));
|
|
235
366
|
try {
|
|
236
367
|
const code =
|
|
237
|
-
channel_code ?? (await
|
|
238
|
-
const { peers } = await
|
|
368
|
+
channel_code ?? (await hubRequest("channel.create")).channelCode;
|
|
369
|
+
const { peers } = await hubRequest("channel.join", {
|
|
239
370
|
channelCode: code,
|
|
240
371
|
});
|
|
241
372
|
state.currentChannel = code;
|
|
@@ -255,7 +386,7 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
255
386
|
inputSchema: {},
|
|
256
387
|
},
|
|
257
388
|
needChannel((_args, code) =>
|
|
258
|
-
|
|
389
|
+
hubRequest("channel.peers", { channelCode: code }),
|
|
259
390
|
),
|
|
260
391
|
);
|
|
261
392
|
|
|
@@ -268,15 +399,15 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
268
399
|
inputSchema: {},
|
|
269
400
|
},
|
|
270
401
|
needChannel(async (_args, code) => {
|
|
271
|
-
const { peers } = await
|
|
402
|
+
const { peers } = await hubRequest("channel.peers", {
|
|
272
403
|
channelCode: code,
|
|
273
404
|
});
|
|
274
|
-
const { tasks } = await
|
|
405
|
+
const { tasks } = await hubRequest("task.list", {
|
|
275
406
|
channelCode: code,
|
|
276
407
|
to: state.instanceId,
|
|
277
408
|
status: "submitted",
|
|
278
409
|
});
|
|
279
|
-
const { running } = await
|
|
410
|
+
const { running } = await hubRequest("worker.status", {
|
|
280
411
|
channelCode: code,
|
|
281
412
|
});
|
|
282
413
|
return {
|
|
@@ -335,7 +466,7 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
335
466
|
{ to, request, attachments = [], mode = "auto", cwd = process.cwd() },
|
|
336
467
|
code,
|
|
337
468
|
) =>
|
|
338
|
-
|
|
469
|
+
hubRequest("task.create", {
|
|
339
470
|
channelCode: code,
|
|
340
471
|
to,
|
|
341
472
|
request,
|
|
@@ -399,7 +530,7 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
399
530
|
`ask_agent takes a tool name (e.g. "codex"); use send_task to pin an instance like "${to}"`,
|
|
400
531
|
);
|
|
401
532
|
const waitS = clampWaitSeconds(wait_seconds);
|
|
402
|
-
const created = await
|
|
533
|
+
const created = await hubRequest("task.create", {
|
|
403
534
|
channelCode: code,
|
|
404
535
|
to,
|
|
405
536
|
request,
|
|
@@ -412,13 +543,11 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
412
543
|
const { taskId } = created;
|
|
413
544
|
if (created.dispatch !== "spawned" && created.dispatch !== "queued") {
|
|
414
545
|
// 워커가 뜨지 않았다: 쓰레기 submitted 태스크를 남기지 않는다
|
|
415
|
-
await
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
})
|
|
421
|
-
.catch(() => {});
|
|
546
|
+
await hubRequest("task.cancel", {
|
|
547
|
+
channelCode: code,
|
|
548
|
+
taskId,
|
|
549
|
+
reason: "ask_agent: worker not started",
|
|
550
|
+
}).catch(() => {});
|
|
422
551
|
return {
|
|
423
552
|
status: "not_started",
|
|
424
553
|
taskId,
|
|
@@ -510,7 +639,7 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
510
639
|
if (git_range !== undefined) review.gitRange = git_range;
|
|
511
640
|
if (paths !== undefined) review.paths = paths;
|
|
512
641
|
if (focus !== undefined) review.focus = focus;
|
|
513
|
-
const created = await
|
|
642
|
+
const created = await hubRequest("task.create", {
|
|
514
643
|
channelCode: code,
|
|
515
644
|
to,
|
|
516
645
|
request: request ?? "",
|
|
@@ -525,13 +654,11 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
525
654
|
if (wait_seconds === undefined) return created;
|
|
526
655
|
const { taskId } = created;
|
|
527
656
|
if (created.dispatch === "none") {
|
|
528
|
-
await
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
})
|
|
534
|
-
.catch(() => {});
|
|
657
|
+
await hubRequest("task.cancel", {
|
|
658
|
+
channelCode: code,
|
|
659
|
+
taskId,
|
|
660
|
+
reason: "request_review: worker not started",
|
|
661
|
+
}).catch(() => {});
|
|
535
662
|
return {
|
|
536
663
|
status: "not_started",
|
|
537
664
|
taskId,
|
|
@@ -567,7 +694,7 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
567
694
|
},
|
|
568
695
|
},
|
|
569
696
|
needChannel(({ task_id, reason }, code) =>
|
|
570
|
-
|
|
697
|
+
hubRequest("task.cancel", {
|
|
571
698
|
channelCode: code,
|
|
572
699
|
taskId: task_id,
|
|
573
700
|
reason,
|
|
@@ -602,7 +729,7 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
602
729
|
},
|
|
603
730
|
needChannel(
|
|
604
731
|
({ status, mine_only = true, sent_by_me = false, kind }, code) =>
|
|
605
|
-
|
|
732
|
+
hubRequest("task.list", {
|
|
606
733
|
channelCode: code,
|
|
607
734
|
to: sent_by_me ? undefined : mine_only ? state.instanceId : undefined,
|
|
608
735
|
from: sent_by_me ? state.instanceId : undefined,
|
|
@@ -621,7 +748,7 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
621
748
|
inputSchema: { task_id: z.string() },
|
|
622
749
|
},
|
|
623
750
|
needChannel(({ task_id }, code) =>
|
|
624
|
-
|
|
751
|
+
hubRequest("task.get", { channelCode: code, taskId: task_id }),
|
|
625
752
|
),
|
|
626
753
|
);
|
|
627
754
|
|
|
@@ -645,17 +772,17 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
645
772
|
},
|
|
646
773
|
},
|
|
647
774
|
needChannel(async ({ task_id, result, failed = false }, code) => {
|
|
648
|
-
const { task } = await
|
|
775
|
+
const { task } = await hubRequest("task.get", {
|
|
649
776
|
channelCode: code,
|
|
650
777
|
taskId: task_id,
|
|
651
778
|
});
|
|
652
779
|
if (task.status === "submitted") {
|
|
653
|
-
await
|
|
780
|
+
await hubRequest("task.claim", {
|
|
654
781
|
channelCode: code,
|
|
655
782
|
taskId: task_id,
|
|
656
783
|
});
|
|
657
784
|
}
|
|
658
|
-
return
|
|
785
|
+
return hubRequest("task.complete", {
|
|
659
786
|
channelCode: code,
|
|
660
787
|
taskId: task_id,
|
|
661
788
|
result,
|
|
@@ -691,14 +818,14 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
691
818
|
},
|
|
692
819
|
},
|
|
693
820
|
needChannel(async ({ task_id, verdict, findings = [], summary }, code) => {
|
|
694
|
-
const { task } = await
|
|
821
|
+
const { task } = await hubRequest("task.get", {
|
|
695
822
|
channelCode: code,
|
|
696
823
|
taskId: task_id,
|
|
697
824
|
});
|
|
698
825
|
if (task.status === "submitted") {
|
|
699
|
-
await
|
|
826
|
+
await hubRequest("task.claim", { channelCode: code, taskId: task_id });
|
|
700
827
|
}
|
|
701
|
-
return
|
|
828
|
+
return hubRequest("task.complete", {
|
|
702
829
|
channelCode: code,
|
|
703
830
|
taskId: task_id,
|
|
704
831
|
status: "completed",
|
|
@@ -722,7 +849,7 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
722
849
|
},
|
|
723
850
|
},
|
|
724
851
|
needChannel(({ summary, artifacts = [] }, code) =>
|
|
725
|
-
|
|
852
|
+
hubRequest("context.add", {
|
|
726
853
|
channelCode: code,
|
|
727
854
|
summary,
|
|
728
855
|
artifacts,
|
|
@@ -744,7 +871,7 @@ export function registerTools(server, hub, { agent, instanceId }) {
|
|
|
744
871
|
},
|
|
745
872
|
},
|
|
746
873
|
needChannel(({ limit }, code) =>
|
|
747
|
-
|
|
874
|
+
hubRequest("context.list", { channelCode: code, limit }),
|
|
748
875
|
),
|
|
749
876
|
);
|
|
750
877
|
}
|