dsh-lark-agent 0.2.0 → 0.3.0
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/README.md +6 -6
- package/dist/bot-menu.d.ts +2 -1
- package/dist/bot-menu.js +3 -2
- package/dist/bridge.d.ts +30 -1
- package/dist/bridge.js +269 -49
- package/dist/dsh-runtime.d.ts +20 -2
- package/dist/dsh-runtime.js +113 -11
- package/dist/interaction-cards.d.ts +8 -3
- package/dist/interaction-cards.js +4 -4
- package/dist/live-reply.d.ts +23 -9
- package/dist/live-reply.js +183 -54
- package/dist/model-controls.d.ts +9 -1
- package/dist/model-controls.js +45 -20
- package/dist/permission-controls.d.ts +8 -1
- package/dist/permission-controls.js +18 -7
- package/dist/replies.d.ts +10 -0
- package/dist/replies.js +18 -5
- package/package.json +7 -7
package/dist/dsh-runtime.js
CHANGED
|
@@ -27,6 +27,7 @@ export class DshRuntimeError extends Error {
|
|
|
27
27
|
DISPOSED: '当前会话已关闭,请重新开始。',
|
|
28
28
|
MODEL_UNAVAILABLE: '当前宿主暂时无法提供模型选项,请检查模型配置。',
|
|
29
29
|
INVALID_MODEL_SELECTION: '这个模型或思考深度当前不可用,请刷新选项后重试。',
|
|
30
|
+
MODEL_NOT_SAVED: '模型设置没能保存到电脑上的 Harness,请稍后重试。',
|
|
30
31
|
PERMISSION_UNAVAILABLE: '权限模式暂时无法在飞书切换,请在电脑上的 Harness 里调整。',
|
|
31
32
|
};
|
|
32
33
|
super(messages[code]);
|
|
@@ -61,6 +62,10 @@ function clip(value, limit) {
|
|
|
61
62
|
const points = Array.from(value.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, '').trim());
|
|
62
63
|
return points.length > limit ? points.slice(0, limit).join('') + '…' : points.join('');
|
|
63
64
|
}
|
|
65
|
+
// The App's order: a call's own title or description first, then what it acts on.
|
|
66
|
+
const DETAIL_KEYS = ['title', 'description', 'objective', 'task', 'task_name', 'name', 'question', 'questions', 'prompt', 'message',
|
|
67
|
+
'command', 'cmd', 'queries', 'query', 'pattern', 'url', 'uri', 'file_path', 'path', 'target', 'action', 'status'];
|
|
68
|
+
const CODE_KEYS = new Set(['command', 'cmd', 'pattern', 'file_path', 'path']);
|
|
64
69
|
/** What a call is about, from well-known argument fields only; never the whole argument object. */
|
|
65
70
|
function callDetail(args, limit) {
|
|
66
71
|
let value;
|
|
@@ -72,11 +77,13 @@ function callDetail(args, limit) {
|
|
|
72
77
|
}
|
|
73
78
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
74
79
|
return;
|
|
75
|
-
for (const key of
|
|
80
|
+
for (const key of DETAIL_KEYS) {
|
|
76
81
|
const field = value[key];
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
82
|
+
const items = Array.isArray(field) ? field.map(item => typeof item === 'string' ? item
|
|
83
|
+
: item && typeof item === 'object' && typeof item.question === 'string' ? item.question : undefined) : [field];
|
|
84
|
+
const text = items.filter((item) => typeof item === 'string' && Boolean(item.trim())).join(', ');
|
|
85
|
+
if (text.trim())
|
|
86
|
+
return { text: clip(text.replace(/\s+/g, ' ').trim(), limit), code: CODE_KEYS.has(key) };
|
|
80
87
|
}
|
|
81
88
|
}
|
|
82
89
|
/** An approval is only informed if the owner sees the arguments being decided. */
|
|
@@ -258,6 +265,54 @@ function permissionOptions(service) {
|
|
|
258
265
|
}
|
|
259
266
|
return options;
|
|
260
267
|
}
|
|
268
|
+
/** The App's default for new sessions, when the catalog names one of its own options. */
|
|
269
|
+
function permissionDefault(service, options) {
|
|
270
|
+
let catalog;
|
|
271
|
+
try {
|
|
272
|
+
catalog = service.catalog();
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const value = catalog && typeof catalog === 'object' ? catalog.defaultPreset : undefined;
|
|
278
|
+
return typeof value === 'string' && options.some(option => option.id === value) ? value : undefined;
|
|
279
|
+
}
|
|
280
|
+
function sessionProjections(ctx) {
|
|
281
|
+
let service;
|
|
282
|
+
try {
|
|
283
|
+
service = ctx.get('sessionProjections');
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
return typeof service?.stateOf === 'function' ? service : undefined;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* The model a session's next request uses under the App's own rule: its latest
|
|
292
|
+
* pick, else its last request's model (an effort the adapter filled in stays
|
|
293
|
+
* unset), else the host default. Undefined when the host keeps no such record.
|
|
294
|
+
*/
|
|
295
|
+
function sessionChoice(projections, session, defaults) {
|
|
296
|
+
try {
|
|
297
|
+
const state = projections.stateOf(session, 'modelSelection');
|
|
298
|
+
if (!state || typeof state !== 'object')
|
|
299
|
+
return;
|
|
300
|
+
if (state.pending)
|
|
301
|
+
return nativeSelection(modelSelection(state.pending));
|
|
302
|
+
const header = session
|
|
303
|
+
.requestHeader?.();
|
|
304
|
+
if (header?.config) {
|
|
305
|
+
const { provider, model, reasoningEffort } = header.config;
|
|
306
|
+
return nativeSelection(modelSelection({ provider, model,
|
|
307
|
+
...(reasoningEffort === undefined || header.adapterDefaults?.reasoningEffort === true ? {} : { reasoningEffort: String(reasoningEffort) }) }));
|
|
308
|
+
}
|
|
309
|
+
const fallback = defaults?.currentSelection();
|
|
310
|
+
return fallback ? nativeSelection(modelSelection(fallback)) : undefined;
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
261
316
|
const MODEL_CATALOG_TTL_MS = 30_000;
|
|
262
317
|
const modelId = (value) => typeof value === 'string' && value.length > 0 && value.length <= 256
|
|
263
318
|
&& value.trim() === value && !/[\u0000-\u001f\u007f]/.test(value);
|
|
@@ -537,6 +592,22 @@ export class DshRuntime {
|
|
|
537
592
|
const service = permissionPresets(this.ctx);
|
|
538
593
|
return service ? permissionOptions(service) : [];
|
|
539
594
|
}
|
|
595
|
+
/** Whether harness sessions here share their model choice with the App (see RuntimeSession.sharesModelSelection). */
|
|
596
|
+
sharesModelSelection() {
|
|
597
|
+
return Boolean(sessionProjections(this.ctx)) && typeof this.ctx.get('agentDefaultModel')?.saveSelection === 'function';
|
|
598
|
+
}
|
|
599
|
+
/** Make a choice the App's default for new sessions, as the App's own model selector does. */
|
|
600
|
+
async saveDefaultModel(selection) {
|
|
601
|
+
const service = this.ctx.get('agentDefaultModel');
|
|
602
|
+
if (typeof service?.saveSelection !== 'function')
|
|
603
|
+
return;
|
|
604
|
+
try {
|
|
605
|
+
await service.saveSelection(nativeSelection(modelSelection(selection)));
|
|
606
|
+
}
|
|
607
|
+
catch {
|
|
608
|
+
throw new DshRuntimeError('MODEL_NOT_SAVED');
|
|
609
|
+
}
|
|
610
|
+
}
|
|
540
611
|
/** The preset a stored session runs on: its current projection, else its header. */
|
|
541
612
|
recordedPreset(session) {
|
|
542
613
|
const projections = this.ctx.get('sessionProjections');
|
|
@@ -570,7 +641,17 @@ export class DshRuntime {
|
|
|
570
641
|
let memoryText = '';
|
|
571
642
|
let artifactsText = '';
|
|
572
643
|
let active;
|
|
573
|
-
|
|
644
|
+
// A restricted session, or one on an administrator's fixed route, keeps that route.
|
|
645
|
+
// A harness session on the App host follows its own record once it exists (see sharesModelSelection).
|
|
646
|
+
const projections = harness && !(policy.provider && policy.model) ? sessionProjections(this.ctx) : undefined;
|
|
647
|
+
const defaultModel = projections ? this.ctx.get('agentDefaultModel') : undefined;
|
|
648
|
+
let fixed;
|
|
649
|
+
let shared;
|
|
650
|
+
const selected = {
|
|
651
|
+
get current() { return (shared && projections && sessionChoice(projections, shared, defaultModel)) || fixed; },
|
|
652
|
+
set current(next) { fixed = next; },
|
|
653
|
+
assembled: undefined,
|
|
654
|
+
};
|
|
574
655
|
let handle;
|
|
575
656
|
// Legacy hosts log raw chunks as session events, modern hosts publish live
|
|
576
657
|
// frames. Use whichever arrives first so a host offering both shows text once.
|
|
@@ -600,8 +681,8 @@ export class DshRuntime {
|
|
|
600
681
|
// Kept only so an approval prompt can show the exact call being decided.
|
|
601
682
|
if (turn.calls.size < 256)
|
|
602
683
|
turn.calls.set(block.id, args);
|
|
603
|
-
const detail = callDetail(args,
|
|
604
|
-
emit(turn, { type: 'tool', callId: block.id, name: clip(block.name, 64), ...(detail ? { detail } : {}) });
|
|
684
|
+
const detail = callDetail(args, 160);
|
|
685
|
+
emit(turn, { type: 'tool', callId: block.id, name: clip(block.name, 64), ...(detail ? { detail: detail.text, ...(detail.code ? { code: true } : {}) } : {}) });
|
|
605
686
|
};
|
|
606
687
|
try {
|
|
607
688
|
if (!isAbsolute(policy.cwd))
|
|
@@ -779,8 +860,12 @@ export class DshRuntime {
|
|
|
779
860
|
}
|
|
780
861
|
});
|
|
781
862
|
agentCtx.on('tools/result', (execution, result) => {
|
|
782
|
-
if (active
|
|
783
|
-
|
|
863
|
+
if (!active || typeof execution.callId !== 'string')
|
|
864
|
+
return;
|
|
865
|
+
// A command that ran but exited non-zero still succeeded as a tool call; show its code like the App.
|
|
866
|
+
const exitCode = result.value?.exitCode;
|
|
867
|
+
emit(active, { type: 'tool-done', callId: execution.callId, ok: !result.isError,
|
|
868
|
+
...(typeof exitCode === 'number' && Number.isInteger(exitCode) && exitCode !== 0 ? { exitCode } : {}) });
|
|
784
869
|
});
|
|
785
870
|
}
|
|
786
871
|
else {
|
|
@@ -854,6 +939,8 @@ export class DshRuntime {
|
|
|
854
939
|
throw new DshRuntimeError(resumeId ? 'RESTORE_FAILED' : 'SETUP_FAILED');
|
|
855
940
|
}
|
|
856
941
|
const agent = handle.agent;
|
|
942
|
+
if (projections && sessionChoice(projections, agent.session, defaultModel))
|
|
943
|
+
shared = agent.session;
|
|
857
944
|
// Keep this capability while the owner is active. Cordis unloads sibling
|
|
858
945
|
// effects concurrently, so native agent teardown may detach this session
|
|
859
946
|
// before the bridge's teardown reaches it.
|
|
@@ -992,8 +1079,22 @@ export class DshRuntime {
|
|
|
992
1079
|
throw new DshRuntimeError('DISPOSED');
|
|
993
1080
|
if (active || agent.status === 'running')
|
|
994
1081
|
throw new DshRuntimeError('BUSY');
|
|
995
|
-
|
|
1082
|
+
const choice = nativeSelection(modelSelection(next));
|
|
1083
|
+
if (!shared) {
|
|
1084
|
+
selected.current = choice;
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
// Recorded like the App selector's own pick, so the App shows it as this session's model.
|
|
1088
|
+
// Older SDK typings predate this event; only a host with the record gets here.
|
|
1089
|
+
try {
|
|
1090
|
+
agent.session.append('model/selection', { provider: choice.provider, model: choice.model,
|
|
1091
|
+
...(choice.reasoningEffort === undefined ? {} : { reasoningEffort: String(choice.reasoningEffort) }) });
|
|
1092
|
+
}
|
|
1093
|
+
catch {
|
|
1094
|
+
throw new DshRuntimeError('INVALID_MODEL_SELECTION');
|
|
1095
|
+
}
|
|
996
1096
|
},
|
|
1097
|
+
sharesModelSelection: Boolean(shared),
|
|
997
1098
|
permissionState: () => {
|
|
998
1099
|
const service = harness && !disposed ? permissionPresets(this.ctx) : undefined;
|
|
999
1100
|
const options = service ? permissionOptions(service) : [];
|
|
@@ -1006,7 +1107,8 @@ export class DshRuntime {
|
|
|
1006
1107
|
catch {
|
|
1007
1108
|
return;
|
|
1008
1109
|
}
|
|
1009
|
-
|
|
1110
|
+
const defaultPreset = permissionDefault(service, options);
|
|
1111
|
+
return { current: typeof current === 'string' ? current : 'custom', options, ...(defaultPreset ? { defaultPreset } : {}) };
|
|
1010
1112
|
},
|
|
1011
1113
|
setPermission: (id) => {
|
|
1012
1114
|
if (disposed)
|
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
import type { ApprovalPrompt, QuestionAnswer, UserQuestion } from './dsh-runtime.js';
|
|
2
|
-
import { type ReplyPayload } from './replies.js';
|
|
2
|
+
import { type PanelTone, type ReplyPayload } from './replies.js';
|
|
3
3
|
/** A verified card click on a pending approval or question. */
|
|
4
4
|
export interface InteractionAnswer {
|
|
5
5
|
verb: 'approve' | 'deny' | 'choose' | 'reply';
|
|
6
6
|
index?: number;
|
|
7
7
|
formValues?: unknown;
|
|
8
8
|
}
|
|
9
|
+
/** How a card closed: allowed or answered (success), refused (error), or expired and stopped (muted). */
|
|
10
|
+
export interface InteractionOutcome {
|
|
11
|
+
text: string;
|
|
12
|
+
tone: PanelTone;
|
|
13
|
+
}
|
|
9
14
|
/** Buttons answer one short single-choice question in one tap; anything else uses a form. */
|
|
10
15
|
export declare function simpleQuestion(questions: readonly UserQuestion[]): boolean;
|
|
11
16
|
/** The decision card. Without tokens it shows the settled outcome and no buttons. */
|
|
12
17
|
export declare function approvalCard(prompt: ApprovalPrompt, tokens?: {
|
|
13
18
|
allow: string;
|
|
14
19
|
deny: string;
|
|
15
|
-
}, outcome?:
|
|
20
|
+
}, outcome?: InteractionOutcome): ReplyPayload;
|
|
16
21
|
/** A question card: buttons for one short choice, otherwise a form. Without tokens it shows the outcome. */
|
|
17
22
|
export declare function questionCard(questions: readonly UserQuestion[], tokens?: {
|
|
18
23
|
choose?: string[];
|
|
19
24
|
reply?: string;
|
|
20
|
-
}, outcome?:
|
|
25
|
+
}, outcome?: InteractionOutcome): ReplyPayload;
|
|
21
26
|
/** Map a click or form back to the host's answer shape, echoing the original option labels. */
|
|
22
27
|
export declare function questionAnswers(questions: readonly UserQuestion[], answer: InteractionAnswer): QuestionAnswer[] | undefined;
|
|
23
28
|
export declare function answeredText(questions: readonly UserQuestion[], answers: readonly QuestionAnswer[]): string;
|
|
@@ -14,12 +14,12 @@ export function simpleQuestion(questions) {
|
|
|
14
14
|
export function approvalCard(prompt, tokens, outcome) {
|
|
15
15
|
return renderPanel({
|
|
16
16
|
title: tokens ? '需要你确认' : '确认结果',
|
|
17
|
-
summary: `是否允许使用工具 ${prompt.toolName}`,
|
|
17
|
+
summary: outcome?.text ?? `是否允许使用工具 ${prompt.toolName}`,
|
|
18
|
+
...(outcome ? { tone: outcome.tone, status: outcome } : {}),
|
|
18
19
|
sections: [
|
|
19
20
|
{ title: '工具', text: code(prompt.toolName) },
|
|
20
21
|
...(prompt.reason ? [{ title: '原因', text: prompt.reason }] : []),
|
|
21
22
|
...(prompt.detail ? [{ title: '参数', text: fenced(prompt.detail) }] : []),
|
|
22
|
-
...(outcome ? [{ text: `**${outcome}**` }] : []),
|
|
23
23
|
],
|
|
24
24
|
...(tokens ? { actions: [{ label: '允许一次', value: { token: tokens.allow }, primary: true }, { label: '拒绝', value: { token: tokens.deny } }],
|
|
25
25
|
actionColumns: 2, note: '只允许这一次,不改变电脑上的权限设置。5 分钟内没有确认会按拒绝处理。' } : {}),
|
|
@@ -36,11 +36,11 @@ export function questionCard(questions, tokens, outcome) {
|
|
|
36
36
|
const title = questions.length === 1 && first.header ? first.header : '需要你选择';
|
|
37
37
|
const form = Boolean(tokens?.reply);
|
|
38
38
|
const base = renderPanel({
|
|
39
|
-
title, summary: `${title}:${first.question}`,
|
|
39
|
+
title, summary: outcome?.text ?? `${title}:${first.question}`,
|
|
40
|
+
...(outcome ? { tone: outcome.tone, status: outcome } : {}),
|
|
40
41
|
sections: [
|
|
41
42
|
...(form ? [{ text: questions.length > 1 ? `请回答下面 ${questions.length} 个问题,然后提交。` : '请选择或填写后提交。' }]
|
|
42
43
|
: questions.map(question => ({ text: questionText(question) }))),
|
|
43
|
-
...(outcome ? [{ text: `**${outcome}**` }] : []),
|
|
44
44
|
],
|
|
45
45
|
...(tokens?.choose ? { actions: [...first.options.map((option, index) => ({ label: option.label, value: { token: tokens.choose[index] }, primary: index === 0 })),
|
|
46
46
|
{ label: '跳过', value: { token: tokens.choose[first.options.length] } }], actionColumns: 1 } : {}),
|
package/dist/live-reply.d.ts
CHANGED
|
@@ -22,10 +22,12 @@ export interface LiveReplyFinal {
|
|
|
22
22
|
note?: string;
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
|
-
* One streaming reply card: the answer, with
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
25
|
+
* One streaming reply card: the answer, with the process in a panel that
|
|
26
|
+
* stays folded unless the reader opens it. Like the App, the process is a
|
|
27
|
+
* list of steps, thinking and tool calls, each on its own row. Only the
|
|
28
|
+
* initial reply is a send; every later operation updates the same card
|
|
29
|
+
* entity and is safe to supersede or repeat. A reply never continues in
|
|
30
|
+
* another message.
|
|
29
31
|
*/
|
|
30
32
|
export declare class LiveReply {
|
|
31
33
|
private readonly transport;
|
|
@@ -33,15 +35,18 @@ export declare class LiveReply {
|
|
|
33
35
|
private readonly options;
|
|
34
36
|
private phase;
|
|
35
37
|
private readonly steps;
|
|
38
|
+
private nextStep;
|
|
36
39
|
private trimmed;
|
|
37
40
|
private answer;
|
|
38
41
|
private separator;
|
|
39
42
|
private mark;
|
|
40
43
|
private tools;
|
|
41
44
|
private note?;
|
|
45
|
+
private footer?;
|
|
42
46
|
private cardId?;
|
|
43
47
|
private sequence;
|
|
44
48
|
private readonly sent;
|
|
49
|
+
private shown;
|
|
45
50
|
private dirty;
|
|
46
51
|
private layoutDirty;
|
|
47
52
|
private degraded;
|
|
@@ -60,6 +65,8 @@ export declare class LiveReply {
|
|
|
60
65
|
}, options?: LiveReplyOptions);
|
|
61
66
|
/** Send the card. False means nothing was sent, so the caller keeps its ordinary reply. */
|
|
62
67
|
open(): Promise<boolean>;
|
|
68
|
+
/** Trusted settings line under the answer, such as the model and thinking depth in use. */
|
|
69
|
+
setFooter(text: string): void;
|
|
63
70
|
push(event: TurnStreamEvent): void;
|
|
64
71
|
/** Stop streaming and write the final card. True only when Feishu confirmed it. */
|
|
65
72
|
finish(final: LiveReplyFinal): Promise<boolean>;
|
|
@@ -73,15 +80,22 @@ export declare class LiveReply {
|
|
|
73
80
|
/** Pacing between updates; finishing the reply ends it early. */
|
|
74
81
|
private pause;
|
|
75
82
|
private flush;
|
|
83
|
+
/** Streamable text elements of a card: its process rows and the answer. */
|
|
84
|
+
private textElements;
|
|
76
85
|
private markSent;
|
|
77
|
-
|
|
78
|
-
private
|
|
86
|
+
/** One step as a row, in the App's words: what was done and on what. */
|
|
87
|
+
private row;
|
|
79
88
|
private answerContent;
|
|
80
89
|
private panelTitle;
|
|
81
|
-
/** The folded process panel. Readers open it when they want the
|
|
90
|
+
/** The folded process panel. Readers open it when they want the thinking and tool steps. */
|
|
82
91
|
private panel;
|
|
83
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* The rows that fit in maxBytes, newest last. Older steps give way first and
|
|
94
|
+
* leave a note; a single oversized thought keeps its most recent part.
|
|
95
|
+
*/
|
|
96
|
+
private rows;
|
|
97
|
+
/** Live layout when final is undefined; a final layout otherwise (null leaves out the panel's rows). */
|
|
84
98
|
private card;
|
|
85
|
-
/** The answer comes first. The folded process
|
|
99
|
+
/** The answer comes first. The folded process gets all the space that is left. */
|
|
86
100
|
private finalCard;
|
|
87
101
|
}
|