@blade-hq/agent-client 1.2.1-alpha.4 → 1.2.1-beta.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/README.md CHANGED
@@ -182,7 +182,7 @@ const unsubscribe = chat.subscribe(() => rerender(chat.getState()))
182
182
  ### 动作
183
183
 
184
184
  ```ts
185
- await chat.send("你好") // 发消息(字符串或多模态内容)
185
+ const accepted = await chat.send("你好") // 服务端接受后为 true;拒绝时为 false
186
186
  await chat.send("换个方案", { mode: "planning" }) // 指定模式/模型等选项
187
187
  chat.append("补充:预算不超过 5 万") // 智能体运行中追加说明
188
188
  await chat.stop() // 停止当前回复
@@ -389,6 +389,6 @@ function Message({ message }: { message: ChatMessage }) {
389
389
  - **会话状态机**:`SessionHub`、`SessionState`、`SendOptions`、`ConnectionStatus`、`AskUserAnswerData`、`AgentLoopInfo`、`ActiveCompactionState`、`createInitialSessionState`、`AgentSessionEventName`
390
390
  - **页面协作**:`EmbeddedChat`、`EmbeddedChatOptions`、`CommandHandler`、`CommandEnvelope`、`InboundAction`、`InboundEnvelope`、`isCommandEnvelope`、`isInboundEnvelope`
391
391
  - **消息与投影协议**:`MessageContent`、`MessageContentPart`、`TextContentPart`、`ImageUrlContentPart`、`FileContentPart`、`ToolCallInfo`、`ToolBridgeContent`、`CompactionInfo`、`MemoryRefInfo`、`ArchivedFileInfo`、`ArchivedToolCallInfo`、`TurnProjection`、`ContentBlock`、`PatchEnvelope`、`MemoryRef`、`buildMessageContent`、`normalizeMessageContent`、`isHiddenInternalMessage`、`transformSlashCommand`、`extractTextAttachments`、`ParsedTextAttachment`、`ParsedTextContext`
392
- - **Solution / 任务协议**:`Solution`、`SolutionAppField`、`SolutionAppState`、`SolutionAppUiConfig`、`SolutionRef`、`PublishedSolutionRef`、`ExistingSolutionRef`、`PreparedSolution`、`PreparedSolutionAsset`、`LayoutType`、`BizRole`、`TaskStatus`、`BackgroundTask`
392
+ - **Solution / 任务协议**:`Solution`、`SolutionAppField`、`SolutionAppState`、`SolutionAppUiConfig`、`SolutionRef`、`PublishedSolutionRef`、`ExistingSolutionRef`、`PreparedSolution`、`PreparedSolutionAsset`、`LayoutType`、`BizRole`、`TaskStatus`、`BackgroundTask`、`BackgroundTaskStopResult`
393
393
  - **Headless**:`HeadlessResource`、`RunOptions`、`RunResult`、`RunTrace`
394
394
  - **低层通道(apps/web 等高级集成)**:`createSocket`、`CreateSocketOptions`、`TypedSocket`、`AsrAudioPayload`、`ClientProjectionBuilder`、`RawEvent`
package/dist/index.d.ts CHANGED
@@ -31,7 +31,7 @@ export type { ModeId, PrimarySkillParallelMode, PrimarySkillSnapshot, SessionDet
31
31
  export { LayoutType } from "./schemas/solution";
32
32
  export type { BizRole, Solution, SolutionAppField, SolutionAppState, SolutionAppUiConfig, } from "./schemas/solution";
33
33
  export { Task, TaskStatus } from "./schemas/task";
34
- export type { BackgroundTask } from "./schemas/background";
34
+ export type { BackgroundTask, BackgroundTaskStopResult } from "./schemas/background";
35
35
  export { ClientProjectionBuilder } from "./shared/projection";
36
36
  export type { RawEvent } from "./shared/projection";
37
37
  export { createSocket } from "./socket";
package/dist/index.js CHANGED
@@ -319,13 +319,22 @@ var HeadlessResource = class {
319
319
  socket.on("chat:end", onChatEnd);
320
320
  socket.on("system:error", onSystemError);
321
321
  socket.emit("session:subscribe", { session_id, event_format: "raw" });
322
- socket.emit("chat:send", {
323
- session_id,
324
- message: prompt,
325
- headless: true,
326
- output_schema: options.schema ?? null,
327
- model: options.model ?? null
328
- });
322
+ socket.emit(
323
+ "chat:send",
324
+ {
325
+ session_id,
326
+ message: prompt,
327
+ headless: true,
328
+ output_schema: options.schema ?? null,
329
+ model: options.model ?? null
330
+ },
331
+ (response) => {
332
+ if (response.status === "accepted") return;
333
+ rejectAfterChatEnd(
334
+ new HeadlessError(response.message ?? "headless run was not accepted", response)
335
+ );
336
+ }
337
+ );
329
338
  });
330
339
  }
331
340
  };
@@ -3037,6 +3046,11 @@ function extractTerminalChatEndStatus(events) {
3037
3046
  }
3038
3047
  return null;
3039
3048
  }
3049
+ function hasChatRunEvent(events) {
3050
+ return events.some(
3051
+ (event) => event.type !== "session:post_chat_followup" && !event.type.startsWith("compaction:")
3052
+ );
3053
+ }
3040
3054
  function isUiMetaLike(value) {
3041
3055
  return isRecord5(value) && ("resourceHTML" in value || "resourceUri" in value || "resourceURI" in value);
3042
3056
  }
@@ -3059,6 +3073,7 @@ var AgentSession = class _AgentSession {
3059
3073
  disposed = false;
3060
3074
  getAppContext = null;
3061
3075
  sendPending = false;
3076
+ manualCompactionPending = false;
3062
3077
  deliveredCommandKeys = /* @__PURE__ */ new Set();
3063
3078
  deliveredToolPreviews = /* @__PURE__ */ new Map();
3064
3079
  oomNotifiedKeys = /* @__PURE__ */ new Set();
@@ -3153,22 +3168,26 @@ var AgentSession = class _AgentSession {
3153
3168
  /** 发送一条消息。字符串或多模态内容均可。 */
3154
3169
  async send(content, options = {}) {
3155
3170
  this.runtime.ensureConnected();
3156
- if (this.state.isStreaming || this.sendPending) {
3157
- this.update((s) => addErrorMessage(s, "\u5F53\u524D\u56DE\u590D\u5C1A\u672A\u7ED3\u675F\uFF0C\u8BF7\u7A0D\u540E\u518D\u53D1\u9001\u3002"));
3158
- return;
3171
+ if (this.state.isStreaming || this.state.activeCompaction || this.sendPending || this.manualCompactionPending) {
3172
+ this.update(
3173
+ (s) => addErrorMessage(s, "\u5F53\u524D\u56DE\u590D\u5C1A\u672A\u7ED3\u675F\u6216\u5BF9\u8BDD\u6B63\u5728\u6574\u7406\uFF0C\u8BF7\u7A0D\u540E\u518D\u53D1\u9001\u3002")
3174
+ );
3175
+ return false;
3159
3176
  }
3160
3177
  this.sendPending = true;
3161
3178
  try {
3162
- await this.sendUnlocked(content, options);
3179
+ return await this.sendUnlocked(content, options);
3163
3180
  } finally {
3164
3181
  this.sendPending = false;
3165
3182
  }
3166
3183
  }
3167
3184
  async sendUnlocked(content, options) {
3168
3185
  await this.ensureIdleBeforeSend();
3169
- if (this.state.isStreaming) {
3170
- this.update((s) => addErrorMessage(s, "\u5F53\u524D\u56DE\u590D\u5C1A\u672A\u7ED3\u675F\uFF0C\u8BF7\u7A0D\u540E\u518D\u53D1\u9001\u3002"));
3171
- return;
3186
+ if (this.state.isStreaming || this.state.activeCompaction || this.manualCompactionPending) {
3187
+ this.update(
3188
+ (s) => addErrorMessage(s, "\u5F53\u524D\u56DE\u590D\u5C1A\u672A\u7ED3\u675F\u6216\u5BF9\u8BDD\u6B63\u5728\u6574\u7406\uFF0C\u8BF7\u7A0D\u540E\u518D\u53D1\u9001\u3002")
3189
+ );
3190
+ return false;
3172
3191
  }
3173
3192
  let appContext;
3174
3193
  if (this.getAppContext) {
@@ -3189,19 +3208,28 @@ var AgentSession = class _AgentSession {
3189
3208
  } catch (error) {
3190
3209
  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";
3191
3210
  this.update((s) => addErrorMessage(s, message));
3192
- return;
3211
+ return false;
3193
3212
  }
3194
3213
  }
3195
3214
  const askUserAnswer = options.askUserAnswer;
3215
+ let optimisticTurnId = null;
3216
+ let optimisticAskAnswer;
3217
+ let previousAskAnswer;
3196
3218
  if (!(askUserAnswer && typeof askUserAnswer.tool_call_id === "string")) {
3197
- this.update((s) => addUserMessage(s, content));
3219
+ this.update((state) => {
3220
+ const next = addUserMessage(state, content);
3221
+ optimisticTurnId = next.turns.at(-1)?.turn_id ?? null;
3222
+ return next;
3223
+ });
3198
3224
  }
3199
3225
  this.update((s) => ({ ...s, isStreaming: true }));
3200
3226
  if (askUserAnswer && typeof askUserAnswer.tool_call_id === "string") {
3201
3227
  const { tool_call_id, ...rest } = askUserAnswer;
3228
+ previousAskAnswer = this.state.askAnswers[tool_call_id];
3229
+ optimisticAskAnswer = rest;
3202
3230
  this.update((s) => ({
3203
3231
  ...s,
3204
- askAnswers: { ...s.askAnswers, [tool_call_id]: rest }
3232
+ askAnswers: { ...s.askAnswers, [tool_call_id]: optimisticAskAnswer }
3205
3233
  }));
3206
3234
  }
3207
3235
  if (!options.replayDecision) {
@@ -3222,15 +3250,54 @@ var AgentSession = class _AgentSession {
3222
3250
  try {
3223
3251
  await this.ensureJoined();
3224
3252
  } catch (error) {
3225
- this.update(
3226
- (s) => addErrorMessage(
3227
- { ...s, isStreaming: false, status: "failed" },
3228
- error instanceof Error && error.message ? error.message : "\u4F1A\u8BDD\u8BA2\u9605\u5931\u8D25\uFF0C\u6D88\u606F\u672A\u53D1\u9001"
3229
- )
3253
+ this.rollbackOptimisticSend(
3254
+ optimisticTurnId,
3255
+ askUserAnswer,
3256
+ optimisticAskAnswer,
3257
+ previousAskAnswer,
3258
+ error instanceof Error && error.message ? error.message : "\u4F1A\u8BDD\u8BA2\u9605\u5931\u8D25\uFF0C\u6D88\u606F\u672A\u53D1\u9001"
3230
3259
  );
3231
- return;
3260
+ return false;
3232
3261
  }
3233
- this.runtime.emitEvent("chat:send", payload);
3262
+ try {
3263
+ const response = await this.runtime.emitWithAck("chat:send", payload, 3e4);
3264
+ if (response?.status !== "accepted") {
3265
+ throw new Error(
3266
+ typeof response?.message === "string" ? response.message : "\u6D88\u606F\u672A\u88AB\u670D\u52A1\u7AEF\u63A5\u53D7"
3267
+ );
3268
+ }
3269
+ return true;
3270
+ } catch (error) {
3271
+ this.rollbackOptimisticSend(
3272
+ optimisticTurnId,
3273
+ askUserAnswer,
3274
+ optimisticAskAnswer,
3275
+ previousAskAnswer,
3276
+ error instanceof Error && error.message ? error.message : "\u6D88\u606F\u53D1\u9001\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5"
3277
+ );
3278
+ return false;
3279
+ }
3280
+ }
3281
+ rollbackOptimisticSend(optimisticTurnId, askUserAnswer, optimisticAskAnswer, previousAskAnswer, message) {
3282
+ this.pendingReplayMessage = null;
3283
+ this.pendingReplayMode = void 0;
3284
+ this.update((current) => {
3285
+ let next = optimisticTurnId ? withTurns(
3286
+ current,
3287
+ current.turns.filter((turn) => turn.turn_id !== optimisticTurnId)
3288
+ ) : current;
3289
+ const toolCallId = askUserAnswer && typeof askUserAnswer.tool_call_id === "string" ? askUserAnswer.tool_call_id : null;
3290
+ if (toolCallId && next.askAnswers[toolCallId] === optimisticAskAnswer) {
3291
+ const askAnswers = { ...next.askAnswers };
3292
+ if (previousAskAnswer === void 0) {
3293
+ delete askAnswers[toolCallId];
3294
+ } else {
3295
+ askAnswers[toolCallId] = previousAskAnswer;
3296
+ }
3297
+ next = { ...next, askAnswers };
3298
+ }
3299
+ return addErrorMessage({ ...next, isStreaming: false }, message);
3300
+ });
3234
3301
  }
3235
3302
  /** 智能体运行期间追加补充说明(不打断当前回复)。 */
3236
3303
  append(text) {
@@ -3244,7 +3311,28 @@ var AgentSession = class _AgentSession {
3244
3311
  }
3245
3312
  /** 手动触发上下文压缩。 */
3246
3313
  compact() {
3247
- return this.runtime.emitWithAck("chat:compact", { session_id: this.sessionId }, 3e4).then((response) => response ?? {});
3314
+ if (this.state.isStreaming || this.state.activeCompaction || this.manualCompactionPending) {
3315
+ return Promise.resolve({
3316
+ status: "error",
3317
+ message: "\u5F53\u524D\u56DE\u590D\u5C1A\u672A\u7ED3\u675F\u6216\u5BF9\u8BDD\u6B63\u5728\u6574\u7406\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002"
3318
+ });
3319
+ }
3320
+ this.manualCompactionPending = true;
3321
+ this.update((state) => ({
3322
+ ...state,
3323
+ activeCompaction: {
3324
+ turn_id: "pending-manual-compaction",
3325
+ compaction_id: "pending-manual-compaction",
3326
+ status: "streaming",
3327
+ trigger: "manual"
3328
+ }
3329
+ }));
3330
+ return this.runtime.emitWithAck("chat:compact", { session_id: this.sessionId }, 3e4).then((response) => response ?? {}).finally(() => {
3331
+ this.manualCompactionPending = false;
3332
+ if (this.state.activeCompaction?.compaction_id === "pending-manual-compaction") {
3333
+ this.update((state) => ({ ...state, activeCompaction: null }));
3334
+ }
3335
+ });
3248
3336
  }
3249
3337
  /**
3250
3338
  * 把一段业务数据作为附件放进聊天输入框(比如地图上选中的点位)。
@@ -3334,10 +3422,7 @@ var AgentSession = class _AgentSession {
3334
3422
  _handleTurnEvents(events) {
3335
3423
  if (!events.length) return;
3336
3424
  const terminalChatEndStatus = extractTerminalChatEndStatus(events);
3337
- const followupOnly = events.every(
3338
- (event) => event.type === "session:post_chat_followup"
3339
- );
3340
- if (!terminalChatEndStatus && !followupOnly) {
3425
+ if (!terminalChatEndStatus && hasChatRunEvent(events)) {
3341
3426
  this.markRunningIfIdle();
3342
3427
  }
3343
3428
  if (this.state.turns.length) {
@@ -3411,11 +3496,30 @@ var AgentSession = class _AgentSession {
3411
3496
  this.pendingReplayMessage = null;
3412
3497
  this.pendingReplayMode = void 0;
3413
3498
  const respond = (decision) => {
3414
- this.runtime.emitEvent("chat:send", {
3415
- session_id: this.sessionId,
3416
- message,
3417
- mode,
3418
- replay_decision: decision
3499
+ void this.runtime.emitWithAck(
3500
+ "chat:send",
3501
+ {
3502
+ session_id: this.sessionId,
3503
+ message,
3504
+ mode,
3505
+ replay_decision: decision
3506
+ },
3507
+ 3e4
3508
+ ).then((response) => {
3509
+ if (response?.status === "accepted") return;
3510
+ this.update(
3511
+ (state) => addErrorMessage(
3512
+ { ...state, isStreaming: false },
3513
+ typeof response?.message === "string" ? response.message : "\u6D88\u606F\u672A\u88AB\u670D\u52A1\u7AEF\u63A5\u53D7"
3514
+ )
3515
+ );
3516
+ }).catch((error) => {
3517
+ this.update(
3518
+ (state) => addErrorMessage(
3519
+ { ...state, isStreaming: false },
3520
+ error instanceof Error && error.message ? error.message : "\u6D88\u606F\u53D1\u9001\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5"
3521
+ )
3522
+ );
3419
3523
  });
3420
3524
  };
3421
3525
  const handled = this.emitter.emitFirst("replayMismatch", {
@@ -3697,6 +3801,18 @@ var AgentSession = class _AgentSession {
3697
3801
  };
3698
3802
 
3699
3803
  // src/session/hub.ts
3804
+ var BACKGROUND_STATUSES = [
3805
+ "starting",
3806
+ "running",
3807
+ "cancelling",
3808
+ "succeeded",
3809
+ "failed",
3810
+ "cancelled",
3811
+ "lost"
3812
+ ];
3813
+ function normalizeBackgroundStatus(value) {
3814
+ return BACKGROUND_STATUSES.includes(value) ? value : "running";
3815
+ }
3700
3816
  var SessionHub = class {
3701
3817
  client;
3702
3818
  sessions = /* @__PURE__ */ new Map();
@@ -3951,7 +4067,11 @@ var SessionHub = class {
3951
4067
  id: taskId,
3952
4068
  command: typeof task.command === "string" ? task.command : "",
3953
4069
  description: typeof task.description === "string" ? task.description : void 0,
3954
- status: task.status === "running" || task.status === "failed" ? task.status : "done",
4070
+ // Pass the server's status through unchanged. Collapsing it here
4071
+ // made subscribers see a successful-looking "done" for tasks the
4072
+ // REST API correctly reported as cancelled or lost.
4073
+ status: normalizeBackgroundStatus(task.status),
4074
+ failureReason: typeof task.failure_reason === "string" ? task.failure_reason : void 0,
3955
4075
  exitCode: typeof task.exit_code === "number" || task.exit_code === null ? task.exit_code : void 0,
3956
4076
  elapsedSeconds: typeof task.elapsed_seconds === "number" ? task.elapsed_seconds : 0,
3957
4077
  output: Array.isArray(task.output_preview) && task.output_preview.length > 0 ? task.output_preview.join("\n") : void 0
@@ -3994,7 +4114,7 @@ function resolveAuthToken(options) {
3994
4114
 
3995
4115
  // src/version.ts
3996
4116
  var SDK_NAME = "agent-client";
3997
- var SDK_VERSION = true ? "1.2.1-alpha.4" : "1.1.1";
4117
+ var SDK_VERSION = true ? "1.2.1-beta.1" : "1.1.1";
3998
4118
 
3999
4119
  // src/socket.ts
4000
4120
  function withSdkIdentity(auth) {