@blade-hq/agent-client 1.2.1-beta.9 → 2608.0.1
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/index.js +124 -33
- package/dist/index.js.map +1 -1
- package/dist/session/agent-session.d.ts +9 -2
- package/dist/session/hub.d.ts +18 -1
- package/dist/shared/projection/helpers.d.ts +9 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -56,9 +56,14 @@ async function loginWithPopup(client, options = {}) {
|
|
|
56
56
|
return new Promise((resolve, reject) => {
|
|
57
57
|
const timeoutMs = options.timeoutMs ?? 5 * 60 * 1e3;
|
|
58
58
|
let settled = false;
|
|
59
|
+
let closeTimer;
|
|
60
|
+
const stopCloseWatch = () => {
|
|
61
|
+
globalThis.clearInterval(closeWatcher);
|
|
62
|
+
if (closeTimer !== void 0) globalThis.clearTimeout(closeTimer);
|
|
63
|
+
};
|
|
59
64
|
const cleanup = () => {
|
|
60
65
|
window.removeEventListener("message", onMessage);
|
|
61
|
-
|
|
66
|
+
stopCloseWatch();
|
|
62
67
|
globalThis.clearTimeout(timeoutHandle);
|
|
63
68
|
};
|
|
64
69
|
const settle = (fn) => {
|
|
@@ -73,6 +78,7 @@ async function loginWithPopup(client, options = {}) {
|
|
|
73
78
|
if (!isAuthCallbackMessage(event.data)) return;
|
|
74
79
|
if (event.data.state !== state) return;
|
|
75
80
|
const message = event.data;
|
|
81
|
+
stopCloseWatch();
|
|
76
82
|
try {
|
|
77
83
|
popup.close();
|
|
78
84
|
} catch {
|
|
@@ -98,11 +104,10 @@ async function loginWithPopup(client, options = {}) {
|
|
|
98
104
|
});
|
|
99
105
|
};
|
|
100
106
|
const closeWatcher = globalThis.setInterval(() => {
|
|
101
|
-
if (popup.closed
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
107
|
+
if (!popup.closed || settled || closeTimer !== void 0) return;
|
|
108
|
+
closeTimer = globalThis.setTimeout(() => {
|
|
109
|
+
settle(() => reject(new Error("\u767B\u5F55\u7A97\u53E3\u5DF2\u5173\u95ED\uFF0C\u672A\u5B8C\u6210\u6388\u6743\u3002")));
|
|
110
|
+
}, 400);
|
|
106
111
|
}, 500);
|
|
107
112
|
const timeoutHandle = globalThis.setTimeout(() => {
|
|
108
113
|
settle(() => reject(new Error("\u767B\u5F55\u8D85\u65F6\uFF0C\u8BF7\u91CD\u8BD5\u3002")));
|
|
@@ -822,6 +827,24 @@ function sourceLoopFor(loopId, loopDescriptions) {
|
|
|
822
827
|
if (!description) return null;
|
|
823
828
|
return { loop_name: loopId, description };
|
|
824
829
|
}
|
|
830
|
+
function normalizePostChatFollowup(data, assistantEntryId) {
|
|
831
|
+
const raw = data.suggestions;
|
|
832
|
+
const suggestions = [];
|
|
833
|
+
if (Array.isArray(raw)) {
|
|
834
|
+
for (const value of raw) {
|
|
835
|
+
if (typeof value !== "string") continue;
|
|
836
|
+
const text = value.trim();
|
|
837
|
+
if (text && !suggestions.includes(text)) suggestions.push(text);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
const recaption = String(data.recaption ?? "");
|
|
841
|
+
if (suggestions.length === 0 && !recaption) return null;
|
|
842
|
+
return {
|
|
843
|
+
assistant_entry_id: assistantEntryId,
|
|
844
|
+
recaption,
|
|
845
|
+
suggestions: suggestions.slice(0, 3)
|
|
846
|
+
};
|
|
847
|
+
}
|
|
825
848
|
function normalizeChildPause(pausePayload) {
|
|
826
849
|
const rawPtd = pausePayload.pause_tool_data;
|
|
827
850
|
const pauseToolData = typeof rawPtd === "object" && rawPtd !== null ? { ...rawPtd } : { ...pausePayload };
|
|
@@ -1240,12 +1263,7 @@ var ClientProjectionBuilder = class {
|
|
|
1240
1263
|
if (!latest || latest.role !== "assistant" || latest.turn_id !== assistantEntryId) {
|
|
1241
1264
|
return null;
|
|
1242
1265
|
}
|
|
1243
|
-
const
|
|
1244
|
-
const postChatFollowup = Array.isArray(suggestions) && suggestions.length === 3 && suggestions.every((value) => typeof value === "string") ? {
|
|
1245
|
-
assistant_entry_id: assistantEntryId,
|
|
1246
|
-
recaption: String(payload.recaption ?? ""),
|
|
1247
|
-
suggestions
|
|
1248
|
-
} : null;
|
|
1266
|
+
const postChatFollowup = normalizePostChatFollowup(payload, assistantEntryId);
|
|
1249
1267
|
if (!postChatFollowup) return null;
|
|
1250
1268
|
const turn = {
|
|
1251
1269
|
...latest,
|
|
@@ -1611,7 +1629,9 @@ function projectHistory(entries) {
|
|
|
1611
1629
|
const toolOwners = /* @__PURE__ */ new Map();
|
|
1612
1630
|
const latestAssistantByLoop = /* @__PURE__ */ new Map();
|
|
1613
1631
|
const pendingChildPauses = /* @__PURE__ */ new Map();
|
|
1632
|
+
const answeredToolCallIds = /* @__PURE__ */ new Set();
|
|
1614
1633
|
let latestRootMessage = null;
|
|
1634
|
+
let latestRootAssistant = null;
|
|
1615
1635
|
let sequence = 0;
|
|
1616
1636
|
for (const entry of entries) {
|
|
1617
1637
|
const kind = String(entry.kind ?? "");
|
|
@@ -1619,13 +1639,9 @@ function projectHistory(entries) {
|
|
|
1619
1639
|
const id = String(entry.id ?? `history-${sequence + 1}`);
|
|
1620
1640
|
if (kind === "post_chat_followup" && isRecord3(entry.data)) {
|
|
1621
1641
|
const assistantEntryId = String(entry.data.assistant_entry_id ?? "");
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
latestRootMessage.post_chat_followup =
|
|
1625
|
-
assistant_entry_id: assistantEntryId,
|
|
1626
|
-
recaption: String(entry.data.recaption ?? ""),
|
|
1627
|
-
suggestions
|
|
1628
|
-
};
|
|
1642
|
+
if (latestRootMessage?.role === "assistant" && latestRootMessage.turn_id === assistantEntryId) {
|
|
1643
|
+
const followup = normalizePostChatFollowup(entry.data, assistantEntryId);
|
|
1644
|
+
if (followup) latestRootMessage.post_chat_followup = followup;
|
|
1629
1645
|
}
|
|
1630
1646
|
continue;
|
|
1631
1647
|
}
|
|
@@ -1670,6 +1686,10 @@ function projectHistory(entries) {
|
|
|
1670
1686
|
}
|
|
1671
1687
|
turns.push(turn);
|
|
1672
1688
|
if (role === "assistant") latestAssistantByLoop.set(loopId, turn);
|
|
1689
|
+
if (loopId === "root") {
|
|
1690
|
+
if (role === "assistant") latestRootAssistant = turn;
|
|
1691
|
+
else if (role === "user") latestRootAssistant = null;
|
|
1692
|
+
}
|
|
1673
1693
|
for (const toolCall of toolCalls) toolOwners.set(toolCall.id, turn);
|
|
1674
1694
|
continue;
|
|
1675
1695
|
}
|
|
@@ -1764,6 +1784,7 @@ function projectHistory(entries) {
|
|
|
1764
1784
|
} else if (kind === "ask_user_answer") {
|
|
1765
1785
|
const data = entry.data ?? {};
|
|
1766
1786
|
const toolCallId = String(data.tool_call_id ?? "");
|
|
1787
|
+
if (toolCallId) answeredToolCallIds.add(toolCallId);
|
|
1767
1788
|
const pending = pendingChildPauses.get(toolCallId);
|
|
1768
1789
|
if (pending) {
|
|
1769
1790
|
setToolCallStatus(latestAssistantByLoop.get(pending.childLoopId), toolCallId, "done", null, null);
|
|
@@ -1790,8 +1811,44 @@ function projectHistory(entries) {
|
|
|
1790
1811
|
);
|
|
1791
1812
|
}
|
|
1792
1813
|
}
|
|
1814
|
+
const rootAssistant = latestRootAssistant;
|
|
1815
|
+
if (rootAssistant) {
|
|
1816
|
+
for (const toolCall of rootAssistant.tool_calls) {
|
|
1817
|
+
if (answeredToolCallIds.has(toolCall.id)) continue;
|
|
1818
|
+
if (toolCall.tool_name.split(":").pop() !== "AskUserQuestion") continue;
|
|
1819
|
+
if (toolCall.status === "error" || toolCall.status === "cancelled") continue;
|
|
1820
|
+
const pauseArguments = parseToolArguments(toolCall.arguments);
|
|
1821
|
+
if (typeof toolCall.result === "string") {
|
|
1822
|
+
const resultArguments = parseToolArguments(toolCall.result);
|
|
1823
|
+
if (Array.isArray(resultArguments.questions)) {
|
|
1824
|
+
pauseArguments.questions = resultArguments.questions;
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
applyAskUserPauseToTurn(
|
|
1828
|
+
rootAssistant,
|
|
1829
|
+
{
|
|
1830
|
+
name: toolCall.tool_name,
|
|
1831
|
+
tool_call_id: toolCall.id,
|
|
1832
|
+
arguments: pauseArguments
|
|
1833
|
+
},
|
|
1834
|
+
toolCall.id,
|
|
1835
|
+
"",
|
|
1836
|
+
"",
|
|
1837
|
+
(name) => name
|
|
1838
|
+
);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1793
1841
|
return turns;
|
|
1794
1842
|
}
|
|
1843
|
+
function parseToolArguments(value) {
|
|
1844
|
+
if (!value) return {};
|
|
1845
|
+
try {
|
|
1846
|
+
const parsed = JSON.parse(value);
|
|
1847
|
+
return isRecord3(parsed) ? parsed : {};
|
|
1848
|
+
} catch {
|
|
1849
|
+
return {};
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1795
1852
|
function buildToolCalls(value) {
|
|
1796
1853
|
if (!Array.isArray(value)) return [];
|
|
1797
1854
|
return value.filter(isRecord3).map((raw) => {
|
|
@@ -3118,7 +3175,7 @@ var AgentSession = class _AgentSession {
|
|
|
3118
3175
|
countFiles: async () => readonlyError(),
|
|
3119
3176
|
downloadFile: async () => readonlyError(),
|
|
3120
3177
|
attachAppCli: async () => readonlyError(),
|
|
3121
|
-
onDispose: () =>
|
|
3178
|
+
onDispose: () => true
|
|
3122
3179
|
},
|
|
3123
3180
|
"connected"
|
|
3124
3181
|
);
|
|
@@ -3376,15 +3433,18 @@ var AgentSession = class _AgentSession {
|
|
|
3376
3433
|
downloadFile(filePath, downloadName) {
|
|
3377
3434
|
return this.runtime.downloadFile(this.sessionId, filePath, downloadName);
|
|
3378
3435
|
}
|
|
3379
|
-
/**
|
|
3436
|
+
/**
|
|
3437
|
+
* 归还一份持有。同一个会话被多处 connect 时按引用计数释放:只有最后
|
|
3438
|
+
* 一份归还才真正断开订阅与监听器,之前的归还不影响仍在使用的一方。
|
|
3439
|
+
*/
|
|
3380
3440
|
dispose() {
|
|
3381
3441
|
if (this.disposed) return;
|
|
3442
|
+
if (!this.runtime.onDispose(this)) return;
|
|
3382
3443
|
this.disposed = true;
|
|
3383
3444
|
this.cancelPatchFlush();
|
|
3384
3445
|
this.listeners.clear();
|
|
3385
3446
|
this.emitter.clear();
|
|
3386
3447
|
this.commands.clear();
|
|
3387
|
-
this.runtime.onDispose(this);
|
|
3388
3448
|
}
|
|
3389
3449
|
get isDisposed() {
|
|
3390
3450
|
return this.disposed;
|
|
@@ -3834,6 +3894,8 @@ var SessionHub = class {
|
|
|
3834
3894
|
client;
|
|
3835
3895
|
sessions = /* @__PURE__ */ new Map();
|
|
3836
3896
|
bootstrapPending = /* @__PURE__ */ new Map();
|
|
3897
|
+
/** 每个会话当前有多少方持有(connect 一次 +1,dispose 一次 -1)。 */
|
|
3898
|
+
holdCounts = /* @__PURE__ */ new Map();
|
|
3837
3899
|
connection = "disconnected";
|
|
3838
3900
|
connectionListeners = /* @__PURE__ */ new Set();
|
|
3839
3901
|
bound = false;
|
|
@@ -3852,22 +3914,30 @@ var SessionHub = class {
|
|
|
3852
3914
|
this.connectionListeners.delete(listener);
|
|
3853
3915
|
};
|
|
3854
3916
|
}
|
|
3855
|
-
/**
|
|
3917
|
+
/**
|
|
3918
|
+
* 取得(或复用)一个会话的 AgentSession,并完成历史加载 + 房间订阅。
|
|
3919
|
+
*
|
|
3920
|
+
* 每次调用都算一份持有,调用方用完必须 `dispose()` 归还。复用同一实例时
|
|
3921
|
+
* 只有最后一份归还才退订房间——否则 StrictMode 双跑、路由抖动这类"连两次
|
|
3922
|
+
* 只释放一次"的场景会把仍在用的会话退订掉,之后所有 turn/chat 事件静默丢失。
|
|
3923
|
+
*/
|
|
3856
3924
|
async connect(sessionId) {
|
|
3857
3925
|
const existing = this.sessions.get(sessionId);
|
|
3858
3926
|
if (existing && !existing.isDisposed) {
|
|
3927
|
+
this.hold(sessionId);
|
|
3859
3928
|
return this.bootstrapPending.get(sessionId) ?? existing;
|
|
3860
3929
|
}
|
|
3861
3930
|
this.ensureBound();
|
|
3862
3931
|
const session = new AgentSession(sessionId, this.runtimeFor(), this.connection);
|
|
3863
3932
|
this.sessions.set(sessionId, session);
|
|
3933
|
+
this.hold(sessionId);
|
|
3864
3934
|
this.ensureConnected();
|
|
3865
3935
|
const bootstrap = session._bootstrap().then(() => session);
|
|
3866
3936
|
this.bootstrapPending.set(sessionId, bootstrap);
|
|
3867
3937
|
try {
|
|
3868
3938
|
return await bootstrap;
|
|
3869
3939
|
} catch (error) {
|
|
3870
|
-
|
|
3940
|
+
this.forceRelease(session);
|
|
3871
3941
|
throw error;
|
|
3872
3942
|
} finally {
|
|
3873
3943
|
if (this.bootstrapPending.get(sessionId) === bootstrap) {
|
|
@@ -3898,7 +3968,7 @@ var SessionHub = class {
|
|
|
3898
3968
|
disposeAll() {
|
|
3899
3969
|
this.bootstrapPending.clear();
|
|
3900
3970
|
for (const session of [...this.sessions.values()]) {
|
|
3901
|
-
|
|
3971
|
+
this.forceRelease(session);
|
|
3902
3972
|
}
|
|
3903
3973
|
}
|
|
3904
3974
|
/**
|
|
@@ -3935,15 +4005,36 @@ var SessionHub = class {
|
|
|
3935
4005
|
countFiles: (sessionId, dirPath) => this.client.sessions.countFiles(sessionId, dirPath),
|
|
3936
4006
|
downloadFile: (sessionId, filePath, downloadName) => this.client.sessions.downloadFile(sessionId, filePath, downloadName),
|
|
3937
4007
|
attachAppCli: (sessionId, definition) => this.client.sessions.attachAppCli(sessionId, definition),
|
|
3938
|
-
onDispose: (session) =>
|
|
3939
|
-
if (this.sessions.get(session.sessionId) === session) {
|
|
3940
|
-
this.sessions.delete(session.sessionId);
|
|
3941
|
-
}
|
|
3942
|
-
;
|
|
3943
|
-
this.client.socket().emit("session:unsubscribe", { session_id: session.sessionId });
|
|
3944
|
-
}
|
|
4008
|
+
onDispose: (session) => this.release(session)
|
|
3945
4009
|
};
|
|
3946
4010
|
}
|
|
4011
|
+
/** 记一份持有。 */
|
|
4012
|
+
hold(sessionId) {
|
|
4013
|
+
this.holdCounts.set(sessionId, (this.holdCounts.get(sessionId) ?? 0) + 1);
|
|
4014
|
+
}
|
|
4015
|
+
/**
|
|
4016
|
+
* 归还一份持有;还剩别的持有者时返回 false,让 AgentSession 保持可用。
|
|
4017
|
+
* 归零才退订房间并从路由表移除。
|
|
4018
|
+
*/
|
|
4019
|
+
release(session) {
|
|
4020
|
+
const remaining = (this.holdCounts.get(session.sessionId) ?? 0) - 1;
|
|
4021
|
+
if (remaining > 0) {
|
|
4022
|
+
this.holdCounts.set(session.sessionId, remaining);
|
|
4023
|
+
return false;
|
|
4024
|
+
}
|
|
4025
|
+
this.holdCounts.delete(session.sessionId);
|
|
4026
|
+
if (this.sessions.get(session.sessionId) === session) {
|
|
4027
|
+
this.sessions.delete(session.sessionId);
|
|
4028
|
+
}
|
|
4029
|
+
;
|
|
4030
|
+
this.client.socket().emit("session:unsubscribe", { session_id: session.sessionId });
|
|
4031
|
+
return true;
|
|
4032
|
+
}
|
|
4033
|
+
/** 不管还有几方持有都立即释放(启动失败、切换身份)。 */
|
|
4034
|
+
forceRelease(session) {
|
|
4035
|
+
this.holdCounts.delete(session.sessionId);
|
|
4036
|
+
session.dispose();
|
|
4037
|
+
}
|
|
3947
4038
|
ensureConnected() {
|
|
3948
4039
|
const socket = this.client.socket();
|
|
3949
4040
|
if (socket.connected || socket.active) return;
|
|
@@ -4131,7 +4222,7 @@ function resolveAuthToken(options) {
|
|
|
4131
4222
|
|
|
4132
4223
|
// src/version.ts
|
|
4133
4224
|
var SDK_NAME = "agent-client";
|
|
4134
|
-
var SDK_VERSION = true ? "
|
|
4225
|
+
var SDK_VERSION = true ? "2608.0.1" : "1.1.1";
|
|
4135
4226
|
|
|
4136
4227
|
// src/socket.ts
|
|
4137
4228
|
function withSdkIdentity(auth) {
|