lark-remote 0.5.12 → 0.5.13
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 +13 -2
- package/README.en.md +1 -14
- package/README.md +1 -14
- package/dist/bridge/approval-coordinator.d.ts +1 -0
- package/dist/bridge/approval-coordinator.js +12 -24
- package/dist/bridge/index.d.ts +7 -0
- package/dist/bridge/index.js +77 -227
- package/dist/bridge/run-approval.d.ts +15 -0
- package/dist/bridge/run-approval.js +41 -0
- package/dist/bridge/run-usage.d.ts +46 -0
- package/dist/bridge/run-usage.js +96 -0
- package/dist/bridge/session-writeback.d.ts +18 -0
- package/dist/bridge/session-writeback.js +36 -0
- package/dist/card/bash-card-session.js +2 -5
- package/dist/card/card-session.d.ts +2 -0
- package/dist/card/card-session.js +20 -22
- package/dist/card/card-shared.d.ts +4 -4
- package/dist/card/card-shared.js +3 -9
- package/dist/card/collapsible.js +3 -2
- package/dist/card/final-answer-overflow.d.ts +7 -0
- package/dist/card/final-answer-overflow.js +46 -0
- package/dist/card/run-card-session.js +2 -7
- package/dist/card/run-renderer.d.ts +14 -0
- package/dist/card/run-renderer.js +119 -1
- package/dist/card/run-state.d.ts +12 -1
- package/dist/card/run-state.js +73 -34
- package/dist/config/codex-config.js +4 -4
- package/dist/config/index.d.ts +4 -2
- package/dist/config/index.js +6 -4
- package/dist/inbound/turn-assembler.js +6 -0
- package/dist/index.js +15 -409
- package/dist/message-handlers.d.ts +26 -0
- package/dist/message-handlers.js +275 -0
- package/dist/router/card-action-payload.d.ts +1 -1
- package/dist/router/card-action-payload.js +1 -1
- package/dist/router/commands/ls.d.ts +9 -0
- package/dist/router/commands/ls.js +243 -0
- package/dist/router/commands/order.d.ts +12 -0
- package/dist/router/commands/order.js +178 -0
- package/dist/router/commands/resume.d.ts +47 -0
- package/dist/router/commands/resume.js +246 -0
- package/dist/router/commands/ws.d.ts +15 -0
- package/dist/router/commands/ws.js +189 -0
- package/dist/router/config/claude.js +3 -3
- package/dist/router/config/codex.js +2 -6
- package/dist/router/config/common/turn-idle.d.ts +2 -0
- package/dist/router/config/common/turn-idle.js +9 -0
- package/dist/router/config/kimi.js +2 -6
- package/dist/router/config/opencode.js +2 -6
- package/dist/router/index.d.ts +14 -22
- package/dist/router/index.js +70 -1152
- package/dist/router/paging.d.ts +13 -0
- package/dist/router/paging.js +20 -0
- package/dist/router/path-validation.d.ts +27 -0
- package/dist/router/path-validation.js +45 -0
- package/dist/runner/claude/runner.d.ts +3 -4
- package/dist/runner/claude/runner.js +4 -5
- package/dist/runner/claude/session.js +1 -1
- package/dist/runner/codex/app-server/translator.d.ts +2 -0
- package/dist/runner/codex/app-server/translator.js +65 -9
- package/dist/runner/common/acp/base-translator.d.ts +3 -8
- package/dist/runner/common/acp/base-translator.js +16 -7
- package/dist/runner/common/spawning-runner.js +10 -4
- package/dist/runner/common/turn-started.d.ts +2 -0
- package/dist/runner/common/turn-started.js +10 -0
- package/dist/runner/dsh/auth.js +2 -3
- package/dist/runner/dsh/client.d.ts +1 -6
- package/dist/runner/dsh/client.js +11 -165
- package/dist/runner/dsh/daemon.d.ts +0 -2
- package/dist/runner/dsh/daemon.js +1 -6
- package/dist/runner/dsh/errors.d.ts +5 -0
- package/dist/runner/dsh/errors.js +11 -0
- package/dist/runner/dsh/exit-cancel.d.ts +0 -2
- package/dist/runner/dsh/exit-cancel.js +1 -1
- package/dist/runner/dsh/home.d.ts +2 -0
- package/dist/runner/dsh/home.js +8 -0
- package/dist/runner/dsh/inflight.d.ts +0 -2
- package/dist/runner/dsh/inflight.js +1 -1
- package/dist/runner/dsh/mux-stream.d.ts +26 -0
- package/dist/runner/dsh/mux-stream.js +157 -0
- package/dist/runner/dsh/translator.d.ts +0 -10
- package/dist/runner/dsh/translator.js +1 -1
- package/dist/runner/index.d.ts +1 -1
- package/dist/runner/kimi/acp/translator.js +1 -12
- package/dist/runner/opencode/acp/translator.js +1 -12
- package/dist/runner/pi/rpc/translator.d.ts +2 -8
- package/dist/runner/pi/rpc/translator.js +2 -7
- package/dist/runner/registry.d.ts +0 -19
- package/dist/runner/registry.js +0 -32
- package/dist/runner/types.d.ts +17 -1
- package/dist/session/claude/session-index.js +4 -2
- package/dist/session/kimi/sessions.js +2 -2
- package/dist/session/opencode/sessions.js +2 -1
- package/dist/session/pi/sessions.js +3 -2
- package/package.json +1 -1
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { normalizeResultUsage } from '../runner/common/result-usage.js';
|
|
2
|
+
export class RunUsageCollector {
|
|
3
|
+
contextLength;
|
|
4
|
+
finalContextLength;
|
|
5
|
+
finalContextLimit;
|
|
6
|
+
finalCompactCount;
|
|
7
|
+
finalCacheReadTokens;
|
|
8
|
+
finalCacheCreationTokens;
|
|
9
|
+
finalTotalTokens;
|
|
10
|
+
liveInput;
|
|
11
|
+
liveOutput;
|
|
12
|
+
liveContextLimit;
|
|
13
|
+
finalInputTokens;
|
|
14
|
+
finalOutputTokens;
|
|
15
|
+
finalCumulativeTotalTokens;
|
|
16
|
+
finalCumulativeInputTokens;
|
|
17
|
+
finalCumulativeOutputTokens;
|
|
18
|
+
finalCumulativeCacheReadTokens;
|
|
19
|
+
finalCumulativeCacheCreationTokens;
|
|
20
|
+
finalReasoningTokens;
|
|
21
|
+
resultNotSuccess = false;
|
|
22
|
+
get liveInputTokens() {
|
|
23
|
+
return this.liveInput;
|
|
24
|
+
}
|
|
25
|
+
get liveOutputTokens() {
|
|
26
|
+
return this.liveOutput;
|
|
27
|
+
}
|
|
28
|
+
observeCompaction(postTokens) {
|
|
29
|
+
this.contextLength = postTokens;
|
|
30
|
+
}
|
|
31
|
+
observeResult(event) {
|
|
32
|
+
this.resultNotSuccess = event.subtype !== 'success';
|
|
33
|
+
if (!event.usage)
|
|
34
|
+
return;
|
|
35
|
+
const usage = normalizeResultUsage(event.usage);
|
|
36
|
+
if (!this.contextLength)
|
|
37
|
+
this.contextLength = usage.contextLength;
|
|
38
|
+
this.liveInput = usage.inputTokens;
|
|
39
|
+
this.liveOutput = usage.outputTokens;
|
|
40
|
+
this.liveContextLimit = usage.contextLimit;
|
|
41
|
+
if (usage.cacheReadTokens !== undefined)
|
|
42
|
+
this.finalCacheReadTokens = usage.cacheReadTokens;
|
|
43
|
+
if (usage.cacheCreationTokens !== undefined) {
|
|
44
|
+
this.finalCacheCreationTokens = usage.cacheCreationTokens;
|
|
45
|
+
}
|
|
46
|
+
if (usage.totalTokens !== undefined)
|
|
47
|
+
this.finalTotalTokens = usage.totalTokens;
|
|
48
|
+
if (usage.reasoningTokens !== undefined)
|
|
49
|
+
this.finalReasoningTokens = usage.reasoningTokens;
|
|
50
|
+
}
|
|
51
|
+
finalize(finalUsage, authority) {
|
|
52
|
+
this.finalContextLength = finalUsage?.contextLength ?? this.contextLength;
|
|
53
|
+
this.finalContextLimit = finalUsage?.contextLimit ?? this.liveContextLimit;
|
|
54
|
+
this.finalCompactCount = finalUsage?.compactCount;
|
|
55
|
+
this.finalCumulativeTotalTokens = finalUsage?.cumulativeTotalTokens;
|
|
56
|
+
this.finalCumulativeInputTokens = finalUsage?.cumulativeInputTokens;
|
|
57
|
+
this.finalCumulativeOutputTokens = finalUsage?.cumulativeOutputTokens;
|
|
58
|
+
this.finalCumulativeCacheReadTokens = finalUsage?.cumulativeCacheReadTokens;
|
|
59
|
+
this.finalCumulativeCacheCreationTokens = finalUsage?.cumulativeCacheCreationTokens;
|
|
60
|
+
const useLiveFlowScope = authority !== 'jsonl' && this.liveInput !== undefined && this.liveOutput !== undefined;
|
|
61
|
+
if (useLiveFlowScope) {
|
|
62
|
+
this.finalInputTokens = this.liveInput;
|
|
63
|
+
this.finalOutputTokens = this.liveOutput;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
this.finalInputTokens = finalUsage?.inputTokens ?? this.liveInput;
|
|
67
|
+
this.finalOutputTokens = finalUsage?.outputTokens ?? this.liveOutput;
|
|
68
|
+
this.finalCacheReadTokens = finalUsage?.cacheReadTokens ?? this.finalCacheReadTokens;
|
|
69
|
+
this.finalCacheCreationTokens =
|
|
70
|
+
finalUsage?.cacheCreationTokens ?? this.finalCacheCreationTokens;
|
|
71
|
+
this.finalTotalTokens = finalUsage?.totalTokens ?? this.finalTotalTokens;
|
|
72
|
+
}
|
|
73
|
+
meta() {
|
|
74
|
+
return {
|
|
75
|
+
contextLength: this.finalContextLength,
|
|
76
|
+
contextLimit: this.finalContextLimit,
|
|
77
|
+
compactCount: this.finalCompactCount,
|
|
78
|
+
cacheReadTokens: this.finalCacheReadTokens,
|
|
79
|
+
cacheCreationTokens: this.finalCacheCreationTokens,
|
|
80
|
+
totalTokens: this.finalTotalTokens,
|
|
81
|
+
inputTokens: this.finalInputTokens,
|
|
82
|
+
outputTokens: this.finalOutputTokens,
|
|
83
|
+
cumulativeTotalTokens: this.finalCumulativeTotalTokens,
|
|
84
|
+
cumulativeInputTokens: this.finalCumulativeInputTokens,
|
|
85
|
+
cumulativeOutputTokens: this.finalCumulativeOutputTokens,
|
|
86
|
+
cumulativeCacheReadTokens: this.finalCumulativeCacheReadTokens,
|
|
87
|
+
cumulativeCacheCreationTokens: this.finalCumulativeCacheCreationTokens,
|
|
88
|
+
reasoningTokens: this.finalReasoningTokens,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
logSummary(runId) {
|
|
92
|
+
const meta = this.meta();
|
|
93
|
+
return `[lark-remote] final usage runId=${runId} contextLength=${meta.contextLength} contextLimit=${meta.contextLimit ?? 'undefined'} compactCount=${meta.compactCount ?? 'undefined'} cacheRead=${meta.cacheReadTokens ?? 'undefined'} cacheCreate=${meta.cacheCreationTokens ?? 'undefined'}`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=run-usage.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AgentKind, TurnStartedEvent } from '../runner/index.js';
|
|
2
|
+
import type { SessionStore } from '../session/index.js';
|
|
3
|
+
interface InitEvent {
|
|
4
|
+
session_id: string;
|
|
5
|
+
cwd?: string;
|
|
6
|
+
}
|
|
7
|
+
interface WritebackContext {
|
|
8
|
+
sessionStore: SessionStore;
|
|
9
|
+
userId: string;
|
|
10
|
+
agentKind: AgentKind;
|
|
11
|
+
cwd: string;
|
|
12
|
+
sessionEpochAtStart: number;
|
|
13
|
+
runId: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function writeBackInitSession(ctx: WritebackContext, event: InitEvent): void;
|
|
16
|
+
export declare function writeBackTurnStarted(ctx: WritebackContext, event: TurnStartedEvent): boolean;
|
|
17
|
+
export declare function writeBackResultSession(ctx: WritebackContext, sessionId: string | undefined): void;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { getLogger } from '../logger/index.js';
|
|
2
|
+
function pointerMoved(ctx) {
|
|
3
|
+
return ctx.sessionStore.getSessionEpoch(ctx.userId, ctx.agentKind) !== ctx.sessionEpochAtStart;
|
|
4
|
+
}
|
|
5
|
+
export function writeBackInitSession(ctx, event) {
|
|
6
|
+
if (pointerMoved(ctx)) {
|
|
7
|
+
getLogger().info(`[lark-remote] system.init write-back skipped: session pointer moved since run start runId=${ctx.runId} sessionId=${event.session_id}`);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const eventCwd = typeof event.cwd === 'string' ? event.cwd : '';
|
|
11
|
+
const hasCwd = !!ctx.sessionStore.getCwd(ctx.userId);
|
|
12
|
+
const realCwd = eventCwd.length > 0 ? eventCwd : ctx.cwd;
|
|
13
|
+
if (hasCwd && eventCwd && eventCwd !== ctx.cwd) {
|
|
14
|
+
getLogger().info(`[lark-remote] system.init: cwd=${ctx.cwd}, session cwd=${eventCwd}`);
|
|
15
|
+
ctx.sessionStore.setSessionIdAndSessionCwd(ctx.userId, ctx.agentKind, event.session_id, realCwd);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const bootstrapSessionCwd = !hasCwd && eventCwd.length > 0 ? eventCwd : undefined;
|
|
19
|
+
ctx.sessionStore.setSessionIdAndCwd(ctx.userId, ctx.agentKind, event.session_id, realCwd, bootstrapSessionCwd);
|
|
20
|
+
}
|
|
21
|
+
export function writeBackTurnStarted(ctx, event) {
|
|
22
|
+
if (pointerMoved(ctx)) {
|
|
23
|
+
getLogger().info(`[lark-remote] turn_started write-back skipped: session pointer moved since run start runId=${ctx.runId} sessionId=${event.threadId}`);
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
ctx.sessionStore.setSessionIdAndCwd(ctx.userId, ctx.agentKind, event.threadId, ctx.cwd);
|
|
27
|
+
getLogger().info(`[lark-remote] turn_started session write-back runId=${ctx.runId} sessionId=${event.threadId}`);
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
export function writeBackResultSession(ctx, sessionId) {
|
|
31
|
+
if (!sessionId || pointerMoved(ctx))
|
|
32
|
+
return;
|
|
33
|
+
ctx.sessionStore.setSessionIdAndCwd(ctx.userId, ctx.agentKind, sessionId, ctx.cwd);
|
|
34
|
+
getLogger().info(`[lark-remote] result session write-back runId=${ctx.runId} sessionId=${sessionId}`);
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=session-writeback.js.map
|
|
@@ -76,11 +76,8 @@ export class BashCardSession extends CardSession {
|
|
|
76
76
|
if (this.state.terminal !== 'running') {
|
|
77
77
|
return;
|
|
78
78
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
// 保证顺序(pre-terminal 先落地 → terminal 后落地 → terminal 胜出)。
|
|
82
|
-
if (this.flushP)
|
|
83
|
-
await this.flushP;
|
|
79
|
+
// 终态 patch 必须排在已开始的 pre-terminal flush 之后。
|
|
80
|
+
await this.prepareTerminalUpdate();
|
|
84
81
|
this.state = {
|
|
85
82
|
...this.state,
|
|
86
83
|
terminal,
|
|
@@ -117,5 +117,7 @@ export declare abstract class CardSession<S, RO = unknown> {
|
|
|
117
117
|
* connector.updateCard if the controller is unavailable or fails.
|
|
118
118
|
*/
|
|
119
119
|
protected updateCard(): Promise<void>;
|
|
120
|
+
private waitForControllerReady;
|
|
121
|
+
protected prepareTerminalUpdate(): Promise<void>;
|
|
120
122
|
protected release(): void;
|
|
121
123
|
}
|
|
@@ -138,17 +138,7 @@ export class CardSession {
|
|
|
138
138
|
// 二次调用也套 timeout 保护:若 controllerReady 永不 settle(首次超时失败),
|
|
139
139
|
// 不能永久挂起。复用同样的 startTimeoutMs 超时。streamOutcome 已存在,
|
|
140
140
|
// 不重复创建 stream。
|
|
141
|
-
|
|
142
|
-
const timeout = new Promise((_, reject) => {
|
|
143
|
-
timer = setTimeout(() => reject(new Error(`${this.errorPrefix} stream start timeout`)), this.startTimeoutMs);
|
|
144
|
-
});
|
|
145
|
-
try {
|
|
146
|
-
await Promise.race([this.controllerReady, timeout]);
|
|
147
|
-
}
|
|
148
|
-
finally {
|
|
149
|
-
if (timer)
|
|
150
|
-
clearTimeout(timer);
|
|
151
|
-
}
|
|
141
|
+
await this.waitForControllerReady();
|
|
152
142
|
return;
|
|
153
143
|
}
|
|
154
144
|
// NOTE: the two renderCard calls below are NOT redundant. The streamCard
|
|
@@ -177,17 +167,7 @@ export class CardSession {
|
|
|
177
167
|
this.rejectController(error);
|
|
178
168
|
return { ok: false, error };
|
|
179
169
|
});
|
|
180
|
-
|
|
181
|
-
const timeout = new Promise((_, reject) => {
|
|
182
|
-
timer = setTimeout(() => reject(new Error(`${this.errorPrefix} stream start timeout`)), this.startTimeoutMs);
|
|
183
|
-
});
|
|
184
|
-
try {
|
|
185
|
-
await Promise.race([this.controllerReady, timeout]);
|
|
186
|
-
}
|
|
187
|
-
finally {
|
|
188
|
-
if (timer)
|
|
189
|
-
clearTimeout(timer);
|
|
190
|
-
}
|
|
170
|
+
await this.waitForControllerReady();
|
|
191
171
|
}
|
|
192
172
|
async settle() {
|
|
193
173
|
this.release();
|
|
@@ -247,6 +227,24 @@ export class CardSession {
|
|
|
247
227
|
}
|
|
248
228
|
}
|
|
249
229
|
}
|
|
230
|
+
async waitForControllerReady() {
|
|
231
|
+
let timer;
|
|
232
|
+
const timeout = new Promise((_, reject) => {
|
|
233
|
+
timer = setTimeout(() => reject(new Error(`${this.errorPrefix} stream start timeout`)), this.startTimeoutMs);
|
|
234
|
+
});
|
|
235
|
+
try {
|
|
236
|
+
await Promise.race([this.controllerReady, timeout]);
|
|
237
|
+
}
|
|
238
|
+
finally {
|
|
239
|
+
if (timer)
|
|
240
|
+
clearTimeout(timer);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
async prepareTerminalUpdate() {
|
|
244
|
+
this.cancelPendingFlush();
|
|
245
|
+
if (this.flushP)
|
|
246
|
+
await this.flushP;
|
|
247
|
+
}
|
|
250
248
|
release() {
|
|
251
249
|
if (this.released)
|
|
252
250
|
return;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { type AgentKind } from '../runner/
|
|
1
|
+
import { type AgentKind } from '../runner/types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Display name for an agent kind in the run card header.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Display names are static UI labels, so the constant is the single source.
|
|
6
|
+
* Keeping a second mutable registry here made render helpers depend on startup
|
|
7
|
+
* order and forced tests to reset global state.
|
|
8
8
|
*/
|
|
9
9
|
export declare function agentDisplayName(kind: string): string;
|
|
10
10
|
export declare function terminalToLabel(terminal: string): string;
|
package/dist/card/card-shared.js
CHANGED
|
@@ -1,18 +1,12 @@
|
|
|
1
|
-
import { AgentRegistry } from '../runner/index.js';
|
|
2
1
|
import { AGENT_DISPLAY_NAMES } from '../runner/types.js';
|
|
3
2
|
/**
|
|
4
3
|
* Display name for an agent kind in the run card header.
|
|
5
4
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* Display names are static UI labels, so the constant is the single source.
|
|
6
|
+
* Keeping a second mutable registry here made render helpers depend on startup
|
|
7
|
+
* order and forced tests to reset global state.
|
|
9
8
|
*/
|
|
10
9
|
export function agentDisplayName(kind) {
|
|
11
|
-
const globalReg = AgentRegistry.getGlobalInstance();
|
|
12
|
-
if (globalReg) {
|
|
13
|
-
return globalReg.getDisplayName(kind);
|
|
14
|
-
}
|
|
15
|
-
// Fallback for render helpers that run without the global registry.
|
|
16
10
|
return AGENT_DISPLAY_NAMES[kind] ?? kind;
|
|
17
11
|
}
|
|
18
12
|
const TERMINAL_LABELS = {
|
package/dist/card/collapsible.js
CHANGED
|
@@ -76,8 +76,9 @@ export function markdownDiv(content, textSize) {
|
|
|
76
76
|
* to be interpreted as a literal. Escape it to preserve the raw backslash.
|
|
77
77
|
*
|
|
78
78
|
* Note: Square brackets [] and pipe | are NOT escaped -- they are valid markdown
|
|
79
|
-
* (links, tables) and escaping them breaks link rendering. Verified
|
|
80
|
-
* tests/escape-bracket-
|
|
79
|
+
* (links, tables) and escaping them breaks link rendering. Verified by the live
|
|
80
|
+
* regression test tests/feishu-live/escape-bracket-regression.test.ts: unescaped
|
|
81
|
+
* []| does not trigger 11311.
|
|
81
82
|
*/
|
|
82
83
|
function escapeMarkdown(content) {
|
|
83
84
|
if (!content)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Split an authoritative final answer into plain-text messages without losing
|
|
3
|
+
* any content. Feishu cards have a hard size budget; a long answer must spill
|
|
4
|
+
* rather than being silently truncated inside the progress card.
|
|
5
|
+
*/
|
|
6
|
+
export declare const FINAL_ANSWER_CHUNK_BYTES = 20000;
|
|
7
|
+
export declare function splitFinalAnswer(text: string, maxBytes?: number): string[];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Split an authoritative final answer into plain-text messages without losing
|
|
3
|
+
* any content. Feishu cards have a hard size budget; a long answer must spill
|
|
4
|
+
* rather than being silently truncated inside the progress card.
|
|
5
|
+
*/
|
|
6
|
+
export const FINAL_ANSWER_CHUNK_BYTES = 20_000;
|
|
7
|
+
export function splitFinalAnswer(text, maxBytes = FINAL_ANSWER_CHUNK_BYTES) {
|
|
8
|
+
if (!text || maxBytes <= 0)
|
|
9
|
+
return text ? [text] : [];
|
|
10
|
+
const chunks = [];
|
|
11
|
+
let rest = text;
|
|
12
|
+
let remainingBytes = Buffer.byteLength(rest, 'utf8');
|
|
13
|
+
while (remainingBytes > maxBytes) {
|
|
14
|
+
let bytes = 0;
|
|
15
|
+
let end = 0;
|
|
16
|
+
let lastNewline = 0;
|
|
17
|
+
for (const char of rest) {
|
|
18
|
+
const charBytes = Buffer.byteLength(char, 'utf8');
|
|
19
|
+
if (bytes + charBytes > maxBytes)
|
|
20
|
+
break;
|
|
21
|
+
bytes += charBytes;
|
|
22
|
+
end += char.length;
|
|
23
|
+
if (char === '\n')
|
|
24
|
+
lastNewline = end;
|
|
25
|
+
}
|
|
26
|
+
if (end === 0) {
|
|
27
|
+
// A caller can choose a byte limit smaller than one codepoint. Keep one
|
|
28
|
+
// codepoint per chunk rather than looping forever with a zero-length cut.
|
|
29
|
+
const first = Array.from(rest)[0];
|
|
30
|
+
chunks.push(first);
|
|
31
|
+
rest = rest.slice(first.length);
|
|
32
|
+
remainingBytes -= Buffer.byteLength(first, 'utf8');
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
// Prefer a paragraph/line boundary when it does not waste most of a chunk.
|
|
36
|
+
const cut = lastNewline > 0 && lastNewline >= Math.floor(end * 0.6) ? lastNewline : end;
|
|
37
|
+
const chunk = rest.slice(0, cut);
|
|
38
|
+
chunks.push(chunk);
|
|
39
|
+
rest = rest.slice(cut);
|
|
40
|
+
remainingBytes -= Buffer.byteLength(chunk, 'utf8');
|
|
41
|
+
}
|
|
42
|
+
if (rest)
|
|
43
|
+
chunks.push(rest);
|
|
44
|
+
return chunks;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=final-answer-overflow.js.map
|
|
@@ -46,13 +46,8 @@ export class RunCardSession extends CardSession {
|
|
|
46
46
|
}
|
|
47
47
|
async finish(terminal, meta = {}) {
|
|
48
48
|
// 终态必须即时显示(spec):取消合批窗口,不等延迟 flush 调度。
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
// 否则两条并发 controller.update 无 FIFO 保证,in-flight 的 pre-terminal
|
|
52
|
-
// patch 可能在终态 patch 之后 resolve → 用户看到"思考中"终帧。
|
|
53
|
-
// await 保证顺序:pre-terminal 先落地 → terminal 后落地 → terminal 胜出。
|
|
54
|
-
if (this.flushP)
|
|
55
|
-
await this.flushP;
|
|
49
|
+
// 终态 patch 必须排在已开始的 pre-terminal flush 之后。
|
|
50
|
+
await this.prepareTerminalUpdate();
|
|
56
51
|
this.state = finishRun(this.state, terminal, meta);
|
|
57
52
|
await this.updateCard();
|
|
58
53
|
this.release();
|
|
@@ -20,6 +20,12 @@ export interface RunCardRenderOptions {
|
|
|
20
20
|
*/
|
|
21
21
|
sessionFallbackAvailable?: boolean;
|
|
22
22
|
}
|
|
23
|
+
export interface RunCardRenderOutcome {
|
|
24
|
+
card: object;
|
|
25
|
+
finalAnswerStatus: 'none' | 'full' | 'overflow';
|
|
26
|
+
/** Full text to deliver outside the card when finalAnswerStatus is overflow. */
|
|
27
|
+
finalAnswerText?: string;
|
|
28
|
+
}
|
|
23
29
|
type BlockGroup = {
|
|
24
30
|
kind: 'thinking';
|
|
25
31
|
content: string;
|
|
@@ -73,4 +79,12 @@ export declare function estimateCardBytes(state: RunState, options?: RunCardRend
|
|
|
73
79
|
* CardKit 2.0 renderer - simplified for streaming (no tabs)
|
|
74
80
|
*/
|
|
75
81
|
export declare function renderRunCard(state: RunState, options?: RunCardRenderOptions): object;
|
|
82
|
+
/**
|
|
83
|
+
* Render a run card with metadata the bridge needs to deliver an answer that
|
|
84
|
+
* cannot fit in one Feishu card. `renderRunCard` remains the card-only API used
|
|
85
|
+
* by streaming sessions.
|
|
86
|
+
*/
|
|
87
|
+
export declare function renderRunCardDetailed(state: RunState, options?: RunCardRenderOptions): RunCardRenderOutcome;
|
|
88
|
+
/** Exact-match marker used in the card before the answer is sent separately. */
|
|
89
|
+
export declare const FINAL_ANSWER_OVERFLOW_MARKER = "_\u6700\u7EC8\u56DE\u7B54\u8F83\u957F\uFF0C\u5B8C\u6574\u5185\u5BB9\u5DF2\u968F\u540E\u53D1\u9001_";
|
|
76
90
|
export {};
|
|
@@ -867,6 +867,20 @@ function measurePanelBytes(title, expanded, border, content) {
|
|
|
867
867
|
* CardKit 2.0 renderer - simplified for streaming (no tabs)
|
|
868
868
|
*/
|
|
869
869
|
export function renderRunCard(state, options = {}) {
|
|
870
|
+
return renderRunCardDetailed(state, options).card;
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Render a run card with metadata the bridge needs to deliver an answer that
|
|
874
|
+
* cannot fit in one Feishu card. `renderRunCard` remains the card-only API used
|
|
875
|
+
* by streaming sessions.
|
|
876
|
+
*/
|
|
877
|
+
export function renderRunCardDetailed(state, options = {}) {
|
|
878
|
+
if (state.finalAnswer?.content.trim()) {
|
|
879
|
+
return buildFinalAnswerPriorityCard(state, options);
|
|
880
|
+
}
|
|
881
|
+
return { card: renderLegacyRunCard(state, options), finalAnswerStatus: 'none' };
|
|
882
|
+
}
|
|
883
|
+
function renderLegacyRunCard(state, options) {
|
|
870
884
|
// 先估后建:用廉价估算预测是否超 28KB 预算,明显超预算直接跳过完整卡构建走
|
|
871
885
|
// degraded。groups/prepared 只算一次,测量与正式渲染共用同一份截断结果,
|
|
872
886
|
// 避免影子测量 + 正式渲染对每个超限块双倍截断。
|
|
@@ -927,6 +941,110 @@ export function renderRunCard(state, options = {}) {
|
|
|
927
941
|
};
|
|
928
942
|
return truncatedSkeleton;
|
|
929
943
|
}
|
|
944
|
+
/** Exact-match marker used in the card before the answer is sent separately. */
|
|
945
|
+
export const FINAL_ANSWER_OVERFLOW_MARKER = '_最终回答较长,完整内容已随后发送_';
|
|
946
|
+
/**
|
|
947
|
+
* Build a card around the authoritative final answer.
|
|
948
|
+
*
|
|
949
|
+
* The answer is measured first with the fixed controls. Progress groups are
|
|
950
|
+
* added newest-first only while the whole card still fits. If the answer
|
|
951
|
+
* cannot fit even without progress/summary, the card carries an explicit
|
|
952
|
+
* marker and the bridge sends the complete answer separately.
|
|
953
|
+
*/
|
|
954
|
+
function buildFinalAnswerPriorityCard(state, options) {
|
|
955
|
+
const finalAnswer = state.finalAnswer;
|
|
956
|
+
const groups = groupBlocks(state.blocks);
|
|
957
|
+
const prepared = prepareGroupContent(groups);
|
|
958
|
+
const safeAnswer = truncateMarkdownTables(finalAnswer.content);
|
|
959
|
+
const requiresOverflow = safeAnswer !== finalAnswer.content;
|
|
960
|
+
const ts = formatTimestamp(finalAnswer.timestamp);
|
|
961
|
+
const answerElements = renderTextBlock(safeAnswer, ts, state.terminal !== 'running');
|
|
962
|
+
const fixedStart = [statusRow(state), ...omittedBlocksHint(state)];
|
|
963
|
+
const summary = buildSummaryContent(state, { includeNotices: false });
|
|
964
|
+
const controls = [...actionRow(state, options), ...approvalArea(state)];
|
|
965
|
+
const buildWithProgress = (start, includeSummary) => {
|
|
966
|
+
const renderedProgress = buildAnswerProgressContent(state, prepared, groups.slice(start));
|
|
967
|
+
const progress = start < groups.length
|
|
968
|
+
? [markdownDiv(`_💡 已省略 ${start} 个早期步骤_`), ...renderedProgress]
|
|
969
|
+
: renderedProgress;
|
|
970
|
+
return assembleRunCard(state, options, [
|
|
971
|
+
...fixedStart,
|
|
972
|
+
...progress,
|
|
973
|
+
...(requiresOverflow ? [markdownDiv(FINAL_ANSWER_OVERFLOW_MARKER)] : []),
|
|
974
|
+
...answerElements,
|
|
975
|
+
...(includeSummary ? summary : []),
|
|
976
|
+
...controls,
|
|
977
|
+
]);
|
|
978
|
+
};
|
|
979
|
+
let best = buildWithProgress(groups.length, true);
|
|
980
|
+
if (withinRunCardBudget(best)) {
|
|
981
|
+
for (let start = groups.length - 1; start >= 0; start--) {
|
|
982
|
+
const candidate = buildWithProgress(start, true);
|
|
983
|
+
if (!withinRunCardBudget(candidate))
|
|
984
|
+
break;
|
|
985
|
+
best = candidate;
|
|
986
|
+
}
|
|
987
|
+
return {
|
|
988
|
+
card: best,
|
|
989
|
+
finalAnswerStatus: requiresOverflow ? 'overflow' : 'full',
|
|
990
|
+
...(requiresOverflow ? { finalAnswerText: finalAnswer.content } : {}),
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
// The answer outranks the usage summary. Keep the answer and controls first,
|
|
994
|
+
// then retry without the summary before declaring a physical overflow.
|
|
995
|
+
const withoutSummary = buildWithProgress(groups.length, false);
|
|
996
|
+
if (withinRunCardBudget(withoutSummary)) {
|
|
997
|
+
return {
|
|
998
|
+
card: withoutSummary,
|
|
999
|
+
finalAnswerStatus: requiresOverflow ? 'overflow' : 'full',
|
|
1000
|
+
...(requiresOverflow ? { finalAnswerText: finalAnswer.content } : {}),
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
const overflowCard = assembleRunCard(state, options, [
|
|
1004
|
+
...fixedStart,
|
|
1005
|
+
markdownDiv(FINAL_ANSWER_OVERFLOW_MARKER),
|
|
1006
|
+
...controls,
|
|
1007
|
+
]);
|
|
1008
|
+
return {
|
|
1009
|
+
card: overflowCard,
|
|
1010
|
+
finalAnswerStatus: 'overflow',
|
|
1011
|
+
finalAnswerText: finalAnswer.content,
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
/** Render a contiguous progress suffix for the answer-priority card. */
|
|
1015
|
+
function buildAnswerProgressContent(state, prepared, visible) {
|
|
1016
|
+
const elements = [];
|
|
1017
|
+
const outputMax = toolOutputBudget(state);
|
|
1018
|
+
const contentItems = mergeNoticesIntoContent(visible, state.notices);
|
|
1019
|
+
for (const item of contentItems) {
|
|
1020
|
+
if (item.kind === 'notice') {
|
|
1021
|
+
elements.push(renderNoticeRow(item.notice));
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
const group = item;
|
|
1025
|
+
if (group.kind === 'tool') {
|
|
1026
|
+
elements.push(renderTool(group.tool, outputMax));
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
if (group.kind === 'text') {
|
|
1030
|
+
const content = prepared.get(group) ?? '';
|
|
1031
|
+
const ts = formatTimestamp(group.timestamp);
|
|
1032
|
+
elements.push(...renderTextBlock(content, ts, state.terminal !== 'running'));
|
|
1033
|
+
continue;
|
|
1034
|
+
}
|
|
1035
|
+
const spec = describePanelGroup(group, prepared);
|
|
1036
|
+
if (!spec)
|
|
1037
|
+
continue;
|
|
1038
|
+
elements.push(collapsibleMarkdownPanel({
|
|
1039
|
+
title: spec.title,
|
|
1040
|
+
expanded: spec.expanded,
|
|
1041
|
+
border: spec.border,
|
|
1042
|
+
content: spec.content,
|
|
1043
|
+
textSize: 'notation',
|
|
1044
|
+
}));
|
|
1045
|
+
}
|
|
1046
|
+
return elements;
|
|
1047
|
+
}
|
|
930
1048
|
/**
|
|
931
1049
|
* Run 卡片的双预算检查:UTF-8 字节 ≤ 28KB **且** 元素数 ≤ `MAX_CARD_ELEMENTS`。
|
|
932
1050
|
*
|
|
@@ -1037,7 +1155,7 @@ function buildSummaryContent(state, options = {}) {
|
|
|
1037
1155
|
}
|
|
1038
1156
|
else {
|
|
1039
1157
|
const result = state.resultSubtype ?? 'success';
|
|
1040
|
-
const hasContent = state.blocks.length > 0;
|
|
1158
|
+
const hasContent = state.blocks.length > 0 || !!state.finalAnswer?.content.trim();
|
|
1041
1159
|
// 压缩(operationKind='compaction')不会返回 agent 正文——引擎只压缩上下文
|
|
1042
1160
|
// 窗口,空内容属正常,不能沿用普通 run 的「(未返回内容)」异常信号
|
|
1043
1161
|
// (2026-09 用户反馈)。所有会压缩的 agent(codex/claude/kimi/opencode/pi)
|
package/dist/card/run-state.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export type RunTerminal = 'running' | 'finalizing' | 'done' | 'error' | 'interru
|
|
|
13
13
|
* 禁止各自再抄一份字面量——三处漂移过一次(goal_continuing 只进白名单不进
|
|
14
14
|
* 按钮判据),新增终态时必漏一处的风险是结构性的。
|
|
15
15
|
*/
|
|
16
|
-
|
|
16
|
+
declare const RUN_TERMINAL_STATES: readonly ["done", "error", "interrupted", "idle_timeout", "goal_continuing"];
|
|
17
17
|
export type RunFinalTerminal = (typeof RUN_TERMINAL_STATES)[number];
|
|
18
18
|
/** 该状态是否已收尾(非 running/finalizing)。 */
|
|
19
19
|
export declare function isRunTerminal(terminal: RunTerminal): terminal is RunFinalTerminal;
|
|
@@ -85,6 +85,12 @@ export interface RunNotice {
|
|
|
85
85
|
text: string;
|
|
86
86
|
timestamp?: string;
|
|
87
87
|
}
|
|
88
|
+
export interface FinalAnswerState {
|
|
89
|
+
content: string;
|
|
90
|
+
timestamp?: string;
|
|
91
|
+
complete: boolean;
|
|
92
|
+
source: 'protocol' | 'inferred';
|
|
93
|
+
}
|
|
88
94
|
export interface RunState {
|
|
89
95
|
runId: string;
|
|
90
96
|
terminal: RunTerminal;
|
|
@@ -94,6 +100,11 @@ export interface RunState {
|
|
|
94
100
|
/** 本 run 结束时间(epoch ms):终态后耗时定格,不再随渲染时刻增长。 */
|
|
95
101
|
finishedAt?: number;
|
|
96
102
|
blocks: RunBlock[];
|
|
103
|
+
/**
|
|
104
|
+
* The answer the user is waiting for. Kept outside `blocks` so progress
|
|
105
|
+
* traffic cannot evict or consume its display budget.
|
|
106
|
+
*/
|
|
107
|
+
finalAnswer?: FinalAnswerState;
|
|
97
108
|
sessionId?: string;
|
|
98
109
|
resultSubtype?: 'success' | 'error' | 'interrupted';
|
|
99
110
|
/** interrupted 终态的中断来源;undefined = 未归类(渲染默认文案)。 */
|