lark-remote 0.5.7 → 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 +35 -0
- package/dist/bridge/index.d.ts +47 -7
- package/dist/bridge/index.js +300 -129
- package/dist/bridge/queue-manager.d.ts +6 -0
- package/dist/bridge/queue-manager.js +61 -63
- package/dist/card/approval-render.js +2 -1
- package/dist/card/bash-card-session.d.ts +0 -1
- package/dist/card/bash-card-session.js +0 -8
- package/dist/card/bash-renderer.js +2 -1
- package/dist/card/card-budget.js +15 -19
- package/dist/card/card-session.d.ts +3 -13
- package/dist/card/card-session.js +4 -18
- package/dist/card/card-shared.d.ts +11 -0
- package/dist/card/card-shared.js +23 -13
- package/dist/card/run-card-session.d.ts +0 -1
- package/dist/card/run-card-session.js +0 -6
- package/dist/card/run-renderer.d.ts +11 -10
- package/dist/card/run-renderer.js +198 -35
- package/dist/card/run-state.d.ts +29 -2
- package/dist/card/run-state.js +23 -4
- package/dist/card/text-truncate.d.ts +3 -5
- package/dist/card/text-truncate.js +3 -5
- package/dist/card/tool-render.js +7 -21
- package/dist/common/tool-summary.d.ts +8 -0
- package/dist/common/tool-summary.js +38 -20
- package/dist/common/turn-failure.d.ts +13 -0
- package/dist/common/turn-failure.js +18 -0
- package/dist/index.js +13 -14
- package/dist/order/index.d.ts +2 -0
- package/dist/order/index.js +2 -0
- package/dist/router/card-helpers.js +23 -42
- package/dist/router/config/codex.js +2 -3
- package/dist/router/config/common/model-patch.d.ts +8 -0
- package/dist/router/config/common/model-patch.js +13 -0
- package/dist/router/config/common/render.js +30 -77
- package/dist/router/config/dsh.js +2 -1
- package/dist/router/config/kimi.js +3 -3
- package/dist/router/index.d.ts +0 -11
- package/dist/router/index.js +119 -360
- package/dist/router/order-exec-dispatch.js +2 -1
- package/dist/router/usage-format.d.ts +5 -0
- package/dist/router/usage-format.js +1 -1
- package/dist/runner/common/defaults.d.ts +2 -0
- package/dist/runner/common/defaults.js +3 -0
- package/dist/runner/common/jsonrpc/connection-manager.js +1 -1
- package/dist/runner/dsh/client.d.ts +15 -0
- package/dist/runner/dsh/client.js +28 -2
- 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 +59 -1
- package/dist/runner/types.js +9 -0
- package/dist/session/claude/session-index.d.ts +0 -4
- package/dist/session/claude/session-index.js +0 -8
- package/dist/session/dsh/sessions.js +58 -2
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ORDER_NOT_FOUND_TEXT } from '../order/index.js';
|
|
1
2
|
/**
|
|
2
3
|
* Dispatch an `order.exec` card action as an equivalent queued user message
|
|
3
4
|
* (Plan A): resolve the order text at the enqueue boundary, then enqueue it
|
|
@@ -20,7 +21,7 @@ export async function dispatchOrderExecForQueue(args) {
|
|
|
20
21
|
}
|
|
21
22
|
const resolved = router.resolveOrderExecForQueue(orderId);
|
|
22
23
|
if (!resolved) {
|
|
23
|
-
await bridge.sendResult({ text:
|
|
24
|
+
await bridge.sendResult({ text: ORDER_NOT_FOUND_TEXT }, ctx);
|
|
24
25
|
return 'not-found';
|
|
25
26
|
}
|
|
26
27
|
// Step4/入队时刻快照 agent+session,随任务闭包带到执行时刻,
|
|
@@ -40,7 +40,7 @@ function formatTokenK(n) {
|
|
|
40
40
|
export function formatUsageStats(usage, options) {
|
|
41
41
|
const lines = [];
|
|
42
42
|
// 1. Success 状态(通用格式,无具体结果值)
|
|
43
|
-
lines.push('✅ 已完成');
|
|
43
|
+
lines.push(options?.completedLabel ?? '✅ 已完成');
|
|
44
44
|
// 1.4. 实际生效模型(非空字符串才显示)
|
|
45
45
|
if (usage?.model) {
|
|
46
46
|
lines.push(`Model - ${usage.model}`);
|
|
@@ -17,8 +17,8 @@ import { JsonlRpcTransport } from './transport.js';
|
|
|
17
17
|
import { JsonRpcClient } from './client.js';
|
|
18
18
|
import { getLogger } from '../../../logger/index.js';
|
|
19
19
|
import { agentStopperRegistry, } from '../../../platform/agent-stopper.js';
|
|
20
|
+
import { DEFAULT_REQUEST_TIMEOUT_MS } from '../defaults.js';
|
|
20
21
|
const DEFAULT_IDLE_TTL_MS = 30 * 60 * 1000; // 30 minutes
|
|
21
|
-
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000; // 60 seconds
|
|
22
22
|
/** acquire() 的轮次上限:防止 slot 被持续释放时无限重建连接。 */
|
|
23
23
|
const MAX_ACQUIRE_ATTEMPTS = 5;
|
|
24
24
|
export class ConnectionManager {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* replay, and seq-based dedup.
|
|
21
21
|
*/
|
|
22
22
|
import type { DshApprovalStreamItem, DshCommandResult, DshModelCatalogValue, DshModelSelection, DshOpenTurnState, DshPermissionCatalogValue, DshJobView, DshPresetListValue, DshProjectionItem, DshProjectionSnapshot, DshQuestion, DshRemoteEventOutcome, DshSessionPageValue, DshStreamItem, DshTokenUsage } from './types.js';
|
|
23
|
+
import type { GoalView } from '../types.js';
|
|
23
24
|
interface WebSocketMessageEvent {
|
|
24
25
|
data: unknown;
|
|
25
26
|
}
|
|
@@ -193,6 +194,20 @@ export declare class DshClient {
|
|
|
193
194
|
signal?: AbortSignal;
|
|
194
195
|
timeoutMs?: number;
|
|
195
196
|
}): Promise<DshProjectionSnapshot | null>;
|
|
197
|
+
/**
|
|
198
|
+
* Read the session's current goal (one unary, authoritative at call time).
|
|
199
|
+
*
|
|
200
|
+
* 收尾判定必须用宿主状态:本轮 turn 结束不代表任务结束——goal 仍 active 时
|
|
201
|
+
* driver 会在 quiescence 立刻起下一轮。`known:false` 表示读不到,调用方不得
|
|
202
|
+
* 据此断言"已完成"(探测失败降级为收尾,正是"任务还在跑却写完成"的来源)。
|
|
203
|
+
*/
|
|
204
|
+
goalState(sessionId: string, opts?: {
|
|
205
|
+
signal?: AbortSignal;
|
|
206
|
+
timeoutMs?: number;
|
|
207
|
+
}): Promise<{
|
|
208
|
+
known: boolean;
|
|
209
|
+
goal: GoalView | null;
|
|
210
|
+
}>;
|
|
196
211
|
/**
|
|
197
212
|
* Find this run's prompt if it is still sitting in the host's inbox.
|
|
198
213
|
*
|
|
@@ -23,6 +23,8 @@ import { randomUUID } from 'node:crypto';
|
|
|
23
23
|
import WebSocket from 'ws';
|
|
24
24
|
import { getLogger } from '../../logger/index.js';
|
|
25
25
|
import { DSH_COOKIE_LIFETIME_MS, isDshCredentialsNotFound, mintDshBrowserCookie, readDshBrowserSecret, resolveDshCredentialsFile, } from './auth.js';
|
|
26
|
+
import { DEFAULT_REQUEST_TIMEOUT_MS } from '../common/defaults.js';
|
|
27
|
+
import { goalFromProjection } from './goal-view.js';
|
|
26
28
|
function defaultWebSocketFactory(url, options) {
|
|
27
29
|
const socket = new WebSocket(url, {
|
|
28
30
|
...(options?.headers ? { headers: options.headers } : {}),
|
|
@@ -105,8 +107,6 @@ function inboxItemsOf(values) {
|
|
|
105
107
|
}
|
|
106
108
|
return out;
|
|
107
109
|
}
|
|
108
|
-
/** Default unary timeout: long prompts are unbounded on the server side. */
|
|
109
|
-
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
|
|
110
110
|
/** Extract the `token` query parameter from a pasted `dsh web` URL. */
|
|
111
111
|
export function extractDshToken(raw) {
|
|
112
112
|
const value = (raw ?? '').trim();
|
|
@@ -450,6 +450,32 @@ export class DshClient {
|
|
|
450
450
|
: {},
|
|
451
451
|
};
|
|
452
452
|
}
|
|
453
|
+
/**
|
|
454
|
+
* Read the session's current goal (one unary, authoritative at call time).
|
|
455
|
+
*
|
|
456
|
+
* 收尾判定必须用宿主状态:本轮 turn 结束不代表任务结束——goal 仍 active 时
|
|
457
|
+
* driver 会在 quiescence 立刻起下一轮。`known:false` 表示读不到,调用方不得
|
|
458
|
+
* 据此断言"已完成"(探测失败降级为收尾,正是"任务还在跑却写完成"的来源)。
|
|
459
|
+
*/
|
|
460
|
+
async goalState(sessionId, opts) {
|
|
461
|
+
let snapshot;
|
|
462
|
+
try {
|
|
463
|
+
snapshot = await this.projectionSnapshot(sessionId, opts);
|
|
464
|
+
}
|
|
465
|
+
catch (err) {
|
|
466
|
+
if (opts?.signal?.aborted)
|
|
467
|
+
throw err;
|
|
468
|
+
getLogger().warn(`[dsh] goal probe failed session=${sessionId}: ${err.message}`);
|
|
469
|
+
return { known: false, goal: null };
|
|
470
|
+
}
|
|
471
|
+
// 拿不到投影 = 宿主没这个会话的投影,不等于"没有 goal"。
|
|
472
|
+
if (!snapshot)
|
|
473
|
+
return { known: false, goal: null };
|
|
474
|
+
const parsed = goalFromProjection(snapshot.values.goal);
|
|
475
|
+
if (parsed === undefined)
|
|
476
|
+
return { known: false, goal: null };
|
|
477
|
+
return { known: true, goal: parsed };
|
|
478
|
+
}
|
|
453
479
|
/**
|
|
454
480
|
* Find this run's prompt if it is still sitting in the host's inbox.
|
|
455
481
|
*
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
import type { GoalView } from '../types.js';
|
|
15
|
+
/**
|
|
16
|
+
* 解析投影里的 goal 状态。
|
|
17
|
+
*
|
|
18
|
+
* @returns `GoalView` = 有 goal;`null` = 宿主明确没有 goal;`undefined` = 读不懂。
|
|
19
|
+
*/
|
|
20
|
+
export declare function goalFromProjection(value: unknown): GoalView | null | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* 解析 `goal/change` 事件 payload。
|
|
23
|
+
*
|
|
24
|
+
* @returns `GoalView` = 变更后的 goal;`null` = clear(现在没有 goal);
|
|
25
|
+
* `undefined` = 读不懂。
|
|
26
|
+
*/
|
|
27
|
+
export declare function goalFromChange(data: unknown): GoalView | null | undefined;
|
|
28
|
+
/** 便利构造:把解析结果包成 runner 事件。 */
|
|
29
|
+
export declare function goalStateEvent(goal: GoalView | null, known: boolean): {
|
|
30
|
+
type: 'goal_state';
|
|
31
|
+
goal: GoalView | null;
|
|
32
|
+
known: boolean;
|
|
33
|
+
timestamp: string;
|
|
34
|
+
};
|
|
35
|
+
/** 两次观测是否等价(用于抑制重复帧,不触发重渲染)。 */
|
|
36
|
+
export declare function sameGoal(a: GoalView | null | undefined, b: GoalView | null | undefined): boolean;
|
|
@@ -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;
|
|
@@ -507,6 +546,8 @@ export interface Runner {
|
|
|
507
546
|
*/
|
|
508
547
|
export declare const AGENT_KINDS: readonly ["claude", "codex", "opencode", "pi", "kimi", "dsh"];
|
|
509
548
|
export type AgentKind = (typeof AGENT_KINDS)[number];
|
|
549
|
+
/** 用户可见 agent 显示名单源:启动注册与无 registry 的回退路径共用。 */
|
|
550
|
+
export declare const AGENT_DISPLAY_NAMES: Record<AgentKind, string>;
|
|
510
551
|
/**
|
|
511
552
|
* Compact 能力声明(运行时门控的能力清单,编译期强制完整)。
|
|
512
553
|
*
|
|
@@ -529,6 +570,23 @@ export interface AgentSession {
|
|
|
529
570
|
mtime: number;
|
|
530
571
|
/** DSH:session 创建时固定的 preset;其他 agent 无此概念(undefined)。 */
|
|
531
572
|
agentPreset?: string;
|
|
573
|
+
/**
|
|
574
|
+
* 服务端投影已有的会话标题(DSH `projections.values.title`)。
|
|
575
|
+
*
|
|
576
|
+
* 与 `summary` 的区别:`summary` 是「首条用户输入」的兜底口径(claude /
|
|
577
|
+
* codex / pi / kimi 无标题字段),`title` 是 agent 自己起的标题。列表卡
|
|
578
|
+
* 用它决定标签文案(标题 / 会话摘要),缺省时回退 `summary`。
|
|
579
|
+
*/
|
|
580
|
+
title?: string;
|
|
581
|
+
/**
|
|
582
|
+
* 最后一轮用户输入(服务端投影,单行预览)。
|
|
583
|
+
*
|
|
584
|
+
* 只给「列表响应里已顺带返回」的 reader 用(DSH 的 `turnOutline` 末条),
|
|
585
|
+
* 目的是让列表卡不必为每行做一次全量内容读取。
|
|
586
|
+
*/
|
|
587
|
+
lastUserMessage?: string;
|
|
588
|
+
/** 最后一轮助手输出(服务端投影已截断,单行预览)。 */
|
|
589
|
+
lastAssistantMessage?: string;
|
|
532
590
|
}
|
|
533
591
|
/** A single renderable event from session history. */
|
|
534
592
|
export interface AgentSessionContentEvent {
|
package/dist/runner/types.js
CHANGED
|
@@ -16,6 +16,15 @@
|
|
|
16
16
|
* 新增 agent 只改这里。
|
|
17
17
|
*/
|
|
18
18
|
export const AGENT_KINDS = ['claude', 'codex', 'opencode', 'pi', 'kimi', 'dsh'];
|
|
19
|
+
/** 用户可见 agent 显示名单源:启动注册与无 registry 的回退路径共用。 */
|
|
20
|
+
export const AGENT_DISPLAY_NAMES = {
|
|
21
|
+
claude: 'Claude',
|
|
22
|
+
codex: 'Codex',
|
|
23
|
+
opencode: 'Opencode',
|
|
24
|
+
pi: 'Pi',
|
|
25
|
+
kimi: 'Kimi',
|
|
26
|
+
dsh: 'DSH',
|
|
27
|
+
};
|
|
19
28
|
/**
|
|
20
29
|
* Compact 能力声明(运行时门控的能力清单,编译期强制完整)。
|
|
21
30
|
*
|
|
@@ -74,10 +74,6 @@ export declare class SessionIndex {
|
|
|
74
74
|
private readonly byCwd;
|
|
75
75
|
private lastRefreshAt;
|
|
76
76
|
private built;
|
|
77
|
-
/** Test-only: count of parseSessionJsonl calls since creation */
|
|
78
|
-
parseCount: number;
|
|
79
|
-
/** Test-only: count of directory scan operations */
|
|
80
|
-
scanCount: number;
|
|
81
77
|
constructor(projectsDir: string, opts?: SessionIndexOptions);
|
|
82
78
|
/** Whether a full scan has ever completed successfully (readdir succeeded). */
|
|
83
79
|
get isBuilt(): boolean;
|
|
@@ -146,10 +146,6 @@ export class SessionIndex {
|
|
|
146
146
|
byCwd = new Map();
|
|
147
147
|
lastRefreshAt = 0;
|
|
148
148
|
built = false;
|
|
149
|
-
/** Test-only: count of parseSessionJsonl calls since creation */
|
|
150
|
-
parseCount = 0;
|
|
151
|
-
/** Test-only: count of directory scan operations */
|
|
152
|
-
scanCount = 0;
|
|
153
149
|
constructor(projectsDir, opts = {}) {
|
|
154
150
|
this.projectsDir = projectsDir;
|
|
155
151
|
this.refreshIntervalMs = opts.refreshIntervalMs ?? 5000;
|
|
@@ -161,7 +157,6 @@ export class SessionIndex {
|
|
|
161
157
|
}
|
|
162
158
|
// ── Build (initial full scan) ───────────────────────────
|
|
163
159
|
build() {
|
|
164
|
-
this.scanCount++;
|
|
165
160
|
this.fullScan();
|
|
166
161
|
this.lastRefreshAt = Date.now();
|
|
167
162
|
}
|
|
@@ -171,7 +166,6 @@ export class SessionIndex {
|
|
|
171
166
|
if (now - this.lastRefreshAt < this.refreshIntervalMs) {
|
|
172
167
|
return; // throttled
|
|
173
168
|
}
|
|
174
|
-
this.scanCount++;
|
|
175
169
|
this.fullScan();
|
|
176
170
|
this.lastRefreshAt = now;
|
|
177
171
|
}
|
|
@@ -232,7 +226,6 @@ export class SessionIndex {
|
|
|
232
226
|
}
|
|
233
227
|
// Fingerprint changed → re-parse this single file
|
|
234
228
|
const parsed = parseSessionJsonl(p);
|
|
235
|
-
this.parseCount++;
|
|
236
229
|
try {
|
|
237
230
|
const st = fs.statSync(p, { bigint: true });
|
|
238
231
|
const updated = {
|
|
@@ -303,7 +296,6 @@ export class SessionIndex {
|
|
|
303
296
|
// Concurrent-write guard: stat before and after parse, compare fingerprints
|
|
304
297
|
const fpBefore = fp;
|
|
305
298
|
const parsed = parseSessionJsonl(fullPath);
|
|
306
|
-
this.parseCount++;
|
|
307
299
|
const fpAfter = buildFingerprint(fullPath);
|
|
308
300
|
if (fpBefore !== fpAfter) {
|
|
309
301
|
// File was modified during parse → skip this round, retry next refresh
|
|
@@ -172,9 +172,9 @@ export class DshSessionReader {
|
|
|
172
172
|
.filter((i) => i.cwd !== undefined && samePath(i.cwd, cwd))
|
|
173
173
|
.map((i) => ({
|
|
174
174
|
sessionId: String(i.sessionId),
|
|
175
|
-
summary: '',
|
|
176
175
|
mtime: i.updatedAt,
|
|
177
176
|
...sessionPreset(i),
|
|
177
|
+
...sessionPreview(i),
|
|
178
178
|
}));
|
|
179
179
|
sortByRecencyDesc(filtered, (i) => i.mtime, (i) => i.sessionId);
|
|
180
180
|
const { items: page, total } = paginate(filtered, opts ?? {});
|
|
@@ -200,6 +200,8 @@ export class DshSessionReader {
|
|
|
200
200
|
return { events: [] };
|
|
201
201
|
}
|
|
202
202
|
const throughSeq = this.resolveThroughSeq(sessionId, session);
|
|
203
|
+
// 服务端投影带来的标题与末轮输入:详情卡头部用它们,不必再从事件里猜。
|
|
204
|
+
const preview = sessionPreview(session);
|
|
203
205
|
const value = this.unary('session/page', {
|
|
204
206
|
address: { kind: 'session', sessionId },
|
|
205
207
|
throughSeq,
|
|
@@ -307,12 +309,16 @@ export class DshSessionReader {
|
|
|
307
309
|
// 中断/仍在执行的调用没有结果:把它当独立事件留下,别让这一条消失。
|
|
308
310
|
if (pendingTool)
|
|
309
311
|
events.push(pendingTool);
|
|
312
|
+
// 头部信息口径:最近输入优先用服务端末轮 prompt(事件里可能只是当前 turn
|
|
313
|
+
// 的一部分),标题用服务端 title;两者都缺时保持原行为。
|
|
314
|
+
const headerLastInput = preview.lastUserMessage ?? displayTitle;
|
|
310
315
|
return {
|
|
311
316
|
// 展示仍按 maxEvents 尾截断(默认 50,保持原行为),usage 已在上面基于
|
|
312
317
|
// 完整 history 累加。
|
|
313
318
|
events: capEvents(events, opts?.maxEvents ?? 50),
|
|
314
319
|
...(sessionUsage ? { usage: sessionUsage } : {}),
|
|
315
|
-
...(
|
|
320
|
+
...(headerLastInput ? { displayTitle: headerLastInput } : {}),
|
|
321
|
+
...(preview.title ? { aiTitle: preview.title } : {}),
|
|
316
322
|
};
|
|
317
323
|
}
|
|
318
324
|
catch (error) {
|
|
@@ -353,6 +359,56 @@ function sessionPreset(item) {
|
|
|
353
359
|
const preset = item.projections?.values?.agentPreset ?? item.agentPreset;
|
|
354
360
|
return preset ? { agentPreset: preset } : {};
|
|
355
361
|
}
|
|
362
|
+
/**
|
|
363
|
+
* 会话预览(列表卡上的「标题 / 最近输入 / 最后输出」)。
|
|
364
|
+
*
|
|
365
|
+
* 全部取自 `session/list` 的 `projections.values`——DSH 0.2.x 已经随列表一起
|
|
366
|
+
* 返回 title 与 turnOutline(每轮的 prompt/response),所以这里不需要再拉
|
|
367
|
+
* `session/page`:它按 `maxMessages=10000` 的上限拉整段历史,一页前 5 行就是
|
|
368
|
+
* 5 次这种大响应(把每行的 cwd 守卫与 projections 一起算,是 15 次同步请求)。
|
|
369
|
+
*
|
|
370
|
+
* `turnOutline` 的 prompt/response 由 DSH 侧按长度截断(结尾带 `…`),列表卡
|
|
371
|
+
* 只做单行预览,直接复用即可。缺字段 / 类型不对时全部留空,不影响列表。
|
|
372
|
+
*/
|
|
373
|
+
function sessionPreview(item) {
|
|
374
|
+
const values = item.projections?.values;
|
|
375
|
+
const rawTitle = values?.title;
|
|
376
|
+
const title = typeof rawTitle === 'string' ? rawTitle.trim() : '';
|
|
377
|
+
const outline = values?.turnOutline;
|
|
378
|
+
const lastUserMessage = lastOutlineText(outline, 'prompt');
|
|
379
|
+
const lastAssistantMessage = lastOutlineText(outline, 'response');
|
|
380
|
+
return {
|
|
381
|
+
// summary 同时填标题:只认 summary 的消费方(旧卡片路径、其他 agent 的
|
|
382
|
+
// 统一逻辑)也能拿到可读文本,不必区分 agent。
|
|
383
|
+
summary: title,
|
|
384
|
+
...(title ? { title } : {}),
|
|
385
|
+
...(lastUserMessage ? { lastUserMessage } : {}),
|
|
386
|
+
...(lastAssistantMessage ? { lastAssistantMessage } : {}),
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* 从 turnOutline 末条往回找第一个非空文本。
|
|
391
|
+
*
|
|
392
|
+
* 在途 turn 已经写进 outline,但 `response` 还是空串(实测末条
|
|
393
|
+
* `{turn:6, prompt:"…", response:""}`);直接取末条会让「最后输出」空掉,
|
|
394
|
+
* 所以回溯到最近一条有内容的结果。
|
|
395
|
+
*/
|
|
396
|
+
function lastOutlineText(outline, key) {
|
|
397
|
+
if (!Array.isArray(outline))
|
|
398
|
+
return undefined;
|
|
399
|
+
for (let i = outline.length - 1; i >= 0; i -= 1) {
|
|
400
|
+
const entry = outline[i];
|
|
401
|
+
if (!entry || typeof entry !== 'object')
|
|
402
|
+
continue;
|
|
403
|
+
const raw = entry[key];
|
|
404
|
+
if (typeof raw !== 'string')
|
|
405
|
+
continue;
|
|
406
|
+
const text = raw.trim();
|
|
407
|
+
if (text)
|
|
408
|
+
return text;
|
|
409
|
+
}
|
|
410
|
+
return undefined;
|
|
411
|
+
}
|
|
356
412
|
function emptyUsageTotals() {
|
|
357
413
|
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
358
414
|
}
|
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",
|