lark-remote 0.1.7 → 0.1.8
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 +28 -0
- package/dist/bridge/approval-coordinator.d.ts +15 -1
- package/dist/bridge/approval-coordinator.js +35 -2
- package/dist/bridge/index.d.ts +19 -6
- package/dist/bridge/index.js +25 -8
- package/dist/card/approval-render.js +187 -45
- package/dist/card/card-shared.d.ts +6 -4
- package/dist/card/card-shared.js +6 -4
- package/dist/card/run-renderer.d.ts +7 -0
- package/dist/card/run-renderer.js +22 -11
- package/dist/config/index.d.ts +24 -1
- package/dist/config/index.js +34 -3
- package/dist/index.js +28 -14
- package/dist/router/card-action-payload.d.ts +30 -0
- package/dist/router/card-action-payload.js +39 -0
- package/dist/router/config/claude.js +8 -3
- package/dist/router/config/dsh.d.ts +51 -0
- package/dist/router/config/dsh.js +179 -0
- package/dist/router/config/index.d.ts +1 -1
- package/dist/router/config/index.js +4 -1
- package/dist/router/config/types.d.ts +6 -0
- package/dist/router/index.d.ts +20 -1
- package/dist/router/index.js +113 -7
- package/dist/router/utils.js +12 -2
- package/dist/runner/agent-choices-common.js +3 -0
- package/dist/runner/claude/session.js +24 -5
- package/dist/runner/codex/app-server/protocol-types.d.ts +35 -0
- package/dist/runner/codex/app-server/protocol-types.js +1 -1
- package/dist/runner/codex/app-server/runner.d.ts +10 -0
- package/dist/runner/codex/app-server/runner.js +56 -1
- package/dist/runner/codex/app-server/translator.d.ts +7 -0
- package/dist/runner/codex/app-server/translator.js +45 -0
- package/dist/runner/common/acp/protocol-types.d.ts +33 -0
- package/dist/runner/common/acp/protocol-types.js +2 -0
- package/dist/runner/dsh/client.d.ts +95 -0
- package/dist/runner/dsh/client.js +418 -0
- package/dist/runner/dsh/fake-dsh-server.d.ts +60 -0
- package/dist/runner/dsh/fake-dsh-server.js +156 -0
- package/dist/runner/dsh/index.d.ts +3 -0
- package/dist/runner/dsh/index.js +4 -0
- package/dist/runner/dsh/runner.d.ts +57 -0
- package/dist/runner/dsh/runner.js +191 -0
- package/dist/runner/dsh/translator.d.ts +28 -0
- package/dist/runner/dsh/translator.js +98 -0
- package/dist/runner/dsh/types.d.ts +155 -0
- package/dist/runner/dsh/types.js +14 -0
- package/dist/runner/index.d.ts +1 -0
- package/dist/runner/index.js +1 -0
- package/dist/runner/kimi/acp/runner.js +69 -1
- package/dist/runner/kimi/acp/translator.d.ts +14 -0
- package/dist/runner/kimi/acp/translator.js +108 -3
- package/dist/runner/pi/rpc/runner.d.ts +1 -7
- package/dist/runner/pi/rpc/runner.js +2 -11
- package/dist/runner/probe.js +6 -1
- package/dist/runner/question-common.d.ts +26 -0
- package/dist/runner/question-common.js +42 -0
- package/dist/runner/types.d.ts +63 -10
- package/dist/session/claude/sessions.js +37 -15
- package/dist/session/dsh/index.d.ts +1 -0
- package/dist/session/dsh/index.js +2 -0
- package/dist/session/dsh/sessions.d.ts +39 -0
- package/dist/session/dsh/sessions.js +237 -0
- package/dist/session/index.d.ts +1 -0
- package/dist/session/index.js +1 -0
- package/dist/workspace/index.d.ts +4 -0
- package/dist/workspace/index.js +8 -1
- package/package.json +2 -1
|
@@ -81,6 +81,14 @@ export declare class KimiAcpTranslator {
|
|
|
81
81
|
* Handle a server request (reverse RPC from the ACP server).
|
|
82
82
|
*/
|
|
83
83
|
handleServerRequest(id: number | string, method: string, params: unknown): AcpTranslatorEvent[];
|
|
84
|
+
/**
|
|
85
|
+
* Kimi elicitation form:requestedSchema.properties 按顺序解析为 UserQuestion[]
|
|
86
|
+
* (type:array → 多选;oneOf/anyOf const → 选项 label;title → header??question)。
|
|
87
|
+
* 题面完整文本只在 form 级 message 里合并出现(逐字段 title 可能只是短 header),
|
|
88
|
+
* 因此 message 作为卡片概要行(view.intro)透传。
|
|
89
|
+
* form 会丢弃非声明选项值 → isOther=false(卡片隐藏自定义答案输入)。
|
|
90
|
+
*/
|
|
91
|
+
private handleElicitationCreate;
|
|
84
92
|
/**
|
|
85
93
|
* Produce a turn_started event. Called by the runner when session/prompt
|
|
86
94
|
* is sent (no wire notification for this — self-produced).
|
|
@@ -108,6 +116,12 @@ export declare class KimiAcpTranslator {
|
|
|
108
116
|
private handleToolCallUpdate;
|
|
109
117
|
private handleUsageUpdate;
|
|
110
118
|
private handleRequestPermission;
|
|
119
|
+
/**
|
|
120
|
+
* request_permission 兜底桥(elicitation/create 失败或旧版 kimi):
|
|
121
|
+
* 选项为 q{n}_opt_{i}(allow_once,label 直取 name)+ q{n}_skip(reject_once,
|
|
122
|
+
* 跳过语义)。单题单选、无 Other(kimi 桥丢弃非声明值)。
|
|
123
|
+
*/
|
|
124
|
+
private handlePermissionQuestion;
|
|
111
125
|
/**
|
|
112
126
|
* Derive the approval decision list from the server's offered options.
|
|
113
127
|
* accept/decline/cancel are universal; acceptForSession requires an
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* turn_started (self-produced) → operationKind 'turn'/'compaction'
|
|
29
29
|
* session/request_permission → approval_requested
|
|
30
30
|
*/
|
|
31
|
+
import { makeQuestionApprovalEvent } from '../../question-common.js';
|
|
31
32
|
import { NotificationMethod, ServerRequestMethod, SessionEventType, } from '../../common/acp/protocol-types.js';
|
|
32
33
|
import { getLogger } from '../../../logger/index.js';
|
|
33
34
|
/**
|
|
@@ -96,8 +97,41 @@ export class KimiAcpTranslator {
|
|
|
96
97
|
if (method === ServerRequestMethod.REQUEST_PERMISSION) {
|
|
97
98
|
return this.handleRequestPermission(id, params);
|
|
98
99
|
}
|
|
100
|
+
if (method === ServerRequestMethod.ELICITATION_CREATE) {
|
|
101
|
+
return this.handleElicitationCreate(id, params);
|
|
102
|
+
}
|
|
99
103
|
return [];
|
|
100
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Kimi elicitation form:requestedSchema.properties 按顺序解析为 UserQuestion[]
|
|
107
|
+
* (type:array → 多选;oneOf/anyOf const → 选项 label;title → header??question)。
|
|
108
|
+
* 题面完整文本只在 form 级 message 里合并出现(逐字段 title 可能只是短 header),
|
|
109
|
+
* 因此 message 作为卡片概要行(view.intro)透传。
|
|
110
|
+
* form 会丢弃非声明选项值 → isOther=false(卡片隐藏自定义答案输入)。
|
|
111
|
+
*/
|
|
112
|
+
handleElicitationCreate(requestId, params) {
|
|
113
|
+
const properties = params.requestedSchema?.properties ?? {};
|
|
114
|
+
const questions = [];
|
|
115
|
+
for (const [key, prop] of Object.entries(properties)) {
|
|
116
|
+
const q = elicitationPropertyToQuestion(prop);
|
|
117
|
+
if (!q) {
|
|
118
|
+
getLogger().warn(`[kimi-acp-translator] elicitation property skipped: key=${key}`);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
questions.push(q);
|
|
122
|
+
}
|
|
123
|
+
if (questions.length === 0) {
|
|
124
|
+
// 无法解析 → 交还 runner 自动响应(不悬挂服务端)。
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
const message = params.message?.trim();
|
|
128
|
+
const single = questions.length === 1;
|
|
129
|
+
const intro = message && (!single || message !== questions[0].question) ? message : undefined;
|
|
130
|
+
const event = makeQuestionApprovalEvent(requestId, questions, params.sessionId);
|
|
131
|
+
if (intro)
|
|
132
|
+
event.view.intro = intro;
|
|
133
|
+
return [event];
|
|
134
|
+
}
|
|
101
135
|
/**
|
|
102
136
|
* Produce a turn_started event. Called by the runner when session/prompt
|
|
103
137
|
* is sent (no wire notification for this — self-produced).
|
|
@@ -274,9 +308,20 @@ export class KimiAcpTranslator {
|
|
|
274
308
|
// Private approval handlers
|
|
275
309
|
// =========================================================================
|
|
276
310
|
handleRequestPermission(requestId, params) {
|
|
277
|
-
// §5.4
|
|
278
|
-
|
|
279
|
-
|
|
311
|
+
// §5.4 升级:question elicitation 识别(isQuestion 标记 / AskUserQuestion
|
|
312
|
+
// toolCall 标题 / q{n}_opt_* 选项命名空间)→ 渲染为提问卡;无法解析的
|
|
313
|
+
// 退化形态(无 toolCall 也无选项)仍返回 [] 由 runner 自动响应 cancelled。
|
|
314
|
+
const toolCall = params.toolCall;
|
|
315
|
+
const questionMarker = params.isQuestion === true ||
|
|
316
|
+
toolCall?.title === 'AskUserQuestion' ||
|
|
317
|
+
optionIdsMatchQuestionNamespace(params.options);
|
|
318
|
+
if (questionMarker) {
|
|
319
|
+
return this.handlePermissionQuestion(requestId, params);
|
|
320
|
+
}
|
|
321
|
+
// §5.4:无 toolCall 且无提问标记 → 无法构成审批视图,返回 [] 由 runner
|
|
322
|
+
// 自动响应 cancelled(服务端不会悬挂)。
|
|
323
|
+
if (!toolCall) {
|
|
324
|
+
getLogger().info(`[kimi-acp-translator] permission request without toolCall, auto-responding cancelled (requestId=${requestId})`);
|
|
280
325
|
return [];
|
|
281
326
|
}
|
|
282
327
|
const view = {
|
|
@@ -301,6 +346,27 @@ export class KimiAcpTranslator {
|
|
|
301
346
|
},
|
|
302
347
|
];
|
|
303
348
|
}
|
|
349
|
+
/**
|
|
350
|
+
* request_permission 兜底桥(elicitation/create 失败或旧版 kimi):
|
|
351
|
+
* 选项为 q{n}_opt_{i}(allow_once,label 直取 name)+ q{n}_skip(reject_once,
|
|
352
|
+
* 跳过语义)。单题单选、无 Other(kimi 桥丢弃非声明值)。
|
|
353
|
+
*/
|
|
354
|
+
handlePermissionQuestion(requestId, params) {
|
|
355
|
+
const selectable = (params.options ?? []).filter((opt) => /^q\d+_opt_\d+$/.test(opt.optionId));
|
|
356
|
+
if (selectable.length === 0) {
|
|
357
|
+
getLogger().info(`[kimi-acp-translator] question elicitation unparseable, auto-responding cancelled (requestId=${requestId})`);
|
|
358
|
+
return [];
|
|
359
|
+
}
|
|
360
|
+
const questionText = extractQuestionText(params.toolCall);
|
|
361
|
+
const questions = [
|
|
362
|
+
{
|
|
363
|
+
question: questionText,
|
|
364
|
+
isOther: false,
|
|
365
|
+
options: selectable.map((opt) => ({ label: opt.name })),
|
|
366
|
+
},
|
|
367
|
+
];
|
|
368
|
+
return [makeQuestionApprovalEvent(requestId, questions, params.sessionId)];
|
|
369
|
+
}
|
|
304
370
|
/**
|
|
305
371
|
* Derive the approval decision list from the server's offered options.
|
|
306
372
|
* accept/decline/cancel are universal; acceptForSession requires an
|
|
@@ -333,4 +399,43 @@ export class KimiAcpTranslator {
|
|
|
333
399
|
function truncate(text, maxLen) {
|
|
334
400
|
return text.length <= maxLen ? text : text.slice(0, maxLen) + '…';
|
|
335
401
|
}
|
|
402
|
+
/** elicitation schema property → UserQuestion(无法解析返回 null)。 */
|
|
403
|
+
function elicitationPropertyToQuestion(prop) {
|
|
404
|
+
const title = prop.title?.trim();
|
|
405
|
+
if (!title)
|
|
406
|
+
return null;
|
|
407
|
+
const multiSelect = prop.type === 'array';
|
|
408
|
+
const rawOptions = multiSelect ? prop.items?.anyOf : prop.oneOf;
|
|
409
|
+
const options = (rawOptions ?? [])
|
|
410
|
+
.filter((opt) => typeof opt?.const === 'string')
|
|
411
|
+
.map((opt) => ({
|
|
412
|
+
label: opt.const,
|
|
413
|
+
...(opt.description ? { description: opt.description } : {}),
|
|
414
|
+
}));
|
|
415
|
+
if (options.length === 0)
|
|
416
|
+
return null;
|
|
417
|
+
return {
|
|
418
|
+
question: title,
|
|
419
|
+
header: title,
|
|
420
|
+
multiSelect,
|
|
421
|
+
isOther: false,
|
|
422
|
+
options,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
/** 选项命名空间是否匹配 kimi question 桥(q{n}_opt_{i} / q{n}_skip)。 */
|
|
426
|
+
function optionIdsMatchQuestionNamespace(options) {
|
|
427
|
+
return (options ?? []).some((opt) => /^q\d+_(opt_\d+|skip)$/.test(opt.optionId));
|
|
428
|
+
}
|
|
429
|
+
/** 从 AskUserQuestion toolCall 提取题面(content 文本优先,标题兜底)。 */
|
|
430
|
+
function extractQuestionText(toolCall) {
|
|
431
|
+
const content = toolCall?.content;
|
|
432
|
+
if (Array.isArray(content)) {
|
|
433
|
+
for (const block of content) {
|
|
434
|
+
const text = block.content?.text;
|
|
435
|
+
if (typeof text === 'string' && text.trim())
|
|
436
|
+
return text.trim();
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return toolCall?.title?.trim() || '请选择';
|
|
440
|
+
}
|
|
336
441
|
//# sourceMappingURL=translator.js.map
|
|
@@ -14,8 +14,7 @@
|
|
|
14
14
|
* turn idle watchdog, connection-lost retry). Differences:
|
|
15
15
|
* - pi binds the session at spawn via `--session-id` (no RPC resume-by-id);
|
|
16
16
|
* the PiRpcConnectionManager respawns when the requested session changes.
|
|
17
|
-
* -
|
|
18
|
-
* respondApproval is a no-op.
|
|
17
|
+
* - pi,极简风格,没有 Ask User question。
|
|
19
18
|
* - `prompt` ack ≠ turn completion; the turn ends on `agent_settled`.
|
|
20
19
|
*/
|
|
21
20
|
import type { AgentSessionReader, AgentEvent, AgentStatusInfo, SpawnOptions } from '../../types.js';
|
|
@@ -71,10 +70,5 @@ export declare class PiRpcRunner extends ConnectionBasedRunner<PiRpcClient, PiRp
|
|
|
71
70
|
* completion (the completion signal); `compaction_*` events stream meanwhile.
|
|
72
71
|
*/
|
|
73
72
|
runCompact(_message: string, opts: SpawnOptions): AsyncGenerator<AgentEvent>;
|
|
74
|
-
/**
|
|
75
|
-
* Approvals are out of scope for pi RPC (no server requests surface in this
|
|
76
|
-
* integration); the bridge's duck-typed respondApproval call is a no-op.
|
|
77
|
-
*/
|
|
78
|
-
respondApproval(_requestId: number | string, _response: unknown): Promise<void>;
|
|
79
73
|
private handleEvent;
|
|
80
74
|
}
|
|
@@ -14,8 +14,7 @@
|
|
|
14
14
|
* turn idle watchdog, connection-lost retry). Differences:
|
|
15
15
|
* - pi binds the session at spawn via `--session-id` (no RPC resume-by-id);
|
|
16
16
|
* the PiRpcConnectionManager respawns when the requested session changes.
|
|
17
|
-
* -
|
|
18
|
-
* respondApproval is a no-op.
|
|
17
|
+
* - pi,极简风格,没有 Ask User question。
|
|
19
18
|
* - `prompt` ack ≠ turn completion; the turn ends on `agent_settled`.
|
|
20
19
|
*/
|
|
21
20
|
import { ConnectionBasedRunner } from '../../common/connection-based-runner.js';
|
|
@@ -172,16 +171,8 @@ export class PiRpcRunner extends ConnectionBasedRunner {
|
|
|
172
171
|
this.pushEvents([translator.produceCompactResult(opts.sessionId, response)]);
|
|
173
172
|
});
|
|
174
173
|
}
|
|
175
|
-
/**
|
|
176
|
-
* Approvals are out of scope for pi RPC (no server requests surface in this
|
|
177
|
-
* integration); the bridge's duck-typed respondApproval call is a no-op.
|
|
178
|
-
*/
|
|
179
|
-
async respondApproval(_requestId, _response) {
|
|
180
|
-
// no-op
|
|
181
|
-
}
|
|
182
174
|
handleEvent(evt) {
|
|
183
|
-
|
|
184
|
-
this.pushEvents(events);
|
|
175
|
+
this.pushEvents(this.currentTranslator?.handleEvent(evt) ?? []);
|
|
185
176
|
}
|
|
186
177
|
}
|
|
187
178
|
/** Build pi `--mode rpc` spawn args. `sessionId` is bound at spawn (pi has no
|
package/dist/runner/probe.js
CHANGED
|
@@ -28,6 +28,9 @@ const BINARY_MAP = {
|
|
|
28
28
|
opencode: 'opencode',
|
|
29
29
|
pi: 'pi',
|
|
30
30
|
kimi: 'kimi',
|
|
31
|
+
// DSH is an HTTP-only agent (no CLI). `which ''` fails → reported unavailable,
|
|
32
|
+
// which is semantically correct for the /config availability display.
|
|
33
|
+
dsh: '',
|
|
31
34
|
};
|
|
32
35
|
const CACHE_TTL_MS = 5 * 60_000;
|
|
33
36
|
/** Module-level cache, survives across calls within the process. */
|
|
@@ -65,7 +68,9 @@ export async function probeAgentAvailability(kind) {
|
|
|
65
68
|
* on resource-constrained devices.
|
|
66
69
|
*/
|
|
67
70
|
export async function probeAllAgents() {
|
|
68
|
-
|
|
71
|
+
// Skip agents with no CLI binary (DSH is HTTP-only, binary === ''): the
|
|
72
|
+
// probe / availability display only covers spawn-able agents.
|
|
73
|
+
const kinds = Object.keys(BINARY_MAP).filter((k) => BINARY_MAP[k] !== '');
|
|
69
74
|
const entries = await Promise.all(kinds.map(async (k) => [k, await probeAgentAvailability(k)]));
|
|
70
75
|
return new Map(entries);
|
|
71
76
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AskUserQuestion 公共工厂与映射(agent 无关)。
|
|
3
|
+
*
|
|
4
|
+
* 接入约定(types.ts UserQuestion 注释的落点):
|
|
5
|
+
* - 各 runner 用 makeQuestionApprovalEvent 构造标准 question 审批事件,
|
|
6
|
+
* 避免四处手拼事件形状(claude/codex/kimi/pi 共用);
|
|
7
|
+
* - 用户答案经 ApprovalCoordinator 以 {问题文本: 值} 回传 runner 后,
|
|
8
|
+
* 用 mapAnswersByIndex 按问题顺序展开,再做协议级编码
|
|
9
|
+
* (Codex 问题 id、Kimi q{i}、Pi 单题取值)。
|
|
10
|
+
*/
|
|
11
|
+
import type { ApprovalRequestedEvent, UserQuestion } from './types.js';
|
|
12
|
+
export interface QuestionEventOptions {
|
|
13
|
+
/** Per-request 审批超时覆盖(Codex autoResolutionMs)。 */
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 构造标准 question approval_requested 事件。
|
|
18
|
+
* availableDecisions 恒为空数组:提问只能作答或跳过(decline/cancel 是
|
|
19
|
+
* coordinator 的通用安全兜底),不允许 accept/decline 按钮混入提问卡。
|
|
20
|
+
*/
|
|
21
|
+
export declare function makeQuestionApprovalEvent(requestId: number | string, questions: UserQuestion[], threadId: string, opts?: QuestionEventOptions): ApprovalRequestedEvent;
|
|
22
|
+
/**
|
|
23
|
+
* 按问题顺序把 coordinator 回传的 {问题文本: 值} 展开成索引对齐数组。
|
|
24
|
+
* 未作答的问题对应 undefined,由各 runner 决定协议编码(跳过/空 answers)。
|
|
25
|
+
*/
|
|
26
|
+
export declare function mapAnswersByIndex(questions: UserQuestion[], answers: Record<string, string | string[]>): Array<string | string[] | undefined>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AskUserQuestion 公共工厂与映射(agent 无关)。
|
|
3
|
+
*
|
|
4
|
+
* 接入约定(types.ts UserQuestion 注释的落点):
|
|
5
|
+
* - 各 runner 用 makeQuestionApprovalEvent 构造标准 question 审批事件,
|
|
6
|
+
* 避免四处手拼事件形状(claude/codex/kimi/pi 共用);
|
|
7
|
+
* - 用户答案经 ApprovalCoordinator 以 {问题文本: 值} 回传 runner 后,
|
|
8
|
+
* 用 mapAnswersByIndex 按问题顺序展开,再做协议级编码
|
|
9
|
+
* (Codex 问题 id、Kimi q{i}、Pi 单题取值)。
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* 构造标准 question approval_requested 事件。
|
|
13
|
+
* availableDecisions 恒为空数组:提问只能作答或跳过(decline/cancel 是
|
|
14
|
+
* coordinator 的通用安全兜底),不允许 accept/decline 按钮混入提问卡。
|
|
15
|
+
*/
|
|
16
|
+
export function makeQuestionApprovalEvent(requestId, questions, threadId, opts) {
|
|
17
|
+
const view = {
|
|
18
|
+
requestId,
|
|
19
|
+
kind: 'question',
|
|
20
|
+
questions,
|
|
21
|
+
availableDecisions: [],
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
type: 'approval_requested',
|
|
25
|
+
requestId,
|
|
26
|
+
kind: 'question',
|
|
27
|
+
threadId,
|
|
28
|
+
turnId: '',
|
|
29
|
+
itemId: '',
|
|
30
|
+
view,
|
|
31
|
+
...(opts?.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
|
|
32
|
+
timestamp: new Date().toISOString(),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 按问题顺序把 coordinator 回传的 {问题文本: 值} 展开成索引对齐数组。
|
|
37
|
+
* 未作答的问题对应 undefined,由各 runner 决定协议编码(跳过/空 answers)。
|
|
38
|
+
*/
|
|
39
|
+
export function mapAnswersByIndex(questions, answers) {
|
|
40
|
+
return questions.map((q) => answers[q.question]);
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=question-common.js.map
|
package/dist/runner/types.d.ts
CHANGED
|
@@ -25,11 +25,18 @@ export interface ApprovalRequestedEvent {
|
|
|
25
25
|
type: 'approval_requested';
|
|
26
26
|
/** JSON-RPC id of the server request. Wire type is string | integer — keep raw. */
|
|
27
27
|
requestId: number | string;
|
|
28
|
-
kind: 'command' | 'file' | 'permissions' | 'question';
|
|
28
|
+
kind: 'command' | 'file' | 'permissions' | 'question' | 'tool';
|
|
29
29
|
threadId: string;
|
|
30
30
|
turnId: string;
|
|
31
31
|
itemId: string;
|
|
32
32
|
view: ApprovalView;
|
|
33
|
+
/**
|
|
34
|
+
* Per-request 审批超时覆盖(毫秒)。由 runner 在翻译时填入
|
|
35
|
+
* (Codex autoResolutionMs),coordinator 优先使用;
|
|
36
|
+
* 缺省回落 run 级默认超时。放在事件上而非桥按 agentKind 分支,
|
|
37
|
+
* 保证公共层无 agent 分支。
|
|
38
|
+
*/
|
|
39
|
+
timeoutMs?: number;
|
|
33
40
|
timestamp?: string;
|
|
34
41
|
}
|
|
35
42
|
export interface ApprovalResolvedEvent {
|
|
@@ -67,22 +74,52 @@ export type ApprovalAction = {
|
|
|
67
74
|
} | {
|
|
68
75
|
action: 'cancel';
|
|
69
76
|
}
|
|
70
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* AskUserQuestion 答案(question → label 或 label[])。
|
|
79
|
+
* notes:Codex user_note 补充说明(question → 文本),runner 编码为
|
|
80
|
+
* "user_note: <text>" 条目;其他 agent 忽略该字段。
|
|
81
|
+
*/
|
|
71
82
|
| {
|
|
72
83
|
action: 'answer';
|
|
73
84
|
answers: Record<string, string | string[]>;
|
|
85
|
+
notes?: Record<string, string>;
|
|
74
86
|
};
|
|
75
|
-
/**
|
|
76
|
-
export interface
|
|
87
|
+
/** AskUserQuestion 公共契约(Agent 无关)的单个选项。 */
|
|
88
|
+
export interface UserQuestionOption {
|
|
77
89
|
label: string;
|
|
78
90
|
description?: string;
|
|
79
91
|
}
|
|
80
|
-
/**
|
|
81
|
-
|
|
92
|
+
/**
|
|
93
|
+
* AskUserQuestion 公共契约(Agent 无关)的单个问题。
|
|
94
|
+
*
|
|
95
|
+
* 接入约定(新 agent 接入 AskUserQuestion 时在这里扩展,禁止在 coordinator /
|
|
96
|
+
* renderer / router 里写 agent 分支):
|
|
97
|
+
* 1. 各 agent runner 把协议提问翻译为 `ApprovalView{ kind:'question', questions }`;
|
|
98
|
+
* 2. 用户回答经 ApprovalCoordinator 收集,以 `ApprovalAction{action:'answer',
|
|
99
|
+
* answers:{问题文本: string|string[]}}` 回传 runner;
|
|
100
|
+
* 3. runner 侧把文本 key 答案翻译回自己协议的 shape(Claude 直接用文本 key;
|
|
101
|
+
* Codex 需按问题 id 映射)。
|
|
102
|
+
*/
|
|
103
|
+
export interface UserQuestion {
|
|
104
|
+
/** Agent 侧问题唯一标识(Codex request_user_input 的 id 预留;Claude 无此字段)。 */
|
|
105
|
+
id?: string;
|
|
82
106
|
question: string;
|
|
83
107
|
header?: string;
|
|
84
108
|
multiSelect?: boolean;
|
|
85
|
-
options:
|
|
109
|
+
options: UserQuestionOption[];
|
|
110
|
+
/** 是否允许自定义答案(Codex isOther 预留;当前单选渲染层恒提供 Other 输入)。 */
|
|
111
|
+
isOther?: boolean;
|
|
112
|
+
/** 敏感输入(Codex isSecret 预留)。 */
|
|
113
|
+
isSecret?: boolean;
|
|
114
|
+
/** 自由文本题的输入框占位提示(渲染层自由文本题 input)。 */
|
|
115
|
+
placeholder?: string;
|
|
116
|
+
/**
|
|
117
|
+
* 是否渲染「补充说明(可选)」输入(Codex user_note;数据驱动无 agent 分支)。
|
|
118
|
+
* note 是选项之外的附加文本,随答案提交,不单独构成答案。
|
|
119
|
+
*/
|
|
120
|
+
allowNote?: boolean;
|
|
121
|
+
/** 当前已填写的补充说明(卡片回显;coordinator answerNote 回流)。 */
|
|
122
|
+
note?: string;
|
|
86
123
|
/**
|
|
87
124
|
* 当前已选选项 label(卡片渲染与多选切换用;单选在协调器内即时提交)。
|
|
88
125
|
* 与 permissions.items.selected 同模式:协调器原地更新后以
|
|
@@ -92,10 +129,24 @@ export interface ClaudeUserQuestion {
|
|
|
92
129
|
}
|
|
93
130
|
export interface ApprovalView {
|
|
94
131
|
requestId: number | string;
|
|
95
|
-
kind: 'command' | 'file' | 'permissions' | 'question';
|
|
132
|
+
kind: 'command' | 'file' | 'permissions' | 'question' | 'tool';
|
|
96
133
|
reason?: string;
|
|
134
|
+
/**
|
|
135
|
+
* 提问卡顶部概要行(数据驱动,无 agent 分支):Kimi elicitation form 的
|
|
136
|
+
* 题面只在 form 级 message 里合并出现,逐字段 title 可能只是短 header,
|
|
137
|
+
* 概要行用于展示完整题干。
|
|
138
|
+
*/
|
|
139
|
+
intro?: string;
|
|
97
140
|
command?: string;
|
|
98
141
|
commandCwd?: string;
|
|
142
|
+
/**
|
|
143
|
+
* 被审批的工具名(kind === 'tool' 时展示,如 ExitPlanMode)。非命令工具的
|
|
144
|
+
* 权限请求(plan 退出/通知型工具)input 无 command 语义,用工具名 + reason
|
|
145
|
+
* 表达审批内容,避免落入 command 槽位显示无意义 `{}`。
|
|
146
|
+
*/
|
|
147
|
+
toolName?: string;
|
|
148
|
+
/** 原始 input 的 JSON 字符串(kind === 'tool' 且 input 非空时展示)。 */
|
|
149
|
+
toolInput?: string;
|
|
99
150
|
fileChanges?: Array<{
|
|
100
151
|
path: string;
|
|
101
152
|
kind: 'add' | 'update' | 'delete';
|
|
@@ -115,7 +166,7 @@ export interface ApprovalView {
|
|
|
115
166
|
}>;
|
|
116
167
|
};
|
|
117
168
|
/** Claude AskUserQuestion 问题列表(kind === 'question' 时存在)。 */
|
|
118
|
-
questions?:
|
|
169
|
+
questions?: UserQuestion[];
|
|
119
170
|
availableDecisions: string[];
|
|
120
171
|
/** Raw payloads for structured decisions (e.g. acceptWithExecpolicyAmendment). */
|
|
121
172
|
decisionPayloads?: Record<string, unknown>;
|
|
@@ -283,12 +334,14 @@ export interface Runner {
|
|
|
283
334
|
*/
|
|
284
335
|
unregisterExitHandlers?(): void;
|
|
285
336
|
}
|
|
286
|
-
export type AgentKind = 'claude' | 'codex' | 'opencode' | 'pi' | 'kimi';
|
|
337
|
+
export type AgentKind = 'claude' | 'codex' | 'opencode' | 'pi' | 'kimi' | 'dsh';
|
|
287
338
|
/** Unified session descriptor returned by every AgentSessionReader. */
|
|
288
339
|
export interface AgentSession {
|
|
289
340
|
sessionId: string;
|
|
290
341
|
summary: string;
|
|
291
342
|
mtime: number;
|
|
343
|
+
/** DSH:session 创建时固定的 preset;其他 agent 无此概念(undefined)。 */
|
|
344
|
+
agentPreset?: string;
|
|
292
345
|
}
|
|
293
346
|
/** A single renderable event from session history. */
|
|
294
347
|
export interface AgentSessionContentEvent {
|
|
@@ -154,7 +154,7 @@ function isSessionActive(filePath, mtimeMs, now = Date.now()) {
|
|
|
154
154
|
// Explicit terminal markers — regardless of mtime. Claude CLI 2.x uses
|
|
155
155
|
// `last-prompt` / `mode` (no `result` event) to mark turn end, so a
|
|
156
156
|
// 24-minute-old session with a last-prompt tail is already done
|
|
157
|
-
// (regression 2026-06-21:
|
|
157
|
+
// (regression 2026-06-21: previously falsely reported as active).
|
|
158
158
|
const TERMINAL_TYPES = new Set(['result', 'last-prompt', 'mode', 'permission-mode']);
|
|
159
159
|
if (typeof obj.type === 'string' && TERMINAL_TYPES.has(obj.type))
|
|
160
160
|
return false;
|
|
@@ -224,7 +224,16 @@ export function isClaudeSessionActive(sessionId, cwd, opts = {}) {
|
|
|
224
224
|
*/
|
|
225
225
|
function scalarScan(filePath) {
|
|
226
226
|
const acc = new UsageAccumulator();
|
|
227
|
-
|
|
227
|
+
// Per-message-id best usage record (per-field max). A streaming assistant
|
|
228
|
+
// message spans multiple jsonl lines sharing the same message id; some
|
|
229
|
+
// backends (third-party gateways, e.g. DeepSeek) write all-zero usage on
|
|
230
|
+
// the early placeholder lines (empty thinking block) and the real usage
|
|
231
|
+
// only on the final line carrying stop_reason. First-occurrence-wins dedup
|
|
232
|
+
// would pin the zero record and discard the real one (regression
|
|
233
|
+
// 2026-08-19: auto-resume card per-run stats all 0). Per-field max
|
|
234
|
+
// reconstructs the full usage regardless of line order — on the official
|
|
235
|
+
// API every line repeats identical usage, so max is a no-op there.
|
|
236
|
+
const usageByMessageId = new Map();
|
|
228
237
|
let lastPostTokens = 0;
|
|
229
238
|
let tailOffset = -1;
|
|
230
239
|
let lastAiTitle;
|
|
@@ -244,13 +253,13 @@ function scalarScan(filePath) {
|
|
|
244
253
|
const usage = msg?.usage;
|
|
245
254
|
if (usage) {
|
|
246
255
|
const msgId = msg?.id;
|
|
247
|
-
if (msgId
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
input: usage.input_tokens ?? 0,
|
|
251
|
-
output: usage.output_tokens ?? 0,
|
|
252
|
-
cacheRead: usage.cache_read_input_tokens ?? 0,
|
|
253
|
-
cacheCreation: usage.cache_creation_input_tokens ?? 0,
|
|
256
|
+
if (msgId) {
|
|
257
|
+
const prev = usageByMessageId.get(msgId);
|
|
258
|
+
usageByMessageId.set(msgId, {
|
|
259
|
+
input: Math.max(prev?.input ?? 0, usage.input_tokens ?? 0),
|
|
260
|
+
output: Math.max(prev?.output ?? 0, usage.output_tokens ?? 0),
|
|
261
|
+
cacheRead: Math.max(prev?.cacheRead ?? 0, usage.cache_read_input_tokens ?? 0),
|
|
262
|
+
cacheCreation: Math.max(prev?.cacheCreation ?? 0, usage.cache_creation_input_tokens ?? 0),
|
|
254
263
|
});
|
|
255
264
|
}
|
|
256
265
|
}
|
|
@@ -305,6 +314,12 @@ function scalarScan(filePath) {
|
|
|
305
314
|
}
|
|
306
315
|
}
|
|
307
316
|
});
|
|
317
|
+
// Feed one best record per message id, in first-appearance order (Map
|
|
318
|
+
// preserves insertion order), so acc.last is the LAST message's full usage
|
|
319
|
+
// and totals sum each API response exactly once.
|
|
320
|
+
for (const record of usageByMessageId.values()) {
|
|
321
|
+
acc.add(record);
|
|
322
|
+
}
|
|
308
323
|
// Build usage result (same logic as old aggregateSessionUsage)
|
|
309
324
|
const t = acc.totals;
|
|
310
325
|
const l = acc.last;
|
|
@@ -319,14 +334,19 @@ function scalarScan(filePath) {
|
|
|
319
334
|
if (contextLength === 0 || lastWindow > contextLength) {
|
|
320
335
|
contextLength = lastWindow;
|
|
321
336
|
}
|
|
337
|
+
// 非累计字段 = 末轮(本 run)scope,累计字段 = session 总和(所有 run)。
|
|
338
|
+
// 对齐 codex reader 语义(§9.21:`last_token_usage`=单 turn 增量、
|
|
339
|
+
// `total_token_usage`=session 累计)——否则 jsonl 兜底路径会把 session 累计
|
|
340
|
+
// 当成"本 run"显示,与卡片"本 run ≤ 累计"不变量冲突(累计反而更小)。
|
|
341
|
+
const lastTotal = l ? l.input + l.output + l.cacheRead + l.cacheCreation : 0;
|
|
322
342
|
const usage = {
|
|
323
|
-
inputTokens:
|
|
324
|
-
outputTokens:
|
|
343
|
+
inputTokens: l ? l.input : 0,
|
|
344
|
+
outputTokens: l ? l.output : 0,
|
|
325
345
|
contextLength,
|
|
326
346
|
compactCount: acc.compactCount,
|
|
327
|
-
cacheReadTokens:
|
|
328
|
-
cacheCreationTokens:
|
|
329
|
-
totalTokens:
|
|
347
|
+
cacheReadTokens: l && l.cacheRead > 0 ? l.cacheRead : undefined,
|
|
348
|
+
cacheCreationTokens: l && l.cacheCreation > 0 ? l.cacheCreation : undefined,
|
|
349
|
+
totalTokens: lastTotal,
|
|
330
350
|
cumulativeTotalTokens: t.input + t.output + t.cacheRead + t.cacheCreation,
|
|
331
351
|
cumulativeInputTokens: t.input,
|
|
332
352
|
cumulativeOutputTokens: t.output,
|
|
@@ -435,7 +455,9 @@ export function readSessionContent(sessionId, cwd, opts = {}) {
|
|
|
435
455
|
// ~3×). Total parse count adds the index-miss fallback guard's fixed
|
|
436
456
|
// early-stop overhead (~2 parses, independent of line count).
|
|
437
457
|
const scan = scalarScan(filePath);
|
|
438
|
-
|
|
458
|
+
// 有 usage 即返回:非累计字段已是末轮 scope,命中判定用 session 累计兜底
|
|
459
|
+
// (末轮可能 input/output 为 0 而仍有 cacheRead,累计口径不会被末轮遮挡)。
|
|
460
|
+
const usage = (scan.usage.cumulativeTotalTokens ?? 0) > 0 ? scan.usage : undefined;
|
|
439
461
|
// Extract events AFTER the last user message by re-reading + re-parsing
|
|
440
462
|
// only the tail. If no user message was found (tailOffset === -1), fall
|
|
441
463
|
// back to re-reading the whole file so the card still has something to
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DshSessionReader } from './sessions.js';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DshSessionReader — AgentSessionReader backed by the DSH Web Host over HTTP.
|
|
3
|
+
*
|
|
4
|
+
* The AgentSessionReader contract is fully synchronous (file-style readers),
|
|
5
|
+
* but DSH exposes only async HTTP. The reader therefore bridges with
|
|
6
|
+
* `spawnSync('curl', ...)` for its unary calls (session.list / session.history)
|
|
7
|
+
* — a synchronous HTTP round-trip to the local DSH host. This keeps the
|
|
8
|
+
* router/bridge interface contract intact without touching any upstream logic.
|
|
9
|
+
*
|
|
10
|
+
* listSessions: session.list → filter by cwd → sort by updatedAt desc → local
|
|
11
|
+
* pagination (DSH v1 has no cursor pagination).
|
|
12
|
+
* readSessionContent: session.history → map SessionEvents to display events +
|
|
13
|
+
* session-wide usage accumulation (assistant/message.usage).
|
|
14
|
+
*/
|
|
15
|
+
import type { AgentSession, AgentSessionReader, SessionContent } from '../../runner/index.js';
|
|
16
|
+
interface DshSessionReaderOptions {
|
|
17
|
+
host?: string;
|
|
18
|
+
/** Injectable synchronous unary transport. Defaults to spawnSync curl. */
|
|
19
|
+
syncRequest?: (baseUrl: string, method: string, payload: unknown) => unknown;
|
|
20
|
+
}
|
|
21
|
+
export declare class DshSessionReader implements AgentSessionReader {
|
|
22
|
+
private readonly baseUrl;
|
|
23
|
+
private readonly syncRequest;
|
|
24
|
+
constructor(opts?: DshSessionReaderOptions);
|
|
25
|
+
private unary;
|
|
26
|
+
listSessions(cwd: string, opts?: {
|
|
27
|
+
limit?: number;
|
|
28
|
+
offset?: number;
|
|
29
|
+
}): {
|
|
30
|
+
sessions: AgentSession[];
|
|
31
|
+
total: number;
|
|
32
|
+
};
|
|
33
|
+
getNewestSession(cwd: string): AgentSession | null;
|
|
34
|
+
readSessionContent(sessionId: string, cwd: string, opts?: {
|
|
35
|
+
maxEvents?: number;
|
|
36
|
+
}): SessionContent;
|
|
37
|
+
isSessionActive(sessionId: string, cwd: string): boolean;
|
|
38
|
+
}
|
|
39
|
+
export {};
|