dsh-task-ledger 0.0.46 → 0.0.47
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/dist/client.js +107 -46
- package/dist/client.js.map +1 -1
- package/dist/host/repositories/cache.d.ts +27 -0
- package/dist/host/repositories/cache.js +57 -0
- package/dist/host/repositories/ledger.repository.d.ts +2 -2
- package/dist/host/repositories/ledger.repository.js +19 -10
- package/dist/host/routes.d.ts +17 -0
- package/dist/host/routes.js +213 -64
- package/dist/host/services/session.service.d.ts +110 -0
- package/dist/host/services/session.service.js +145 -0
- package/dist/host/skill/registration.d.ts +2 -0
- package/dist/host/skill/registration.js +2 -0
- package/dist/host/workspace.d.ts +6 -0
- package/dist/index.js +30 -1
- package/package.json +1 -1
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话交接(按钮侧方法层):把一条任务交给模型去收尾。
|
|
3
|
+
*
|
|
4
|
+
* 事实([`external/dsh-session-handoff.md`](../../../../docs/design/external/dsh-session-handoff.md)):
|
|
5
|
+
* 浏览器半**没有**「往会话里发自由文本」的官方 API,唯一链路 = host `ctx.agents.create()` 自建会话
|
|
6
|
+
* + `agent.send()` 发预置提示词,客户端再 `openSession` 跳过去。
|
|
7
|
+
*
|
|
8
|
+
* 两条硬约束(都是参考插件真机踩出来的,不是猜的):
|
|
9
|
+
* ① **必须带 preset**:`create` 里不 mount preset 的 agent 落到「空的全局层」——只剩根作用域的东西,
|
|
10
|
+
* fs / bash 全无,模型连 `progress.py` 都跑不起来 ⇒ 这种会话建了等于白建。
|
|
11
|
+
* ② **provider + model 必须成对显式给**:缺省**不是**「走宿主默认路由」。用户没选模型时,
|
|
12
|
+
* 这里按宿主 `agentDefaultModel.currentSelection()` 取部署默认;取不到就报 503,不建半个会话。
|
|
13
|
+
*
|
|
14
|
+
* 本文件只管「结束任务」这一条(2026-10-18 用户拍板:认领 / 释放由 host 直写、不开会话;
|
|
15
|
+
* 结束要走会话,因为要带用户留言、要模型读过程文档后写摘要)。
|
|
16
|
+
*/
|
|
17
|
+
import { randomUUID } from 'node:crypto';
|
|
18
|
+
import { getTask } from '../repositories/ledger.repository.js';
|
|
19
|
+
import { SKILL_SCRIPT } from '../skill/registration.js';
|
|
20
|
+
export class HandoffError extends Error {
|
|
21
|
+
code;
|
|
22
|
+
constructor(code, message) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.code = code;
|
|
25
|
+
this.name = 'HandoffError';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 「结束任务」提示词(2026-10-18 用户口述定稿)。
|
|
30
|
+
*
|
|
31
|
+
* 结构照用户原话:**预置说明在前,用户留言放最末**(越靠后、越只讲一件事的段落遵守率越高 ——
|
|
32
|
+
* 参考插件真机结论)。三道硬约束缺一不可:
|
|
33
|
+
* ① 不要开新会话(用户:我要结束这个流程了,不能让模型再开一堆框);
|
|
34
|
+
* ② 读过程文档后直接收尾(别再回头问,用户已经留言了);
|
|
35
|
+
* ③ 写台账只经 skill 脚本(插件不手改文件,close 由模型执行)。
|
|
36
|
+
*/
|
|
37
|
+
export function buildClosePrompt(input) {
|
|
38
|
+
const worklog = input.worklogPath === null || input.worklogPath === ''
|
|
39
|
+
? '(这条任务没有登记过程文档)'
|
|
40
|
+
: `${input.workspacePath}/ledger/${input.worklogPath}`;
|
|
41
|
+
const message = input.message.trim() === '' ? '(用户没有留具体内容)' : input.message.trim();
|
|
42
|
+
return [
|
|
43
|
+
`用户从任务台账看板点了「结束」,要把任务「${input.title}」(任务号 ${input.taskId})结案。`,
|
|
44
|
+
'',
|
|
45
|
+
`工作区:${input.workspacePath}`,
|
|
46
|
+
`过程文档:${worklog}`,
|
|
47
|
+
'',
|
|
48
|
+
'请按顺序做完这三件,然后停下:',
|
|
49
|
+
'1. 读过程文档的「背景 / 待办 / 下一步」,核对这次到底做了什么、还留了什么坑。',
|
|
50
|
+
`2. 用 task-ledger skill 的脚本结案(在工作区目录下、原样执行这一条命令):`,
|
|
51
|
+
` python3 ${SKILL_SCRIPT} close --id ${input.taskId} --repo ${input.workspacePath} --summary "<一句话说清做成了什么、留了什么坑>"`,
|
|
52
|
+
' `--summary` 是必填参数,不许编套话;命令报错就原样报告错误,不要自己绕过、不要手改台账文件。',
|
|
53
|
+
'3. 收尾即结束——这就是本条消息的全部要求。',
|
|
54
|
+
'',
|
|
55
|
+
'【硬约束】',
|
|
56
|
+
'- 不要在会话里继续派活,**不要新建会话或子 agent**:这是收尾动作,不是新任务的开始。',
|
|
57
|
+
'- 用户已经明确要结束,不会再回答提问:信息不足就按留言与过程文档如实写进摘要/结论,**不要停下来反问**。',
|
|
58
|
+
'- 只动这一个任务,不顺带改别的记录;写台账只经上面那个 skill 脚本。',
|
|
59
|
+
'',
|
|
60
|
+
'用户现在处理结束,以下是用户的留言:',
|
|
61
|
+
message,
|
|
62
|
+
].join('\n');
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 起一个「结束任务」会话并发出首条提示词。
|
|
66
|
+
* 失败抛 {@link HandoffError}(带原因码);成功返回 sessionId 供客户端 `openSession` 跳转。
|
|
67
|
+
*/
|
|
68
|
+
export async function startCloseSession(deps, request) {
|
|
69
|
+
const agents = deps.agents;
|
|
70
|
+
if (agents === undefined) {
|
|
71
|
+
throw new HandoffError('no-agents', '宿主未暴露 agents 服务,起不了会话(按钮交接暂不可用)');
|
|
72
|
+
}
|
|
73
|
+
const presets = deps.presets;
|
|
74
|
+
if (presets === undefined) {
|
|
75
|
+
throw new HandoffError('no-preset', '宿主未暴露 agentPresets 服务:起出来的会话没有 fs / bash 工具,模型跑不了 close');
|
|
76
|
+
}
|
|
77
|
+
const route = resolveModelRoute(deps.defaultModel, request.model);
|
|
78
|
+
// 任务此刻必须还在在办里(刚被别处结案 ⇒ 明确告知,不拿空标题去建会话)。
|
|
79
|
+
const task = await getTask(request.workspace, request.taskId);
|
|
80
|
+
if (task === null) {
|
|
81
|
+
throw new HandoffError('task-missing', `任务 ${request.taskId} 不在「${request.workspace.title}」的在办列表里(可能已被结案)`);
|
|
82
|
+
}
|
|
83
|
+
let presetId;
|
|
84
|
+
try {
|
|
85
|
+
presetId = (await presets.resolve()).id;
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
throw new HandoffError('no-preset', `解析默认 preset 失败:${error instanceof Error ? error.message : String(error)}`);
|
|
89
|
+
}
|
|
90
|
+
const sessionId = randomUUID();
|
|
91
|
+
let handle;
|
|
92
|
+
try {
|
|
93
|
+
handle = await agents.create({
|
|
94
|
+
sessionId,
|
|
95
|
+
// cwd 必须是该工作区真实路径(会话 header 校验 + 工作区归组都按它判)。
|
|
96
|
+
meta: { cwd: request.workspace.path, agentPreset: presetId },
|
|
97
|
+
agentOptions: { provider: route.provider, model: route.model },
|
|
98
|
+
// setup 是发布前唯一能挂工具 / prompt sections / skill 的时机;抛错则整个作用域回滚。
|
|
99
|
+
setup: async (agentCtx) => { await presets.mount(agentCtx, presetId); },
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
throw new HandoffError('create-failed', `会话没建起来(preset=${presetId}):${error instanceof Error ? error.message : String(error)}`);
|
|
104
|
+
}
|
|
105
|
+
const prompt = buildClosePrompt({
|
|
106
|
+
title: task.title ?? request.taskId,
|
|
107
|
+
taskId: request.taskId,
|
|
108
|
+
workspacePath: request.workspace.path,
|
|
109
|
+
worklogPath: task.worklog ?? null,
|
|
110
|
+
message: request.message,
|
|
111
|
+
});
|
|
112
|
+
try {
|
|
113
|
+
handle.agent.send({
|
|
114
|
+
id: randomUUID(),
|
|
115
|
+
role: 'user',
|
|
116
|
+
content: [{ type: 'text', text: prompt }],
|
|
117
|
+
source: { kind: 'task-ledger', form: 'notice', summary: `结束任务 · ${task.title ?? request.taskId}` },
|
|
118
|
+
}, 'next-turn', true);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
throw new HandoffError('send-failed', `会话已建(${sessionId})但首条消息没发出去:${error instanceof Error ? error.message : String(error)}`);
|
|
122
|
+
}
|
|
123
|
+
return { sessionId };
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* 模型路由:用户选了就必须给全一对(只有一半 = 配错,直接人话拒绝);
|
|
127
|
+
* 没选就走宿主部署默认。**没有兜底猜测**——猜一个模型去结案比报错更糟。
|
|
128
|
+
*/
|
|
129
|
+
export function resolveModelRoute(defaultModel, explicit) {
|
|
130
|
+
if (explicit !== undefined) {
|
|
131
|
+
if (explicit.provider === '' || explicit.model === '') {
|
|
132
|
+
throw new HandoffError('no-model', 'provider 与 model 必须成对给出(只给一半会被拒,不做猜测)');
|
|
133
|
+
}
|
|
134
|
+
return explicit;
|
|
135
|
+
}
|
|
136
|
+
if (defaultModel !== undefined) {
|
|
137
|
+
try {
|
|
138
|
+
const route = defaultModel.currentSelection();
|
|
139
|
+
if (route.provider !== '' && route.model !== '')
|
|
140
|
+
return { provider: route.provider, model: route.model };
|
|
141
|
+
}
|
|
142
|
+
catch { /* 读不到就往下走,报同一句人话 */ }
|
|
143
|
+
}
|
|
144
|
+
throw new HandoffError('no-model', '宿主没有可用的默认模型:请在请求里给出 provider + model');
|
|
145
|
+
}
|
|
@@ -9,6 +9,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
9
9
|
const SKILL_DIR = fileURLToPath(new URL('../../../skill', import.meta.url));
|
|
10
10
|
/** SKILL.md 里指示脚本位置的占位符(注册时替换为绝对路径,模型据此找到脚本)。 */
|
|
11
11
|
const SKILL_DIR_TOKEN = '<本 SKILL.md 所在目录>';
|
|
12
|
+
/** skill 脚本绝对路径(提示词里直接给「原样执行这一条」的命令用;与注册共用同一份目录知识)。 */
|
|
13
|
+
export const SKILL_SCRIPT = `${SKILL_DIR}/scripts/progress.py`;
|
|
12
14
|
/** frontmatter 最小解析:只取 name / description(不引 YAML 库;值去首尾引号)。 */
|
|
13
15
|
function parseFrontmatter(text) {
|
|
14
16
|
const match = /^---\n([\s\S]*?)\n---/.exec(text);
|
package/dist/host/workspace.d.ts
CHANGED
|
@@ -4,6 +4,12 @@ export interface RegistryWorkspace {
|
|
|
4
4
|
readonly id: string;
|
|
5
5
|
readonly title: string;
|
|
6
6
|
readonly path: string;
|
|
7
|
+
/**
|
|
8
|
+
* 把会话登记进本工作区的 `sessionIds`(会话归组的唯一途径)。
|
|
9
|
+
* ⚠️ 只设 `meta.cwd` **不会**自动归组(reference: dsh-task-dispatch-table 决策 22 真机结论);
|
|
10
|
+
* 宿主旧版本可能没有该面 ⇒ optional,调用方按缺失降级并如实回报。
|
|
11
|
+
*/
|
|
12
|
+
readonly attachSession?: (sessionId: string) => Promise<void>;
|
|
7
13
|
}
|
|
8
14
|
/**
|
|
9
15
|
* 工作区发现:宿主 registry 全量 → 逐个查 <path>/ledger/progress.json,只返回有的。
|
package/dist/index.js
CHANGED
|
@@ -9,9 +9,22 @@
|
|
|
9
9
|
* 满足不了的依赖会令 entry 一直 pending、卡死整个 dsh 启动);两条注入线
|
|
10
10
|
* 互相独立——skills 缺席不挡数据面,反之亦然。
|
|
11
11
|
*/
|
|
12
|
-
import { registerRoutes } from './host/routes.js';
|
|
12
|
+
import { registerRoutes, registerSessionRoutes } from './host/routes.js';
|
|
13
13
|
import { buildSkillRegistration } from './host/skill/registration.js';
|
|
14
14
|
export const name = 'dsh-task-ledger';
|
|
15
|
+
/**
|
|
16
|
+
* 惰性服务读取:cordis ctx 的属性是 getter,**没 inject 的服务读一下就 throw**
|
|
17
|
+
* (参考插件真机实证:`cannot get property "agentTeams" without inject`)。
|
|
18
|
+
* 探测语义是「有就用、没有就降级」,所以把 throw 归一成 undefined。
|
|
19
|
+
*/
|
|
20
|
+
function readService(services, name) {
|
|
21
|
+
try {
|
|
22
|
+
return services[name];
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
15
28
|
export function apply(ctx) {
|
|
16
29
|
// ── 数据面(任务 3) ──
|
|
17
30
|
ctx.inject(['workspaceRegistry', 'webServer'], (services) => {
|
|
@@ -24,6 +37,22 @@ export function apply(ctx) {
|
|
|
24
37
|
registerRoutes(webServer, { registry });
|
|
25
38
|
console.info('[task-ledger] 数据面路由已注册(/api/task-ledger/*)');
|
|
26
39
|
});
|
|
40
|
+
// ── 会话交接(按钮侧「结束任务」,2026-10-18 拍板 B 案:只有结束走会话) ──
|
|
41
|
+
// 单独一条注入线:agents 缺席只影响这一个端点,不该拖累数据面(上一条线只等 registry + webServer)。
|
|
42
|
+
ctx.inject(['agents', 'workspaceRegistry', 'webServer'], (services) => {
|
|
43
|
+
const registry = services.workspaceRegistry;
|
|
44
|
+
const webServer = services.webServer;
|
|
45
|
+
if (registry === undefined || webServer === undefined) {
|
|
46
|
+
console.warn('[task-ledger] workspaceRegistry / webServer 未就位,「结束任务」端点不注册');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
registerSessionRoutes(webServer, {
|
|
50
|
+
registry,
|
|
51
|
+
agents: services.agents,
|
|
52
|
+
getService: (name) => readService(services, name),
|
|
53
|
+
});
|
|
54
|
+
console.info('[task-ledger] 「结束任务」端点已注册(POST /api/task-ledger/close)');
|
|
55
|
+
});
|
|
27
56
|
// ── skill 全局注册(任务 2) ──
|
|
28
57
|
// unscoped 注册落全局层,模型可 load;disposer 随插件 dispose 自动注销。
|
|
29
58
|
ctx.inject(['skills'], (services) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-task-ledger",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.47",
|
|
4
4
|
"description": "Task ledger board for dsh (DeepSeek Harness): read-only board over task-ledger skill workspaces. The task-ledger skill is bundled — install the plugin, get the skill globally.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek",
|