lark-remote 0.5.8 → 0.5.10

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.
@@ -19,6 +19,7 @@
19
19
  */
20
20
  import { DshClient, DshError, newPromptRequestId } from './client.js';
21
21
  import { DshTranslator } from './translator.js';
22
+ import { goalFromChange, goalFromProjection, goalStateEvent, sameGoal } from './goal-view.js';
22
23
  import { DshApprovalTracker } from './approval.js';
23
24
  import { mergeAsync } from './merge.js';
24
25
  import { getLogger } from '../../logger/index.js';
@@ -81,6 +82,11 @@ function buildQuestionAnswers(questions, response) {
81
82
  }
82
83
  /** Budget for one session/cancel attempt on stop. */
83
84
  const CANCEL_TIMEOUT_MS = 5_000;
85
+ /**
86
+ * goal 探测的预算。goal 是"增强信息":读不到就按没有(known:false)继续跑,
87
+ * 但绝不允许一次慢投影把整个 run 的启动/收尾拖住。
88
+ */
89
+ const GOAL_PROBE_TIMEOUT_MS = 5_000;
84
90
  /** Total stop budget: withdraw + cancel + verify must not hold the lane forever. */
85
91
  const STOP_BUDGET_MS = 5_000;
86
92
  /** A prompt request that was still in flight can land after stop's abort. */
@@ -434,6 +440,18 @@ export class DshRunner {
434
440
  cwd: opts.cwd,
435
441
  model: this.configuredModel ?? 'DSH',
436
442
  };
443
+ // Goal seed:卡片从第一帧就知道这个会话有没有长期目标。放在 init 之后、
444
+ // prompt 之前——用户发消息时若是 queued,卡片会长时间停在「运行中」,
445
+ // 这期间"目标是什么、第几轮"是唯一能解释"为什么还不停"的信息。
446
+ const seedGoal = await this.client.goalState(sessionId, {
447
+ signal: abort.signal,
448
+ timeoutMs: GOAL_PROBE_TIMEOUT_MS,
449
+ });
450
+ const lastGoal = seedGoal.known ? seedGoal.goal : undefined;
451
+ // 只有确实存在 goal 才产事件:没有 goal 的会话保持事件序列不变(少一次
452
+ // 渲染,也不让"读不到"变成一张多出来的卡片事件)。
453
+ if (seedGoal.known && seedGoal.goal)
454
+ yield goalStateEvent(seedGoal.goal, true);
437
455
  try {
438
456
  // Snapshot the pre-prompt high-water mark before mutating the session.
439
457
  // DSH session/page returns the whole session, so starting from seq 0
@@ -476,7 +494,7 @@ export class DshRunner {
476
494
  return;
477
495
  }
478
496
  try {
479
- for await (const ev of this.runTurn(sessionId, opts.cwd, prePromptSeq, promptRequestId, abort)) {
497
+ for await (const ev of this.runTurn(sessionId, opts.cwd, prePromptSeq, promptRequestId, abort, { last: lastGoal })) {
480
498
  yield ev;
481
499
  if (ev.type === 'result') {
482
500
  terminalEmitted = true;
@@ -550,7 +568,7 @@ export class DshRunner {
550
568
  * events; `$events` owns the answerable approval waterfall. The merged
551
569
  * stream keeps either transport from starving the other.
552
570
  */
553
- async *runTurn(sessionId, cwd, prePromptSeq, promptRequestId, abort) {
571
+ async *runTurn(sessionId, cwd, prePromptSeq, promptRequestId, abort, goal) {
554
572
  const streamAbort = new AbortController();
555
573
  const signal = AbortSignal.any([abort.signal, streamAbort.signal]);
556
574
  const translator = new DshTranslator();
@@ -577,6 +595,16 @@ export class DshRunner {
577
595
  // 扩展的,不能因为多了个 key 就报错)。
578
596
  if (item.sessionId !== sessionId)
579
597
  continue;
598
+ // goal 是会话级长期目标,单独成事件:卡片要在 agent 创建目标的当下
599
+ // 就显示它(否则用户只会在一小时后看到「完成」才发现有个 40 轮任务)。
600
+ if (item.key === 'goal') {
601
+ const parsed = goalFromProjection(item.value);
602
+ if (parsed !== undefined && !sameGoal(parsed, goal.last)) {
603
+ goal.last = parsed;
604
+ yield goalStateEvent(parsed, true);
605
+ }
606
+ continue;
607
+ }
580
608
  // 只在消费字段真的变了时才产出事件:投影帧本身按 key 去重推送,
581
609
  // 这里再挡掉"值没变"的重复帧。不需要时间节流——卡片更新本来就有
582
610
  // 100ms 合批窗口,与正文 delta 共用同一条节流路径。
@@ -654,6 +682,16 @@ export class DshRunner {
654
682
  continue;
655
683
  }
656
684
  const event = item.event;
685
+ if (event.type === 'goal/change') {
686
+ // 权威变更(create/edit/pause/resume/complete/block/clear):不等投影
687
+ // 推送,直接按事件更新——agent 刚 create 的那一刻卡片就该显示目标。
688
+ const parsed = goalFromChange(event.data);
689
+ if (parsed !== undefined && !sameGoal(parsed, goal.last)) {
690
+ goal.last = parsed;
691
+ yield goalStateEvent(parsed, true);
692
+ }
693
+ continue;
694
+ }
657
695
  if (event.type === 'tool/call') {
658
696
  const data = event.data;
659
697
  if (data.callId && data.name) {
@@ -686,6 +724,17 @@ export class DshRunner {
686
724
  for (const jobsEvent of await this.collectBackgroundJobs(sessionId, abort.signal)) {
687
725
  yield jobsEvent;
688
726
  }
727
+ // 收尾复核:这一轮结束时 goal 是否仍 active,决定卡片能不能收尾。
728
+ // 必须现读宿主,不能沿用过程中的最后一次观测——goal 可能在本轮
729
+ // 最后一个 step 才被 complete,也可能刚被 driver 置为 blocked。
730
+ const finalGoal = await this.client.goalState(sessionId, {
731
+ signal,
732
+ timeoutMs: GOAL_PROBE_TIMEOUT_MS,
733
+ });
734
+ if (finalGoal.known && !sameGoal(finalGoal.goal, goal.last)) {
735
+ goal.last = finalGoal.goal;
736
+ yield goalStateEvent(finalGoal.goal, true);
737
+ }
689
738
  }
690
739
  yield attachContextLimit(ev, live.contextLimit);
691
740
  return;
@@ -15,6 +15,6 @@ export { KimiAcpRunner } from './kimi/index.js';
15
15
  export { DshRunner } from './dsh/index.js';
16
16
  export { BashProcessRunner, type BashRunner } from './bash/index.js';
17
17
  export { AgentRegistry } from './registry.js';
18
- export type { ResultEvent, PlanEvent, FileChangeEvent, AgentEvent, Runner, RunnerLifetime, AgentKind, AgentSession, AgentSessionContentEvent, AgentSessionUsage, SessionContent, SessionSummary, AgentSessionReader, AgentRunner, TurnStartedEvent, TurnDiffEvent, NoticeEvent, SessionInfoEvent, ToolHint, ApprovalView, ApprovalRequestedEvent, ApprovalResolvedEvent, } from './types.js';
18
+ export type { ResultEvent, PlanEvent, FileChangeEvent, AgentEvent, Runner, RunnerLifetime, AgentKind, AgentSession, AgentSessionContentEvent, AgentSessionUsage, SessionContent, SessionSummary, AgentSessionReader, AgentRunner, TurnStartedEvent, TurnDiffEvent, NoticeEvent, SessionInfoEvent, ToolHint, ApprovalView, ApprovalRequestedEvent, ApprovalResolvedEvent, GoalView, GoalStateEvent, } from './types.js';
19
19
  export { resolveAgentChoices } from './resolve-agent-choices.js';
20
20
  export { syncAgentChoices } from './sync-agent-choices.js';
@@ -347,6 +347,45 @@ export interface RunStatusEvent {
347
347
  toolMs?: number;
348
348
  timestamp?: string;
349
349
  }
350
+ /**
351
+ * 会话级长期目标(DSH `goal` 投影的渲染快照)。
352
+ *
353
+ * 为什么卡片需要它:goal 是**跨 turn** 的自动续跑单元。本轮 turn 结束不等于
354
+ * 任务结束——`goal-round-driver` 会在 quiescence 立刻起下一轮。缺这一维,
355
+ * 卡片只能在"这一轮结束了"时收尾,于是出现"任务还在跑、卡片却写完成"
356
+ * (turn/end 后宿主立刻起 goal round 2,用户点 Compact 被拒)。
357
+ */
358
+ export interface GoalView {
359
+ id: string;
360
+ /** 人类写下的目标全文(可能很长,渲染层负责截断/折叠)。 */
361
+ objective: string;
362
+ phase: 'active' | 'paused' | 'blocked' | 'complete';
363
+ /** 已启动的轮次(最高 admitted round)。 */
364
+ roundsStarted: number;
365
+ /** 轮次上限;达到后 driver 把 goal 置为 blocked(round-limit)。 */
366
+ maxGoalRounds: number;
367
+ /** 仅在 phase='blocked' 时存在:阻断分类与人类可读原因。 */
368
+ blockedReason?: {
369
+ code: string;
370
+ message: string;
371
+ };
372
+ updatedAt?: number;
373
+ }
374
+ /**
375
+ * 一次 goal 状态观测(seed / 变更事件 / 收尾复核共用)。
376
+ *
377
+ * `known=false` 表示**读不到**,与"明确没有 goal"(`goal=null`)是两件事。
378
+ * 把探测失败降级成"没有 goal"会退回旧行为(goal 还在跑、卡片却收尾),
379
+ * 所以两者必须分开表达。
380
+ */
381
+ export interface GoalStateEvent {
382
+ type: 'goal_state';
383
+ /** null = 宿主明确表示该会话没有 goal。 */
384
+ goal: GoalView | null;
385
+ /** false = 探测失败,`goal` 不具权威性,渲染层不得据此断言"已完成"。 */
386
+ known: boolean;
387
+ timestamp?: string;
388
+ }
350
389
  /**
351
390
  * 运行结束后仍在后台运行的任务(终态卡展示 + 逐个停止入口)。
352
391
  *
@@ -431,7 +470,7 @@ export interface TurnDiffEvent {
431
470
  export type RunnerLifetime = 'turn' | 'workspace';
432
471
  /** 工具渲染 hint:runner 自愿声明,渲染层只消费不推断。 */
433
472
  export type ToolHint = 'shell' | 'file' | 'search' | 'web' | 'patch';
434
- export type AgentEvent = SystemInitEvent | SystemCompactEvent | ApprovalRequestedEvent | ApprovalResolvedEvent | ApprovalViewUpdatedEvent | ApprovalExpiredEvent | AssistantEvent | UserEvent | ResultEvent | PlanEvent | FileChangeEvent | TurnStartedEvent | TurnDiffEvent | NoticeEvent | SessionInfoEvent | RunStatusEvent | BackgroundJobsEvent;
473
+ export type AgentEvent = SystemInitEvent | SystemCompactEvent | ApprovalRequestedEvent | ApprovalResolvedEvent | ApprovalViewUpdatedEvent | ApprovalExpiredEvent | AssistantEvent | UserEvent | ResultEvent | PlanEvent | FileChangeEvent | TurnStartedEvent | TurnDiffEvent | NoticeEvent | SessionInfoEvent | RunStatusEvent | BackgroundJobsEvent | GoalStateEvent;
435
474
  export interface SpawnOptions {
436
475
  cwd: string;
437
476
  sessionId?: string;
@@ -364,7 +364,8 @@ function sessionPreset(item) {
364
364
  *
365
365
  * 全部取自 `session/list` 的 `projections.values`——DSH 0.2.x 已经随列表一起
366
366
  * 返回 title 与 turnOutline(每轮的 prompt/response),所以这里不需要再拉
367
- * `session/page`:那是一次上万条事件的大响应,一页 5 个会话就是 5 次。
367
+ * `session/page`:它按 `maxMessages=10000` 的上限拉整段历史,一页前 5 行就是
368
+ * 5 次这种大响应(把每行的 cwd 守卫与 projections 一起算,是 15 次同步请求)。
368
369
  *
369
370
  * `turnOutline` 的 prompt/response 由 DSH 侧按长度截断(结尾带 `…`),列表卡
370
371
  * 只做单行预览,直接复用即可。缺字段 / 类型不对时全部留空,不影响列表。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lark-remote",
3
- "version": "0.5.8",
3
+ "version": "0.5.10",
4
4
  "description": "Feishu (Lark) private-chat bridge to local coding agents — Claude Code / Codex / opencode / pi / kimi, with live-streaming CardKit 2.0 cards",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",