lark-remote 0.5.8 → 0.5.9
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/CHANGELOG.md +21 -2
- package/dist/bridge/index.d.ts +32 -7
- package/dist/bridge/index.js +239 -129
- package/dist/bridge/queue-manager.d.ts +6 -0
- package/dist/bridge/queue-manager.js +20 -5
- package/dist/card/card-shared.js +4 -0
- package/dist/card/run-renderer.d.ts +8 -1
- package/dist/card/run-renderer.js +100 -9
- package/dist/card/run-state.d.ts +29 -2
- package/dist/card/run-state.js +23 -4
- package/dist/index.js +8 -7
- package/dist/router/index.js +14 -14
- package/dist/router/usage-format.d.ts +5 -0
- package/dist/router/usage-format.js +1 -1
- package/dist/runner/dsh/client.d.ts +15 -0
- package/dist/runner/dsh/client.js +27 -0
- package/dist/runner/dsh/goal-view.d.ts +36 -0
- package/dist/runner/dsh/goal-view.js +106 -0
- package/dist/runner/dsh/runner.js +51 -2
- package/dist/runner/index.d.ts +1 -1
- package/dist/runner/types.d.ts +40 -1
- package/dist/session/dsh/sessions.js +2 -1
- package/package.json +1 -1
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH `goal` 域 → 卡片用的 {@link GoalView} 解析。
|
|
3
|
+
*
|
|
4
|
+
* 两个来源、同一个目标类型:
|
|
5
|
+
* 1. 投影值:`session/projections` 的 `values.goal` 与 `session/control` 的
|
|
6
|
+
* `{key:'goal'}` 帧,宿主状态包一层 `current`(`{current:{goal,roundsStarted
|
|
7
|
+
* ,createdAt,updatedAt},seenGoalIds,failure}`);
|
|
8
|
+
* 2. 会话事件 `goal/change`:create/edit/pause/resume/complete/block 携带完整
|
|
9
|
+
* snapshot + roundsStarted;clear 是墓碑,表示"现在没有 goal"。
|
|
10
|
+
*
|
|
11
|
+
* 解析失败返回 `undefined`,表示**读不懂**——它与"没有 goal"(`null`)必须区分:
|
|
12
|
+
* 把读不懂当成没有 goal,就会在 goal 还在自动续跑时收尾卡片。
|
|
13
|
+
*/
|
|
14
|
+
const PHASES = new Set(['active', 'paused', 'blocked', 'complete']);
|
|
15
|
+
function isRecord(value) {
|
|
16
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
17
|
+
}
|
|
18
|
+
function nonNegativeInt(value) {
|
|
19
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 0 ? value : undefined;
|
|
20
|
+
}
|
|
21
|
+
function parseSnapshot(value, roundsStarted) {
|
|
22
|
+
if (!isRecord(value))
|
|
23
|
+
return undefined;
|
|
24
|
+
const phase = value.phase;
|
|
25
|
+
if (typeof phase !== 'string' || !PHASES.has(phase))
|
|
26
|
+
return undefined;
|
|
27
|
+
if (typeof value.objective !== 'string')
|
|
28
|
+
return undefined;
|
|
29
|
+
if (typeof value.id !== 'string' || value.id.length === 0)
|
|
30
|
+
return undefined;
|
|
31
|
+
const max = nonNegativeInt(value.maxGoalRounds);
|
|
32
|
+
if (max === undefined)
|
|
33
|
+
return undefined;
|
|
34
|
+
const reason = value.blockedReason;
|
|
35
|
+
const blockedReason = isRecord(reason) && typeof reason.code === 'string' && typeof reason.message === 'string'
|
|
36
|
+
? { code: reason.code, message: reason.message }
|
|
37
|
+
: undefined;
|
|
38
|
+
return {
|
|
39
|
+
id: value.id,
|
|
40
|
+
objective: value.objective,
|
|
41
|
+
// 上面已按白名单校验,这里收窄回联合类型。
|
|
42
|
+
phase: phase,
|
|
43
|
+
roundsStarted: nonNegativeInt(roundsStarted) ?? 0,
|
|
44
|
+
maxGoalRounds: max,
|
|
45
|
+
...(blockedReason ? { blockedReason } : {}),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 解析投影里的 goal 状态。
|
|
50
|
+
*
|
|
51
|
+
* @returns `GoalView` = 有 goal;`null` = 宿主明确没有 goal;`undefined` = 读不懂。
|
|
52
|
+
*/
|
|
53
|
+
export function goalFromProjection(value) {
|
|
54
|
+
if (value === null)
|
|
55
|
+
return null;
|
|
56
|
+
if (!isRecord(value))
|
|
57
|
+
return undefined;
|
|
58
|
+
// 宿主状态信封:{current:{goal,roundsStarted,...}, seenGoalIds, failure}
|
|
59
|
+
if ('current' in value) {
|
|
60
|
+
const current = value.current;
|
|
61
|
+
if (current === null)
|
|
62
|
+
return null;
|
|
63
|
+
if (!isRecord(current))
|
|
64
|
+
return undefined;
|
|
65
|
+
if (current.goal === undefined || current.goal === null)
|
|
66
|
+
return null;
|
|
67
|
+
return parseSnapshot(current.goal, current.roundsStarted);
|
|
68
|
+
}
|
|
69
|
+
// 兼容直接投递 GoalProjection 的形态:{goal, roundsStarted, ...}
|
|
70
|
+
if (value.goal === undefined || value.goal === null)
|
|
71
|
+
return null;
|
|
72
|
+
return parseSnapshot(value.goal, value.roundsStarted);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 解析 `goal/change` 事件 payload。
|
|
76
|
+
*
|
|
77
|
+
* @returns `GoalView` = 变更后的 goal;`null` = clear(现在没有 goal);
|
|
78
|
+
* `undefined` = 读不懂。
|
|
79
|
+
*/
|
|
80
|
+
export function goalFromChange(data) {
|
|
81
|
+
if (!isRecord(data))
|
|
82
|
+
return undefined;
|
|
83
|
+
if (data.operation === 'clear')
|
|
84
|
+
return null;
|
|
85
|
+
if (typeof data.operation !== 'string')
|
|
86
|
+
return undefined;
|
|
87
|
+
return parseSnapshot(data.goal, data.roundsStarted);
|
|
88
|
+
}
|
|
89
|
+
/** 便利构造:把解析结果包成 runner 事件。 */
|
|
90
|
+
export function goalStateEvent(goal, known) {
|
|
91
|
+
return { type: 'goal_state', goal, known, timestamp: new Date().toISOString() };
|
|
92
|
+
}
|
|
93
|
+
/** 两次观测是否等价(用于抑制重复帧,不触发重渲染)。 */
|
|
94
|
+
export function sameGoal(a, b) {
|
|
95
|
+
if (a === b)
|
|
96
|
+
return true;
|
|
97
|
+
if (a === null || a === undefined || b === null || b === undefined)
|
|
98
|
+
return false;
|
|
99
|
+
return (a.id === b.id &&
|
|
100
|
+
a.phase === b.phase &&
|
|
101
|
+
a.roundsStarted === b.roundsStarted &&
|
|
102
|
+
a.maxGoalRounds === b.maxGoalRounds &&
|
|
103
|
+
a.objective === b.objective &&
|
|
104
|
+
a.blockedReason?.code === b.blockedReason?.code);
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=goal-view.js.map
|
|
@@ -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;
|
package/dist/runner/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/runner/types.d.ts
CHANGED
|
@@ -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
|
|
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.
|
|
3
|
+
"version": "0.5.9",
|
|
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",
|