@blade-hq/agent-client 1.1.12-beta.4 → 1.1.12-beta.5

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/README.md CHANGED
@@ -196,6 +196,26 @@ chat.dispose() // 彻底释放(什么时候
196
196
 
197
197
  ### 页面协作:让智能体和你的页面互动
198
198
 
199
+ **页面状态与业务后端 → 智能体**:
200
+
201
+ ```ts
202
+ await chat.attachApp({
203
+ cli: {
204
+ command: "library",
205
+ runtime: "python",
206
+ source: new URL("./agent/library_cli.py", location.href),
207
+ },
208
+ getContext: () => ({
209
+ route: location.pathname,
210
+ selectedBookId: bookStore.getState().selectedBookId,
211
+ }),
212
+ })
213
+ ```
214
+
215
+ `getContext` 在每次 `send()` 前执行一次,适合路由、当前选中对象等轻量页面状态;它不做响应式依赖追踪。完整业务数据由开发者自己的 CLI 查询。`command` 是智能体在 Bash 中直接执行的命令名,不会套在 `blade` 命令下。
216
+
217
+ `runtime: "python"` 使用沙盒系统 Python 运行单文件脚本;`runtime: "executable"` 接受能在 Linux 沙盒运行的可执行文件。`source` 可传 `Blob`、`File` 或浏览器可读取的 URL。应用 CLI 安装成功后随会话工作区保留;运行时可通过 `BLADE_APP_CONTEXT_FILE` 读取同一份页面上下文 JSON。
218
+
199
219
  **智能体 → 页面**(例如智能体让地图高亮某个点):
200
220
 
201
221
  ```ts
@@ -365,7 +385,7 @@ function Message({ message }: { message: ChatMessage }) {
365
385
  - **SDK 身份**:`SDK_NAME`、`SDK_VERSION`
366
386
  - **声明式会话**:`SessionDefinition`、`SolutionDefinition`、`SkillDefinition`、`SessionConfig`、`TextFile`、`SessionSetupError`、`SessionSetupStage`
367
387
  - **模型目录**:`ModelsResource`、`ModelCatalog`、`ModelOption`
368
- - **会话资源(REST)**:`SessionsResource`、`CreateSessionRequest`、`PaginatedSessionsResult`、`SessionHistory`、`SessionContextStats`、`ShareLinkResult`、`FileEntry`、`UploadFileEntry`、`UploadFilesOptions`、`SessionProfile`、`SessionDetail`、`SessionInfo`、`SessionStatus`、`SessionPortMapping`、`ModeId`、`TemplateId`、`PrimarySkillSnapshot`、`PrimarySkillParallelMode`
388
+ - **会话资源(REST)**:`SessionsResource`、`CreateSessionRequest`、`AppCliDefinition`、`AppCliAttachment`、`AttachAppOptions`、`PaginatedSessionsResult`、`SessionHistory`、`SessionContextStats`、`ShareLinkResult`、`FileEntry`、`UploadFileEntry`、`UploadFilesOptions`、`SessionProfile`、`SessionDetail`、`SessionInfo`、`SessionStatus`、`SessionPortMapping`、`ModeId`、`TemplateId`、`PrimarySkillSnapshot`、`PrimarySkillParallelMode`
369
389
  - **会话状态机**:`SessionHub`、`SessionState`、`SendOptions`、`ConnectionStatus`、`AskUserAnswerData`、`AgentLoopInfo`、`ActiveCompactionState`、`createInitialSessionState`、`AgentSessionEventName`
370
390
  - **页面协作**:`EmbeddedChat`、`EmbeddedChatOptions`、`CommandHandler`、`CommandEnvelope`、`InboundAction`、`InboundEnvelope`、`isCommandEnvelope`、`isInboundEnvelope`
371
391
  - **消息与投影协议**:`MessageContent`、`MessageContentPart`、`TextContentPart`、`ImageUrlContentPart`、`FileContentPart`、`ToolCallInfo`、`ToolBridgeContent`、`CompactionInfo`、`MemoryRefInfo`、`ArchivedFileInfo`、`ArchivedToolCallInfo`、`TurnProjection`、`ContentBlock`、`PatchEnvelope`、`MemoryRef`、`buildMessageContent`、`normalizeMessageContent`、`isHiddenInternalMessage`、`transformSlashCommand`、`extractTextAttachments`、`ParsedTextAttachment`、`ParsedTextContext`
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@ export { BladeApiError } from "./rest";
4
4
  export type { LoginOptions, LoginResult, TokenStorageMode } from "./auth-login";
5
5
  export { SDK_NAME, SDK_VERSION } from "./version";
6
6
  export { AgentSession } from "./session/agent-session";
7
- export type { SendOptions } from "./session/agent-session";
7
+ export type { AttachAppOptions, SendOptions } from "./session/agent-session";
8
8
  export { SessionHub } from "./session/hub";
9
9
  export type { AgentSessionEvents, AgentSessionEventName } from "./session/events";
10
10
  export { SessionSetupError } from "./session/definition";
@@ -21,7 +21,7 @@ export type { HeadlessResource } from "./resources/headless";
21
21
  export { ModelsResource } from "./resources/models";
22
22
  export type { ModelCatalog, ModelOption } from "./resources/models";
23
23
  export type { SessionsResource } from "./resources/sessions";
24
- export type { CreateSessionRequest, FileEntry, PaginatedSessionsResult, SessionContextStats, SessionHistory, ShareLinkResult, UploadFileEntry, UploadFilesOptions, } from "./resources/sessions";
24
+ export type { CreateSessionRequest, AppCliAttachment, AppCliDefinition, FileEntry, PaginatedSessionsResult, SessionContextStats, SessionHistory, ShareLinkResult, UploadFileEntry, UploadFilesOptions, } from "./resources/sessions";
25
25
  export type { ArchivedFileInfo, ArchivedToolCallInfo, ChatMessage, CompactionInfo, FileContentPart, ImageUrlContentPart, MemoryRefInfo, MessageContent, MessageContentPart, TextContentPart, ToolBridgeContent, ToolCallInfo, } from "./schemas/message";
26
26
  export { buildMessageContent, contentPreview, extractTextAttachments, getFileParts, getImageParts, getTextContent, groupMessagesByLoop, isHiddenInternalMessage, normalizeMessageContent, transformSlashCommand, } from "./schemas/message-utils";
27
27
  export type { ParsedTextAttachment, ParsedTextContext } from "./schemas/message-utils";
package/dist/index.js CHANGED
@@ -892,6 +892,11 @@ function toolStatusFromResult(result) {
892
892
  }
893
893
 
894
894
  // src/shared/projection/builder.ts
895
+ function isConversationTurn(turn) {
896
+ return (turn.role === "user" || turn.role === "assistant") && (turn.kind ?? "message") === "message" && !turn.blocks.some(
897
+ (block) => block.type === "mode_change" || block.type === "plan_status" || block.type === "ask_user_answer"
898
+ );
899
+ }
895
900
  var ClientProjectionBuilder = class {
896
901
  activeTurns = /* @__PURE__ */ new Map();
897
902
  lastTurnIds = /* @__PURE__ */ new Map();
@@ -948,6 +953,8 @@ var ClientProjectionBuilder = class {
948
953
  return this.onAskUserAnswer(loopId, payload);
949
954
  case "user:message":
950
955
  return this.onUserMessage(loopId, payload);
956
+ case "session:post_chat_followup":
957
+ return this.onPostChatFollowup(payload);
951
958
  }
952
959
  if (event.type.startsWith("compaction:"))
953
960
  return this.onCompaction(event.type, loopId, payload);
@@ -1005,6 +1012,18 @@ var ClientProjectionBuilder = class {
1005
1012
  this.seq = maxSeq;
1006
1013
  }
1007
1014
  }
1015
+ _seedTurns(turns) {
1016
+ const latestTurns = /* @__PURE__ */ new Map();
1017
+ for (const turn of turns) {
1018
+ this.seedSequence(turn.sequence);
1019
+ if (!isConversationTurn(turn)) continue;
1020
+ const current = latestTurns.get(turn.loop_id);
1021
+ if (!current || turn.sequence >= current.sequence) {
1022
+ latestTurns.set(turn.loop_id, turn);
1023
+ }
1024
+ }
1025
+ this.latestTurns = latestTurns;
1026
+ }
1008
1027
  // --- Event handlers ---
1009
1028
  onMemoryInject(loopId, payload) {
1010
1029
  const refs = payload.memory_refs;
@@ -1193,6 +1212,27 @@ var ClientProjectionBuilder = class {
1193
1212
  usage: null,
1194
1213
  duration_ms: 0
1195
1214
  };
1215
+ this.latestTurns.set("root", turn);
1216
+ return [{ kind: "upsert", turn }];
1217
+ }
1218
+ onPostChatFollowup(payload) {
1219
+ const assistantEntryId = String(payload.assistant_entry_id ?? "");
1220
+ const latest = this.latestTurns.get("root");
1221
+ if (!latest || latest.role !== "assistant" || latest.turn_id !== assistantEntryId) {
1222
+ return null;
1223
+ }
1224
+ const suggestions = payload.suggestions;
1225
+ const postChatFollowup = Array.isArray(suggestions) && suggestions.length === 3 && suggestions.every((value) => typeof value === "string") ? {
1226
+ assistant_entry_id: assistantEntryId,
1227
+ recaption: String(payload.recaption ?? ""),
1228
+ suggestions
1229
+ } : null;
1230
+ if (!postChatFollowup) return null;
1231
+ const turn = {
1232
+ ...latest,
1233
+ post_chat_followup: postChatFollowup
1234
+ };
1235
+ this.latestTurns.set("root", turn);
1196
1236
  return [{ kind: "upsert", turn }];
1197
1237
  }
1198
1238
  // --- Delta / streaming events ---
@@ -1545,11 +1585,24 @@ function projectHistory(entries) {
1545
1585
  const toolOwners = /* @__PURE__ */ new Map();
1546
1586
  const latestAssistantByLoop = /* @__PURE__ */ new Map();
1547
1587
  const pendingChildPauses = /* @__PURE__ */ new Map();
1588
+ let latestRootMessage = null;
1548
1589
  let sequence = 0;
1549
1590
  for (const entry of entries) {
1550
1591
  const kind = String(entry.kind ?? "");
1551
1592
  const loopId = String(entry.loop_name ?? "root");
1552
1593
  const id = String(entry.id ?? `history-${sequence + 1}`);
1594
+ if (kind === "post_chat_followup" && isRecord3(entry.data)) {
1595
+ const assistantEntryId = String(entry.data.assistant_entry_id ?? "");
1596
+ const suggestions = entry.data.suggestions;
1597
+ if (latestRootMessage?.role === "assistant" && latestRootMessage.turn_id === assistantEntryId && Array.isArray(suggestions) && suggestions.length === 3 && suggestions.every((value) => typeof value === "string")) {
1598
+ latestRootMessage.post_chat_followup = {
1599
+ assistant_entry_id: assistantEntryId,
1600
+ recaption: String(entry.data.recaption ?? ""),
1601
+ suggestions
1602
+ };
1603
+ }
1604
+ continue;
1605
+ }
1553
1606
  if (kind === "message" && isRecord3(entry.message)) {
1554
1607
  const message = entry.message;
1555
1608
  const role = String(message.role ?? "");
@@ -1586,6 +1639,9 @@ function projectHistory(entries) {
1586
1639
  duration_ms: numberOrZero(message._duration_ms),
1587
1640
  started_at: typeof entry.timestamp === "string" ? entry.timestamp : void 0
1588
1641
  };
1642
+ if (loopId === "root" && (role === "user" || role === "assistant")) {
1643
+ latestRootMessage = turn;
1644
+ }
1589
1645
  turns.push(turn);
1590
1646
  if (role === "assistant") latestAssistantByLoop.set(loopId, turn);
1591
1647
  for (const toolCall of toolCalls) toolOwners.set(toolCall.id, turn);
@@ -2106,6 +2162,30 @@ var SessionsResource = class {
2106
2162
  );
2107
2163
  return response.json();
2108
2164
  }
2165
+ /** 把应用自己的业务 CLI 安装到会话沙盒,并保留开发者声明的命令名。 */
2166
+ async attachAppCli(sessionId, definition) {
2167
+ let source;
2168
+ if (definition.source instanceof Blob) {
2169
+ source = definition.source;
2170
+ } else {
2171
+ const response2 = await globalThis.fetch(definition.source.toString());
2172
+ if (!response2.ok) {
2173
+ throw new Error(`\u8BFB\u53D6\u5E94\u7528 CLI \u5931\u8D25\uFF1AHTTP ${response2.status}`);
2174
+ }
2175
+ source = await response2.blob();
2176
+ }
2177
+ const filename = definition.filename ?? (typeof File !== "undefined" && definition.source instanceof File ? definition.source.name : void 0) ?? (definition.runtime === "python" ? "app-cli.py" : "app-cli");
2178
+ const form = new FormData();
2179
+ form.append("command", definition.command);
2180
+ form.append("runtime", definition.runtime);
2181
+ form.append("file", source, filename);
2182
+ const response = await this.client.formData(
2183
+ "POST",
2184
+ `/api/sessions/${sessionId}/app-cli`,
2185
+ form
2186
+ );
2187
+ return response.json();
2188
+ }
2109
2189
  async deleteFile(sessionId, filePath) {
2110
2190
  await this.client.json("DELETE", `/api/sessions/${sessionId}/files/${encodeURIComponent(filePath)}`);
2111
2191
  }
@@ -2936,6 +3016,8 @@ var AgentSession = class _AgentSession {
2936
3016
  pendingReplayMode;
2937
3017
  recentChatEndAt = 0;
2938
3018
  disposed = false;
3019
+ getAppContext = null;
3020
+ sendPending = false;
2939
3021
  deliveredCommandKeys = /* @__PURE__ */ new Set();
2940
3022
  deliveredToolPreviews = /* @__PURE__ */ new Map();
2941
3023
  oomNotifiedKeys = /* @__PURE__ */ new Set();
@@ -2962,6 +3044,7 @@ var AgentSession = class _AgentSession {
2962
3044
  listDir: async () => readonlyError(),
2963
3045
  countFiles: async () => readonlyError(),
2964
3046
  downloadFile: async () => readonlyError(),
3047
+ attachAppCli: async () => readonlyError(),
2965
3048
  onDispose: () => void 0
2966
3049
  },
2967
3050
  "connected"
@@ -3016,18 +3099,58 @@ var AgentSession = class _AgentSession {
3016
3099
  return this.commands.onCommand(action, handler);
3017
3100
  }
3018
3101
  // ---------------------------------------------------------------- 动作
3102
+ /**
3103
+ * 声明当前应用提供的页面上下文和业务 CLI。
3104
+ *
3105
+ * CLI 安装成功后才保存 getContext;之后每次 send 前恰好读取一次。
3106
+ */
3107
+ async attachApp(options) {
3108
+ const attachment = options.cli ? await this.runtime.attachAppCli(this.sessionId, options.cli) : null;
3109
+ this.getAppContext = options.getContext;
3110
+ return attachment;
3111
+ }
3019
3112
  /** 发送一条消息。字符串或多模态内容均可。 */
3020
3113
  async send(content, options = {}) {
3021
3114
  this.runtime.ensureConnected();
3022
- if (this.state.isStreaming) {
3115
+ if (this.state.isStreaming || this.sendPending) {
3023
3116
  this.update((s) => addErrorMessage(s, "\u5F53\u524D\u56DE\u590D\u5C1A\u672A\u7ED3\u675F\uFF0C\u8BF7\u7A0D\u540E\u518D\u53D1\u9001\u3002"));
3024
3117
  return;
3025
3118
  }
3119
+ this.sendPending = true;
3120
+ try {
3121
+ await this.sendUnlocked(content, options);
3122
+ } finally {
3123
+ this.sendPending = false;
3124
+ }
3125
+ }
3126
+ async sendUnlocked(content, options) {
3026
3127
  await this.ensureIdleBeforeSend();
3027
3128
  if (this.state.isStreaming) {
3028
3129
  this.update((s) => addErrorMessage(s, "\u5F53\u524D\u56DE\u590D\u5C1A\u672A\u7ED3\u675F\uFF0C\u8BF7\u7A0D\u540E\u518D\u53D1\u9001\u3002"));
3029
3130
  return;
3030
3131
  }
3132
+ let appContext;
3133
+ if (this.getAppContext) {
3134
+ try {
3135
+ const rawContext = await this.getAppContext();
3136
+ if (!isRecord5(rawContext)) {
3137
+ throw new Error("getContext \u5FC5\u987B\u8FD4\u56DE JSON \u5BF9\u8C61");
3138
+ }
3139
+ const serialized = JSON.stringify(rawContext);
3140
+ if (serialized === void 0) {
3141
+ throw new Error("getContext \u5FC5\u987B\u8FD4\u56DE\u53EF\u5E8F\u5217\u5316\u7684 JSON \u5BF9\u8C61");
3142
+ }
3143
+ const normalized = JSON.parse(serialized);
3144
+ if (!isRecord5(normalized)) {
3145
+ throw new Error("getContext \u5FC5\u987B\u8FD4\u56DE JSON \u5BF9\u8C61");
3146
+ }
3147
+ appContext = normalized;
3148
+ } catch (error) {
3149
+ const message = error instanceof Error && error.message ? `\u8BFB\u53D6\u5E94\u7528\u4E0A\u4E0B\u6587\u5931\u8D25\uFF1A${error.message}` : "\u8BFB\u53D6\u5E94\u7528\u4E0A\u4E0B\u6587\u5931\u8D25\uFF0C\u6D88\u606F\u672A\u53D1\u9001";
3150
+ this.update((s) => addErrorMessage(s, message));
3151
+ return;
3152
+ }
3153
+ }
3031
3154
  const askUserAnswer = options.askUserAnswer;
3032
3155
  if (!(askUserAnswer && typeof askUserAnswer.tool_call_id === "string")) {
3033
3156
  this.update((s) => addUserMessage(s, content));
@@ -3052,7 +3175,8 @@ var AgentSession = class _AgentSession {
3052
3175
  model: options.model,
3053
3176
  thinking_override: options.thinkingOverride,
3054
3177
  whatif: options.whatif,
3055
- replay_decision: options.replayDecision
3178
+ replay_decision: options.replayDecision,
3179
+ app_context: appContext
3056
3180
  };
3057
3181
  try {
3058
3182
  await this.ensureJoined();
@@ -3169,11 +3293,14 @@ var AgentSession = class _AgentSession {
3169
3293
  _handleTurnEvents(events) {
3170
3294
  if (!events.length) return;
3171
3295
  const terminalChatEndStatus = extractTerminalChatEndStatus(events);
3172
- if (!terminalChatEndStatus) {
3296
+ const followupOnly = events.every(
3297
+ (event) => event.type === "session:post_chat_followup"
3298
+ );
3299
+ if (!terminalChatEndStatus && !followupOnly) {
3173
3300
  this.markRunningIfIdle();
3174
3301
  }
3175
3302
  if (this.state.turns.length) {
3176
- this.projectionBuilder.seedSequence(Math.max(...this.state.turns.map((t) => t.sequence)));
3303
+ this.projectionBuilder._seedTurns(this.state.turns);
3177
3304
  }
3178
3305
  const updates = this.projectionBuilder.processBatch(events);
3179
3306
  for (const update of updates) {
@@ -3633,6 +3760,7 @@ var SessionHub = class {
3633
3760
  listDir: (sessionId, dirPath) => this.client.sessions.listDir(sessionId, dirPath),
3634
3761
  countFiles: (sessionId, dirPath) => this.client.sessions.countFiles(sessionId, dirPath),
3635
3762
  downloadFile: (sessionId, filePath, downloadName) => this.client.sessions.downloadFile(sessionId, filePath, downloadName),
3763
+ attachAppCli: (sessionId, definition) => this.client.sessions.attachAppCli(sessionId, definition),
3636
3764
  onDispose: (session) => {
3637
3765
  if (this.sessions.get(session.sessionId) === session) {
3638
3766
  this.sessions.delete(session.sessionId);
@@ -3825,7 +3953,7 @@ function resolveAuthToken(options) {
3825
3953
 
3826
3954
  // src/version.ts
3827
3955
  var SDK_NAME = "agent-client";
3828
- var SDK_VERSION = true ? "1.1.12-beta.4" : "1.1.1";
3956
+ var SDK_VERSION = true ? "1.1.12-beta.5" : "1.1.1";
3829
3957
 
3830
3958
  // src/socket.ts
3831
3959
  function withSdkIdentity(auth) {