dsh-taskboard 0.2.0 → 0.2.1
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 +5 -1
- package/lib/client.js +1 -1
- package/lib/host/execution.js +29 -13
- package/lib/host/execution.js.map +1 -1
- package/package.json +1 -1
- package/src/host/execution.ts +39 -19
- package/src/shared/version.ts +1 -1
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ DeepSeek Harness 的**任务看板插件**:人建卡、agent 认领执行、
|
|
|
26
26
|
- 代码级协议闸:agent 永远移不到 done;任务被持有时不可抢占;model/execution 对 agent 只读
|
|
27
27
|
|
|
28
28
|
**执行**
|
|
29
|
-
- 手动执行或 cron
|
|
29
|
+
- 手动执行或 cron 定时:每次执行在任务项目内新建全新会话(干净上下文、可指定模型);开场两条消息同一回合送达——插件上下文行携带任务框架与交接协议(含失败回退路径),卡片内容(提示词或标题+描述)以正常用户消息呈现
|
|
30
30
|
- host 侧调度:关掉浏览器照常触发;错过窗口跳过不补跑
|
|
31
31
|
- 乐观并发(ifVersion)+ 完整归因(谁改的、哪个会话执行的)
|
|
32
32
|
|
|
@@ -49,6 +49,10 @@ node scripts/screenshot.mjs # 重新生成 img/ 截图(需本机 Edge)
|
|
|
49
49
|
|
|
50
50
|
## 升级日志
|
|
51
51
|
|
|
52
|
+
### 0.2.1
|
|
53
|
+
|
|
54
|
+
- 调整任务发起时的提示词
|
|
55
|
+
|
|
52
56
|
### 0.2.0
|
|
53
57
|
|
|
54
58
|
- **看板效率**:顶栏新增搜索(标题/ID,大小写不敏感)、列内排序(默认/最近更新/紧急度/创建时间);筛选与排序按 localStorage 持久化(搜索不持久)
|
package/lib/client.js
CHANGED
|
@@ -1312,7 +1312,7 @@ window.__ModuleLoader__.load({
|
|
|
1312
1312
|
* @module dsh-taskboard/shared/version
|
|
1313
1313
|
*/
|
|
1314
1314
|
/** The package version (must equal package.json "version"). */
|
|
1315
|
-
const PLUGIN_VERSION = "0.2.
|
|
1315
|
+
const PLUGIN_VERSION = "0.2.1";
|
|
1316
1316
|
|
|
1317
1317
|
//#endregion
|
|
1318
1318
|
//#region src/client/board/TaskCard.tsx
|
package/lib/host/execution.js
CHANGED
|
@@ -149,16 +149,27 @@ var ExecutionService = class {
|
|
|
149
149
|
this.deps.renameSession?.(sessionId, task.title);
|
|
150
150
|
} catch {}
|
|
151
151
|
await this.patchExecution(executionId, { sessionId });
|
|
152
|
-
|
|
152
|
+
handle.agent.inject({
|
|
153
153
|
id: this.deps.mintMessageId?.() ?? MessageId(`msg-taskboard-${crypto.randomUUID()}`),
|
|
154
154
|
role: "user",
|
|
155
155
|
content: [{
|
|
156
156
|
type: "text",
|
|
157
|
-
text: this.
|
|
157
|
+
text: this.pluginFraming(task)
|
|
158
|
+
}],
|
|
159
|
+
source: {
|
|
160
|
+
kind: "plugin",
|
|
161
|
+
plugin: "dsh-taskboard"
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
handle.agent.followup({
|
|
165
|
+
id: this.deps.mintMessageId?.() ?? MessageId(`msg-taskboard-${crypto.randomUUID()}`),
|
|
166
|
+
role: "user",
|
|
167
|
+
content: [{
|
|
168
|
+
type: "text",
|
|
169
|
+
text: this.userBody(task)
|
|
158
170
|
}],
|
|
159
171
|
source: { kind: "user" }
|
|
160
|
-
};
|
|
161
|
-
handle.agent.followup(message);
|
|
172
|
+
});
|
|
162
173
|
const settle = () => {
|
|
163
174
|
this.runs.delete(executionId);
|
|
164
175
|
this.deps.store.mutate("execution-recorded", (ledger) => {
|
|
@@ -280,21 +291,26 @@ var ExecutionService = class {
|
|
|
280
291
|
});
|
|
281
292
|
}
|
|
282
293
|
/**
|
|
283
|
-
* The
|
|
284
|
-
*
|
|
285
|
-
*
|
|
294
|
+
* The plugin framing line (rendered as a plugin context row): task head,
|
|
295
|
+
* already-claimed state, and the handoff protocol — everything the session
|
|
296
|
+
* must know about the board. The task id appears exactly once (here); the
|
|
297
|
+
* protocol steps below refer to it as 本任务.
|
|
298
|
+
*/
|
|
299
|
+
pluginFraming(task) {
|
|
300
|
+
return `【任务看板】${task.title}(ID: ${task.id})\n本会话由任务看板执行服务启动,任务已置为进行中——无需认领;「已完成」仅限用户在界面操作(代码已限制,移了会被拒)。\n完成后按序交接:\n1. taskboard_get 读取本任务,取得最新 version\n2. taskboard_comment_add 留评论:做了什么改动 / 如何验证 / 剩余风险\n3. taskboard_move 将本任务移至待验收 in_review(带 ifVersion)\n若无法完成:留评论说明原因,将任务移回待办 todo。`;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* The card body as a normal user bubble: the effective prompt (explicit
|
|
304
|
+
* prompt, else title+description) with template variables resolved from
|
|
305
|
+
* the task's own history at submit time (valuable for recurring patrols):
|
|
286
306
|
* `{{lastExecution}}` → the previous execution's trigger/outcome/error;
|
|
287
307
|
* `{{lastComments}}` → the last three comments (who + body).
|
|
288
308
|
*/
|
|
289
|
-
|
|
290
|
-
const state = "本任务由执行服务启动本会话并已置为 in_progress(你无需再认领,也无需移到 done)。";
|
|
291
|
-
const tail = `完成后请:1) 用 taskboard_get 读取任务 ${task.id} 拿最新 version;2) 用 taskboard_comment_add 留评论(做了什么改动、如何验证、剩余风险);3) 用 taskboard_move 把任务 ${task.id} 移到 in_review(带 ifVersion)。`;
|
|
292
|
-
const base = effectivePrompt(task);
|
|
309
|
+
userBody(task) {
|
|
293
310
|
const lastExec = [...task.executions].reverse().find((e) => e.outcome !== "running");
|
|
294
311
|
const lastExecText = lastExec === void 0 ? "(无)" : `${lastExec.trigger} · ${lastExec.outcome}${lastExec.error !== void 0 ? ` · ${lastExec.error.slice(0, 200)}` : ""} · ${new Date(lastExec.startedAt ?? 0).toISOString()}`;
|
|
295
312
|
const lastCommentsText = task.comments.slice(-3).map((c) => `[${c.threadId !== void 0 ? "agent" : "user"}] ${c.body}`).join("\n") || "(无)";
|
|
296
|
-
|
|
297
|
-
return `【任务】${task.title}(任务 ID: ${task.id})\n\n${state}\n\n${body}\n\n${tail}`;
|
|
313
|
+
return effectivePrompt(task).replace(/\{\{lastExecution\}\}/g, lastExecText).replace(/\{\{lastComments\}\}/g, lastCommentsText);
|
|
298
314
|
}
|
|
299
315
|
/** Move a task back out of in_progress (and release its hold) after a failed start. */
|
|
300
316
|
async revertProgress(taskId) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"execution.js","names":[],"sources":["../../src/host/execution.ts"],"sourcesContent":["/**\n * Host execution service: runs a task through dsh's REAL session machinery —\n * a fresh agent+session inside the task's project workspace (creation carries\n * the pinned model when the task has one), the session is attached to the\n * workspace so it appears in the GUI's project session list, the effective\n * prompt is submitted as an ordinary user message, and the turn settlement\n * (turn/end reason) is folded back into the task's execution record.\n *\n * Every execution is a NEW session: clean context, no reuse of previous runs.\n *\n * @module dsh-taskboard/host/execution\n */\nimport {\n effectivePrompt,\n newCommentId,\n newExecutionId,\n normalizeBody,\n type ExecutionRecord,\n type TaskRecord,\n} from '../shared/protocol.ts'\nimport { MessageId } from './sdk.ts'\nimport type { TaskStore } from './store.ts'\n\n/** Default cap on concurrently running executions (env-overridable). */\nexport const DEFAULT_MAX_CONCURRENT = 3\n\n/** Narrow agents face (the registry's create, structurally). */\nexport interface AgentsFace {\n create(options: {\n sessionId: string\n meta?: { cwd?: string }\n agentOptions?: { provider?: string; model?: string }\n }): Promise<{\n agent: {\n id: string\n followup(message: unknown): void\n whenIdle(): Promise<void>\n }\n dispose(): Promise<void>\n }>\n}\n\n/** Narrow workspaces face for execution. */\nexport interface ExecutionWorkspaceFace {\n get(id: string): { id: string; path: string } | undefined\n attach(workspaceId: string, sessionId: string): Promise<void>\n}\n\n/** Narrow event-bus face for settlement listening. */\nexport interface EventsFace {\n onSessionEvent(listener: (sessionId: string, event: { type: string; data?: unknown }) => void): () => void\n}\n\n/** Everything the execution service needs. */\nexport interface ExecutionDeps {\n store: TaskStore\n agents: AgentsFace\n workspaces: ExecutionWorkspaceFace\n events: EventsFace\n now: () => number\n /** The deployment default model (fills sessions of unpinned tasks). */\n defaultModel?: () => { provider: string; model: string } | undefined\n /** Mint session ids (injectable for tests). */\n mintSessionId?: () => string\n /** Mint message ids (injectable for tests). */\n mintMessageId?: () => string\n /** Best-effort session rename (pins the session list title to the task title). */\n renameSession?: (sessionId: string, title: string) => void\n /** Max concurrently running executions across all tasks (default 3). */\n maxConcurrent?: number\n}\n\n/** Outcome of a run request (immediate; the run settles asynchronously). */\nexport type RunRequestResult =\n | { ok: true; executionId: string; sessionId: string }\n | { ok: false; error: string }\n\n/** Outcome of a cancel request. */\nexport type CancelRequestResult =\n | { ok: true; executionId: string }\n | { ok: false; error: string }\n\n/** Whether a turn/end payload closed with an error reason. */\nfunction isErrorTurnEnd(data: unknown): { message: string } | undefined {\n if (typeof data !== 'object' || data === null) return undefined\n const reason = (data as { reason?: unknown }).reason\n if (typeof reason !== 'object' || reason === null) return undefined\n const kind = (reason as { kind?: unknown }).kind\n if (kind !== 'error') return undefined\n const error = (reason as { error?: { message?: unknown } }).error\n const detail = JSON.stringify(error) ?? ''\n const message = typeof error?.message === 'string' ? error.message : 'turn failed'\n console.error('[dsh-taskboard] turn error detail:', detail.slice(0, 2000))\n void detail\n return { message }\n}\n\n/** One live execution tracked for settlement and cancellation. */\ninterface RunEntry {\n sessionId: string\n settle: () => void\n dispose: () => Promise<void>\n}\n\n/**\n * The execution service.\n */\nexport class ExecutionService {\n /** Live executions by execution id (settles and cancels remove entries). */\n private readonly runs = new Map<string, RunEntry>()\n\n /** @param deps - store + agents + workspaces + events + clock. */\n constructor(private readonly deps: ExecutionDeps) {\n deps.events.onSessionEvent((sessionId, event) => {\n if (event.type !== 'turn/end') return\n const failure = isErrorTurnEnd(event.data)\n if (failure !== undefined) this.noteFailure(sessionId, failure.message)\n })\n }\n\n /** Record a turn failure against the running execution of that session and give the task back. */\n private noteFailure(sessionId: string, message: string): void {\n void this.deps.store.mutate('execution-recorded', (ledger) => {\n for (const task of ledger.tasks) {\n for (const execution of task.executions) {\n if (execution.sessionId === sessionId && execution.outcome === 'running') {\n execution.outcome = 'failed'\n execution.error = message.slice(0, 500)\n execution.endedAt = this.deps.now()\n // The failed session will not finish the work: hand the task back\n // instead of leaving it stuck in in_progress forever — and leave a\n // system comment so the GUI shows why.\n if (task.status === 'in_progress' && task.claimedBy === sessionId) {\n task.status = 'todo'\n task.updatedAt = this.deps.now()\n delete task.claimedBy\n delete task.claimedAt\n task.comments.push({\n id: newCommentId(),\n body: normalizeBody(`[系统] 执行失败:${message.slice(0, 300)};任务已退回待办。`),\n version: 1,\n createdAt: this.deps.now(),\n })\n }\n return [task]\n }\n }\n }\n return undefined\n })\n }\n\n /** Patch one task's execution record in the ledger. */\n private async patchExecution(executionId: string, patch: Partial<ExecutionRecord>): Promise<void> {\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n for (const task of ledger.tasks) {\n const execution = task.executions.find(e => e.id === executionId)\n if (execution !== undefined) {\n Object.assign(execution, patch)\n return [task]\n }\n }\n return undefined\n })\n }\n\n /**\n * Run one task now (manual button or scheduler tick).\n *\n * The in-progress gate and the execution-open write happen inside ONE\n * serial-queue mutation, so two overlapping run() calls (double click,\n * overlapping scheduler ticks) can never both pass — exactly one session\n * is opened per task.\n * @param taskId - the task to run.\n * @param trigger - what started it.\n * @returns the immediate result; settlement lands in the ledger.\n */\n async run(taskId: string, trigger: ExecutionRecord['trigger']): Promise<RunRequestResult> {\n const max = this.deps.maxConcurrent ?? DEFAULT_MAX_CONCURRENT\n if (this.runs.size >= max) {\n return { ok: false, error: `execution concurrency limit reached (${this.runs.size}/${max} running)` }\n }\n const task = this.deps.store.get(taskId)\n if (task === undefined || task.trashedAt !== undefined) {\n return { ok: false, error: `no task ${taskId}` }\n }\n const workspace = this.deps.workspaces.get(task.workspaceId)\n if (workspace === undefined) {\n return { ok: false, error: `unknown workspace ${task.workspaceId}` }\n }\n\n const executionId = newExecutionId()\n const sessionId = this.deps.mintSessionId?.() ?? `session-taskboard-${crypto.randomUUID()}`\n\n // 1. Open the execution record, flip the card to in_progress, and record\n // the executing session as the claim holder — atomically.\n let gate: string | undefined\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n const target = ledger.tasks.find(t => t.id === taskId)\n if (target === undefined || target.trashedAt !== undefined) {\n gate = `no task ${taskId}`\n return undefined\n }\n if (target.status === 'in_progress') {\n gate = 'task is already in progress'\n return undefined\n }\n target.executions.push({\n id: executionId,\n trigger,\n startedAt: this.deps.now(),\n outcome: 'running',\n })\n target.status = 'in_progress'\n target.updatedAt = this.deps.now()\n target.updatedBy = { kind: 'user' }\n target.claimedBy = sessionId\n target.claimedAt = this.deps.now()\n return [target]\n })\n if (gate !== undefined) return { ok: false, error: gate }\n\n // 2. Create the fresh agent+session inside the task's project, carrying\n // the pinned model — or the deployment default when unpinned (the\n // persona template renders {{model}}, so the session always needs one).\n let handle: Awaited<ReturnType<AgentsFace['create']>>\n try {\n const model = task.model ?? this.deps.defaultModel?.()\n handle = await this.deps.agents.create({\n sessionId,\n meta: { cwd: workspace.path },\n ...(model !== undefined ? { agentOptions: { provider: model.provider, model: model.model } } : {}),\n })\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n await this.patchExecution(executionId, { outcome: 'failed', error: message.slice(0, 500), endedAt: this.deps.now() })\n await this.revertProgress(taskId)\n return { ok: false, error: message }\n }\n\n // 3. Attach the session to the workspace (GUI project session list).\n await this.deps.workspaces.attach(task.workspaceId, sessionId).catch(() => { /* cosmetic */ })\n\n // 3b. Best-effort rename: pin the session title to the task title so the\n // session list shows the task name (a user-sourced title also stops\n // automatic first-prompt retitling).\n try {\n this.deps.renameSession?.(sessionId, task.title)\n } catch { /* cosmetic */ }\n\n // 4. Record the session id (execution is really started now).\n await this.patchExecution(executionId, { sessionId })\n\n // 5. Submit the effective prompt as an ordinary user message and settle\n // on quiescence (turn/end errors were already folded by the listener).\n // Source `user` (not `plugin`) so the opening message renders as a\n // normal user bubble in the conversation, exactly like a typed prompt.\n const message = {\n id: this.deps.mintMessageId?.() ?? MessageId(`msg-taskboard-${crypto.randomUUID()}`),\n role: 'user' as const,\n content: [{ type: 'text' as const, text: this.executionPrompt(task) }],\n source: { kind: 'user' as const },\n }\n handle.agent.followup(message)\n\n // 6. Settlement watcher: mark succeeded, release the executing session's\n // hold, and — when the session did NOT follow the handoff protocol —\n // auto-move the card to in_review with a system comment (otherwise a\n // disobedient session would leave it hanging in in_progress forever).\n const settle = (): void => {\n this.runs.delete(executionId)\n void this.deps.store.mutate('execution-recorded', (ledger) => {\n for (const t of ledger.tasks) {\n const execution = t.executions.find(e => e.id === executionId)\n if (execution !== undefined && execution.outcome === 'running') {\n const now = this.deps.now()\n execution.outcome = 'succeeded'\n execution.endedAt = now\n if (t.status === 'in_progress' && t.claimedBy === sessionId) {\n delete t.claimedBy\n delete t.claimedAt\n }\n if (t.status === 'in_progress') {\n const commented = t.comments.some(c => c.threadId === sessionId)\n t.comments.push({\n id: newCommentId(),\n body: normalizeBody(commented\n ? '[系统] 执行会话已结束并留有评论,但未移至待验收;系统自动移入待验收。'\n : '[系统] 执行会话已结束,但未按协议交接(无评论、未移至待验收);系统自动移入待验收,请审查后退回或验收。'),\n version: 1,\n createdAt: now,\n })\n t.status = 'in_review'\n t.updatedAt = now\n t.updatedBy = { kind: 'user' }\n }\n return [t]\n }\n }\n return undefined\n })\n }\n this.runs.set(executionId, { sessionId, settle, dispose: () => handle.dispose() })\n void handle.agent.whenIdle().then(settle, () => {\n this.noteFailure(sessionId, 'agent did not reach quiescence')\n settle()\n })\n\n return { ok: true, executionId, sessionId }\n }\n\n /** How many executions are currently running (for the concurrency cap). */\n inFlight(): number {\n return this.runs.size\n }\n\n /**\n * Cancel the running execution of a task (user action): stop the agent\n * session, mark the execution cancelled, and hand the task back to todo.\n * @param taskId - the task whose execution should be stopped.\n * @returns the immediate result.\n */\n async cancel(taskId: string): Promise<CancelRequestResult> {\n const task = this.deps.store.get(taskId)\n if (task === undefined) return { ok: false, error: `no task ${taskId}` }\n const running = [...task.executions].reverse().find(e => e.outcome === 'running')\n if (running === undefined) return { ok: false, error: 'no running execution' }\n\n const entry = this.runs.get(running.id)\n this.runs.delete(running.id)\n // Stop the agent first (best effort): dispose stops the loop, unregisters\n // the agent, and removes its session. A late whenIdle settlement no-ops —\n // the record is no longer 'running'.\n try {\n await entry?.dispose()\n } catch { /* already gone */ }\n\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n const target = ledger.tasks.find(t => t.id === taskId)\n if (target === undefined) return undefined\n const execution = target.executions.find(e => e.id === running.id)\n if (execution === undefined || execution.outcome !== 'running') return undefined\n execution.outcome = 'cancelled'\n execution.endedAt = this.deps.now()\n if (target.status === 'in_progress') {\n target.status = 'todo'\n target.updatedAt = this.deps.now()\n delete target.claimedBy\n delete target.claimedAt\n }\n return [target]\n })\n return { ok: true, executionId: running.id }\n }\n\n /**\n * Startup reconciliation after a host restart: executions left `running`\n * by the previous process can never settle here (their settlement watchers\n * died with it), so mark them failed and hand their tasks back to todo.\n */\n async reconcile(): Promise<void> {\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n const now = this.deps.now()\n const touched: TaskRecord[] = []\n for (const task of ledger.tasks) {\n let dirty = false\n for (const execution of task.executions) {\n if (execution.outcome === 'running') {\n execution.outcome = 'failed'\n execution.error = 'interrupted by host restart'\n execution.endedAt = now\n dirty = true\n }\n }\n if (!dirty) continue\n if (task.status === 'in_progress') {\n task.status = 'todo'\n task.updatedAt = now\n delete task.claimedBy\n delete task.claimedAt\n }\n touched.push(task)\n }\n return touched.length > 0 ? touched : undefined\n })\n }\n\n /**\n * The prompt text one execution submits (task context + instructions).\n * The effective prompt supports two template variables, rendered from the\n * task's own history at submit time (valuable for recurring patrols):\n * `{{lastExecution}}` → the previous execution's trigger/outcome/error;\n * `{{lastComments}}` → the last three comments (who + body).\n */\n private executionPrompt(task: TaskRecord): string {\n const state = '本任务由执行服务启动本会话并已置为 in_progress(你无需再认领,也无需移到 done)。'\n const tail = `完成后请:1) 用 taskboard_get 读取任务 ${task.id} 拿最新 version;`\n + `2) 用 taskboard_comment_add 留评论(做了什么改动、如何验证、剩余风险);`\n + `3) 用 taskboard_move 把任务 ${task.id} 移到 in_review(带 ifVersion)。`\n const base = effectivePrompt(task)\n const lastExec = [...task.executions].reverse().find(e => e.outcome !== 'running')\n const lastExecText = lastExec === undefined\n ? '(无)'\n : `${lastExec.trigger} · ${lastExec.outcome}${lastExec.error !== undefined ? ` · ${lastExec.error.slice(0, 200)}` : ''} · ${new Date(lastExec.startedAt ?? 0).toISOString()}`\n const lastCommentsText = task.comments.slice(-3)\n .map(c => `[${c.threadId !== undefined ? 'agent' : 'user'}] ${c.body}`)\n .join('\\n') || '(无)'\n const body = base\n .replace(/\\{\\{lastExecution\\}\\}/g, lastExecText)\n .replace(/\\{\\{lastComments\\}\\}/g, lastCommentsText)\n return `【任务】${task.title}(任务 ID: ${task.id})\\n\\n${state}\\n\\n${body}\\n\\n${tail}`\n }\n\n /** Move a task back out of in_progress (and release its hold) after a failed start. */\n private async revertProgress(taskId: string): Promise<void> {\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n const target = ledger.tasks.find(t => t.id === taskId)\n if (target !== undefined && target.status === 'in_progress') {\n target.status = 'todo'\n target.updatedAt = this.deps.now()\n delete target.claimedBy\n delete target.claimedAt\n return [target]\n }\n return undefined\n })\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"execution.js","names":[],"sources":["../../src/host/execution.ts"],"sourcesContent":["/**\n * Host execution service: runs a task through dsh's REAL session machinery —\n * a fresh agent+session inside the task's project workspace (creation carries\n * the pinned model when the task has one), the session is attached to the\n * workspace so it appears in the GUI's project session list, the effective\n * prompt is submitted as an ordinary user message, and the turn settlement\n * (turn/end reason) is folded back into the task's execution record.\n *\n * Every execution is a NEW session: clean context, no reuse of previous runs.\n *\n * @module dsh-taskboard/host/execution\n */\nimport {\n effectivePrompt,\n newCommentId,\n newExecutionId,\n normalizeBody,\n type ExecutionRecord,\n type TaskRecord,\n} from '../shared/protocol.ts'\nimport { MessageId } from './sdk.ts'\nimport type { TaskStore } from './store.ts'\n\n/** Default cap on concurrently running executions (env-overridable). */\nexport const DEFAULT_MAX_CONCURRENT = 3\n\n/** Narrow agents face (the registry's create, structurally). */\nexport interface AgentsFace {\n create(options: {\n sessionId: string\n meta?: { cwd?: string }\n agentOptions?: { provider?: string; model?: string }\n }): Promise<{\n agent: {\n id: string\n followup(message: unknown): void\n inject(message: unknown): void\n whenIdle(): Promise<void>\n }\n dispose(): Promise<void>\n }>\n}\n\n/** Narrow workspaces face for execution. */\nexport interface ExecutionWorkspaceFace {\n get(id: string): { id: string; path: string } | undefined\n attach(workspaceId: string, sessionId: string): Promise<void>\n}\n\n/** Narrow event-bus face for settlement listening. */\nexport interface EventsFace {\n onSessionEvent(listener: (sessionId: string, event: { type: string; data?: unknown }) => void): () => void\n}\n\n/** Everything the execution service needs. */\nexport interface ExecutionDeps {\n store: TaskStore\n agents: AgentsFace\n workspaces: ExecutionWorkspaceFace\n events: EventsFace\n now: () => number\n /** The deployment default model (fills sessions of unpinned tasks). */\n defaultModel?: () => { provider: string; model: string } | undefined\n /** Mint session ids (injectable for tests). */\n mintSessionId?: () => string\n /** Mint message ids (injectable for tests). */\n mintMessageId?: () => string\n /** Best-effort session rename (pins the session list title to the task title). */\n renameSession?: (sessionId: string, title: string) => void\n /** Max concurrently running executions across all tasks (default 3). */\n maxConcurrent?: number\n}\n\n/** Outcome of a run request (immediate; the run settles asynchronously). */\nexport type RunRequestResult =\n | { ok: true; executionId: string; sessionId: string }\n | { ok: false; error: string }\n\n/** Outcome of a cancel request. */\nexport type CancelRequestResult =\n | { ok: true; executionId: string }\n | { ok: false; error: string }\n\n/** Whether a turn/end payload closed with an error reason. */\nfunction isErrorTurnEnd(data: unknown): { message: string } | undefined {\n if (typeof data !== 'object' || data === null) return undefined\n const reason = (data as { reason?: unknown }).reason\n if (typeof reason !== 'object' || reason === null) return undefined\n const kind = (reason as { kind?: unknown }).kind\n if (kind !== 'error') return undefined\n const error = (reason as { error?: { message?: unknown } }).error\n const detail = JSON.stringify(error) ?? ''\n const message = typeof error?.message === 'string' ? error.message : 'turn failed'\n console.error('[dsh-taskboard] turn error detail:', detail.slice(0, 2000))\n void detail\n return { message }\n}\n\n/** One live execution tracked for settlement and cancellation. */\ninterface RunEntry {\n sessionId: string\n settle: () => void\n dispose: () => Promise<void>\n}\n\n/**\n * The execution service.\n */\nexport class ExecutionService {\n /** Live executions by execution id (settles and cancels remove entries). */\n private readonly runs = new Map<string, RunEntry>()\n\n /** @param deps - store + agents + workspaces + events + clock. */\n constructor(private readonly deps: ExecutionDeps) {\n deps.events.onSessionEvent((sessionId, event) => {\n if (event.type !== 'turn/end') return\n const failure = isErrorTurnEnd(event.data)\n if (failure !== undefined) this.noteFailure(sessionId, failure.message)\n })\n }\n\n /** Record a turn failure against the running execution of that session and give the task back. */\n private noteFailure(sessionId: string, message: string): void {\n void this.deps.store.mutate('execution-recorded', (ledger) => {\n for (const task of ledger.tasks) {\n for (const execution of task.executions) {\n if (execution.sessionId === sessionId && execution.outcome === 'running') {\n execution.outcome = 'failed'\n execution.error = message.slice(0, 500)\n execution.endedAt = this.deps.now()\n // The failed session will not finish the work: hand the task back\n // instead of leaving it stuck in in_progress forever — and leave a\n // system comment so the GUI shows why.\n if (task.status === 'in_progress' && task.claimedBy === sessionId) {\n task.status = 'todo'\n task.updatedAt = this.deps.now()\n delete task.claimedBy\n delete task.claimedAt\n task.comments.push({\n id: newCommentId(),\n body: normalizeBody(`[系统] 执行失败:${message.slice(0, 300)};任务已退回待办。`),\n version: 1,\n createdAt: this.deps.now(),\n })\n }\n return [task]\n }\n }\n }\n return undefined\n })\n }\n\n /** Patch one task's execution record in the ledger. */\n private async patchExecution(executionId: string, patch: Partial<ExecutionRecord>): Promise<void> {\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n for (const task of ledger.tasks) {\n const execution = task.executions.find(e => e.id === executionId)\n if (execution !== undefined) {\n Object.assign(execution, patch)\n return [task]\n }\n }\n return undefined\n })\n }\n\n /**\n * Run one task now (manual button or scheduler tick).\n *\n * The in-progress gate and the execution-open write happen inside ONE\n * serial-queue mutation, so two overlapping run() calls (double click,\n * overlapping scheduler ticks) can never both pass — exactly one session\n * is opened per task.\n * @param taskId - the task to run.\n * @param trigger - what started it.\n * @returns the immediate result; settlement lands in the ledger.\n */\n async run(taskId: string, trigger: ExecutionRecord['trigger']): Promise<RunRequestResult> {\n const max = this.deps.maxConcurrent ?? DEFAULT_MAX_CONCURRENT\n if (this.runs.size >= max) {\n return { ok: false, error: `execution concurrency limit reached (${this.runs.size}/${max} running)` }\n }\n const task = this.deps.store.get(taskId)\n if (task === undefined || task.trashedAt !== undefined) {\n return { ok: false, error: `no task ${taskId}` }\n }\n const workspace = this.deps.workspaces.get(task.workspaceId)\n if (workspace === undefined) {\n return { ok: false, error: `unknown workspace ${task.workspaceId}` }\n }\n\n const executionId = newExecutionId()\n const sessionId = this.deps.mintSessionId?.() ?? `session-taskboard-${crypto.randomUUID()}`\n\n // 1. Open the execution record, flip the card to in_progress, and record\n // the executing session as the claim holder — atomically.\n let gate: string | undefined\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n const target = ledger.tasks.find(t => t.id === taskId)\n if (target === undefined || target.trashedAt !== undefined) {\n gate = `no task ${taskId}`\n return undefined\n }\n if (target.status === 'in_progress') {\n gate = 'task is already in progress'\n return undefined\n }\n target.executions.push({\n id: executionId,\n trigger,\n startedAt: this.deps.now(),\n outcome: 'running',\n })\n target.status = 'in_progress'\n target.updatedAt = this.deps.now()\n target.updatedBy = { kind: 'user' }\n target.claimedBy = sessionId\n target.claimedAt = this.deps.now()\n return [target]\n })\n if (gate !== undefined) return { ok: false, error: gate }\n\n // 2. Create the fresh agent+session inside the task's project, carrying\n // the pinned model — or the deployment default when unpinned (the\n // persona template renders {{model}}, so the session always needs one).\n let handle: Awaited<ReturnType<AgentsFace['create']>>\n try {\n const model = task.model ?? this.deps.defaultModel?.()\n handle = await this.deps.agents.create({\n sessionId,\n meta: { cwd: workspace.path },\n ...(model !== undefined ? { agentOptions: { provider: model.provider, model: model.model } } : {}),\n })\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n await this.patchExecution(executionId, { outcome: 'failed', error: message.slice(0, 500), endedAt: this.deps.now() })\n await this.revertProgress(taskId)\n return { ok: false, error: message }\n }\n\n // 3. Attach the session to the workspace (GUI project session list).\n await this.deps.workspaces.attach(task.workspaceId, sessionId).catch(() => { /* cosmetic */ })\n\n // 3b. Best-effort rename: pin the session title to the task title so the\n // session list shows the task name (a user-sourced title also stops\n // automatic first-prompt retitling).\n try {\n this.deps.renameSession?.(sessionId, task.title)\n } catch { /* cosmetic */ }\n\n // 4. Record the session id (execution is really started now).\n await this.patchExecution(executionId, { sessionId })\n\n // 5. Submit the opening pair and settle on quiescence (turn/end errors\n // were already folded by the listener). Two messages, ONE turn:\n // - inject() queues the plugin framing line (next-step, no wake); it\n // renders as a plugin context row in the conversation.\n // - followup() queues the card body as a normal user message\n // (next-turn, wakes the driver). At claim time the loop drains ALL\n // next-step messages plus the one next-turn message into a single\n // turn — framing first, then the user bubble.\n handle.agent.inject({\n id: this.deps.mintMessageId?.() ?? MessageId(`msg-taskboard-${crypto.randomUUID()}`),\n role: 'user' as const,\n content: [{ type: 'text' as const, text: this.pluginFraming(task) }],\n source: { kind: 'plugin' as const, plugin: 'dsh-taskboard' },\n })\n handle.agent.followup({\n id: this.deps.mintMessageId?.() ?? MessageId(`msg-taskboard-${crypto.randomUUID()}`),\n role: 'user' as const,\n content: [{ type: 'text' as const, text: this.userBody(task) }],\n source: { kind: 'user' as const },\n })\n\n // 6. Settlement watcher: mark succeeded, release the executing session's\n // hold, and — when the session did NOT follow the handoff protocol —\n // auto-move the card to in_review with a system comment (otherwise a\n // disobedient session would leave it hanging in in_progress forever).\n const settle = (): void => {\n this.runs.delete(executionId)\n void this.deps.store.mutate('execution-recorded', (ledger) => {\n for (const t of ledger.tasks) {\n const execution = t.executions.find(e => e.id === executionId)\n if (execution !== undefined && execution.outcome === 'running') {\n const now = this.deps.now()\n execution.outcome = 'succeeded'\n execution.endedAt = now\n if (t.status === 'in_progress' && t.claimedBy === sessionId) {\n delete t.claimedBy\n delete t.claimedAt\n }\n if (t.status === 'in_progress') {\n const commented = t.comments.some(c => c.threadId === sessionId)\n t.comments.push({\n id: newCommentId(),\n body: normalizeBody(commented\n ? '[系统] 执行会话已结束并留有评论,但未移至待验收;系统自动移入待验收。'\n : '[系统] 执行会话已结束,但未按协议交接(无评论、未移至待验收);系统自动移入待验收,请审查后退回或验收。'),\n version: 1,\n createdAt: now,\n })\n t.status = 'in_review'\n t.updatedAt = now\n t.updatedBy = { kind: 'user' }\n }\n return [t]\n }\n }\n return undefined\n })\n }\n this.runs.set(executionId, { sessionId, settle, dispose: () => handle.dispose() })\n void handle.agent.whenIdle().then(settle, () => {\n this.noteFailure(sessionId, 'agent did not reach quiescence')\n settle()\n })\n\n return { ok: true, executionId, sessionId }\n }\n\n /** How many executions are currently running (for the concurrency cap). */\n inFlight(): number {\n return this.runs.size\n }\n\n /**\n * Cancel the running execution of a task (user action): stop the agent\n * session, mark the execution cancelled, and hand the task back to todo.\n * @param taskId - the task whose execution should be stopped.\n * @returns the immediate result.\n */\n async cancel(taskId: string): Promise<CancelRequestResult> {\n const task = this.deps.store.get(taskId)\n if (task === undefined) return { ok: false, error: `no task ${taskId}` }\n const running = [...task.executions].reverse().find(e => e.outcome === 'running')\n if (running === undefined) return { ok: false, error: 'no running execution' }\n\n const entry = this.runs.get(running.id)\n this.runs.delete(running.id)\n // Stop the agent first (best effort): dispose stops the loop, unregisters\n // the agent, and removes its session. A late whenIdle settlement no-ops —\n // the record is no longer 'running'.\n try {\n await entry?.dispose()\n } catch { /* already gone */ }\n\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n const target = ledger.tasks.find(t => t.id === taskId)\n if (target === undefined) return undefined\n const execution = target.executions.find(e => e.id === running.id)\n if (execution === undefined || execution.outcome !== 'running') return undefined\n execution.outcome = 'cancelled'\n execution.endedAt = this.deps.now()\n if (target.status === 'in_progress') {\n target.status = 'todo'\n target.updatedAt = this.deps.now()\n delete target.claimedBy\n delete target.claimedAt\n }\n return [target]\n })\n return { ok: true, executionId: running.id }\n }\n\n /**\n * Startup reconciliation after a host restart: executions left `running`\n * by the previous process can never settle here (their settlement watchers\n * died with it), so mark them failed and hand their tasks back to todo.\n */\n async reconcile(): Promise<void> {\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n const now = this.deps.now()\n const touched: TaskRecord[] = []\n for (const task of ledger.tasks) {\n let dirty = false\n for (const execution of task.executions) {\n if (execution.outcome === 'running') {\n execution.outcome = 'failed'\n execution.error = 'interrupted by host restart'\n execution.endedAt = now\n dirty = true\n }\n }\n if (!dirty) continue\n if (task.status === 'in_progress') {\n task.status = 'todo'\n task.updatedAt = now\n delete task.claimedBy\n delete task.claimedAt\n }\n touched.push(task)\n }\n return touched.length > 0 ? touched : undefined\n })\n }\n\n /**\n * The plugin framing line (rendered as a plugin context row): task head,\n * already-claimed state, and the handoff protocol — everything the session\n * must know about the board. The task id appears exactly once (here); the\n * protocol steps below refer to it as 本任务.\n */\n private pluginFraming(task: TaskRecord): string {\n return `【任务看板】${task.title}(ID: ${task.id})\\n`\n + `本会话由任务看板执行服务启动,任务已置为进行中——无需认领;「已完成」仅限用户在界面操作(代码已限制,移了会被拒)。\\n`\n + `完成后按序交接:\\n`\n + `1. taskboard_get 读取本任务,取得最新 version\\n`\n + `2. taskboard_comment_add 留评论:做了什么改动 / 如何验证 / 剩余风险\\n`\n + `3. taskboard_move 将本任务移至待验收 in_review(带 ifVersion)\\n`\n + `若无法完成:留评论说明原因,将任务移回待办 todo。`\n }\n\n /**\n * The card body as a normal user bubble: the effective prompt (explicit\n * prompt, else title+description) with template variables resolved from\n * the task's own history at submit time (valuable for recurring patrols):\n * `{{lastExecution}}` → the previous execution's trigger/outcome/error;\n * `{{lastComments}}` → the last three comments (who + body).\n */\n private userBody(task: TaskRecord): string {\n const lastExec = [...task.executions].reverse().find(e => e.outcome !== 'running')\n const lastExecText = lastExec === undefined\n ? '(无)'\n : `${lastExec.trigger} · ${lastExec.outcome}${lastExec.error !== undefined ? ` · ${lastExec.error.slice(0, 200)}` : ''} · ${new Date(lastExec.startedAt ?? 0).toISOString()}`\n const lastCommentsText = task.comments.slice(-3)\n .map(c => `[${c.threadId !== undefined ? 'agent' : 'user'}] ${c.body}`)\n .join('\\n') || '(无)'\n return effectivePrompt(task)\n .replace(/\\{\\{lastExecution\\}\\}/g, lastExecText)\n .replace(/\\{\\{lastComments\\}\\}/g, lastCommentsText)\n }\n\n /** Move a task back out of in_progress (and release its hold) after a failed start. */\n private async revertProgress(taskId: string): Promise<void> {\n await this.deps.store.mutate('execution-recorded', (ledger) => {\n const target = ledger.tasks.find(t => t.id === taskId)\n if (target !== undefined && target.status === 'in_progress') {\n target.status = 'todo'\n target.updatedAt = this.deps.now()\n delete target.claimedBy\n delete target.claimedAt\n return [target]\n }\n return undefined\n })\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-taskboard",
|
|
3
3
|
"description": "Agent-first task board for the DSH web GUI: host-authoritative task ledger with taskboard_* agent tools, project (= workspace) claim boundaries, per-task model execution in fresh sessions, host-side cron scheduling, and a live SSE kanban view. Mounts via the official dsh plugin system — no DSH source changes.",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
package/src/host/execution.ts
CHANGED
|
@@ -34,6 +34,7 @@ export interface AgentsFace {
|
|
|
34
34
|
agent: {
|
|
35
35
|
id: string
|
|
36
36
|
followup(message: unknown): void
|
|
37
|
+
inject(message: unknown): void
|
|
37
38
|
whenIdle(): Promise<void>
|
|
38
39
|
}
|
|
39
40
|
dispose(): Promise<void>
|
|
@@ -251,17 +252,26 @@ export class ExecutionService {
|
|
|
251
252
|
// 4. Record the session id (execution is really started now).
|
|
252
253
|
await this.patchExecution(executionId, { sessionId })
|
|
253
254
|
|
|
254
|
-
// 5. Submit the
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
|
|
255
|
+
// 5. Submit the opening pair and settle on quiescence (turn/end errors
|
|
256
|
+
// were already folded by the listener). Two messages, ONE turn:
|
|
257
|
+
// - inject() queues the plugin framing line (next-step, no wake); it
|
|
258
|
+
// renders as a plugin context row in the conversation.
|
|
259
|
+
// - followup() queues the card body as a normal user message
|
|
260
|
+
// (next-turn, wakes the driver). At claim time the loop drains ALL
|
|
261
|
+
// next-step messages plus the one next-turn message into a single
|
|
262
|
+
// turn — framing first, then the user bubble.
|
|
263
|
+
handle.agent.inject({
|
|
259
264
|
id: this.deps.mintMessageId?.() ?? MessageId(`msg-taskboard-${crypto.randomUUID()}`),
|
|
260
265
|
role: 'user' as const,
|
|
261
|
-
content: [{ type: 'text' as const, text: this.
|
|
266
|
+
content: [{ type: 'text' as const, text: this.pluginFraming(task) }],
|
|
267
|
+
source: { kind: 'plugin' as const, plugin: 'dsh-taskboard' },
|
|
268
|
+
})
|
|
269
|
+
handle.agent.followup({
|
|
270
|
+
id: this.deps.mintMessageId?.() ?? MessageId(`msg-taskboard-${crypto.randomUUID()}`),
|
|
271
|
+
role: 'user' as const,
|
|
272
|
+
content: [{ type: 'text' as const, text: this.userBody(task) }],
|
|
262
273
|
source: { kind: 'user' as const },
|
|
263
|
-
}
|
|
264
|
-
handle.agent.followup(message)
|
|
274
|
+
})
|
|
265
275
|
|
|
266
276
|
// 6. Settlement watcher: mark succeeded, release the executing session's
|
|
267
277
|
// hold, and — when the session did NOT follow the handoff protocol —
|
|
@@ -386,18 +396,29 @@ export class ExecutionService {
|
|
|
386
396
|
}
|
|
387
397
|
|
|
388
398
|
/**
|
|
389
|
-
* The
|
|
390
|
-
*
|
|
391
|
-
*
|
|
399
|
+
* The plugin framing line (rendered as a plugin context row): task head,
|
|
400
|
+
* already-claimed state, and the handoff protocol — everything the session
|
|
401
|
+
* must know about the board. The task id appears exactly once (here); the
|
|
402
|
+
* protocol steps below refer to it as 本任务.
|
|
403
|
+
*/
|
|
404
|
+
private pluginFraming(task: TaskRecord): string {
|
|
405
|
+
return `【任务看板】${task.title}(ID: ${task.id})\n`
|
|
406
|
+
+ `本会话由任务看板执行服务启动,任务已置为进行中——无需认领;「已完成」仅限用户在界面操作(代码已限制,移了会被拒)。\n`
|
|
407
|
+
+ `完成后按序交接:\n`
|
|
408
|
+
+ `1. taskboard_get 读取本任务,取得最新 version\n`
|
|
409
|
+
+ `2. taskboard_comment_add 留评论:做了什么改动 / 如何验证 / 剩余风险\n`
|
|
410
|
+
+ `3. taskboard_move 将本任务移至待验收 in_review(带 ifVersion)\n`
|
|
411
|
+
+ `若无法完成:留评论说明原因,将任务移回待办 todo。`
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* The card body as a normal user bubble: the effective prompt (explicit
|
|
416
|
+
* prompt, else title+description) with template variables resolved from
|
|
417
|
+
* the task's own history at submit time (valuable for recurring patrols):
|
|
392
418
|
* `{{lastExecution}}` → the previous execution's trigger/outcome/error;
|
|
393
419
|
* `{{lastComments}}` → the last three comments (who + body).
|
|
394
420
|
*/
|
|
395
|
-
private
|
|
396
|
-
const state = '本任务由执行服务启动本会话并已置为 in_progress(你无需再认领,也无需移到 done)。'
|
|
397
|
-
const tail = `完成后请:1) 用 taskboard_get 读取任务 ${task.id} 拿最新 version;`
|
|
398
|
-
+ `2) 用 taskboard_comment_add 留评论(做了什么改动、如何验证、剩余风险);`
|
|
399
|
-
+ `3) 用 taskboard_move 把任务 ${task.id} 移到 in_review(带 ifVersion)。`
|
|
400
|
-
const base = effectivePrompt(task)
|
|
421
|
+
private userBody(task: TaskRecord): string {
|
|
401
422
|
const lastExec = [...task.executions].reverse().find(e => e.outcome !== 'running')
|
|
402
423
|
const lastExecText = lastExec === undefined
|
|
403
424
|
? '(无)'
|
|
@@ -405,10 +426,9 @@ export class ExecutionService {
|
|
|
405
426
|
const lastCommentsText = task.comments.slice(-3)
|
|
406
427
|
.map(c => `[${c.threadId !== undefined ? 'agent' : 'user'}] ${c.body}`)
|
|
407
428
|
.join('\n') || '(无)'
|
|
408
|
-
|
|
429
|
+
return effectivePrompt(task)
|
|
409
430
|
.replace(/\{\{lastExecution\}\}/g, lastExecText)
|
|
410
431
|
.replace(/\{\{lastComments\}\}/g, lastCommentsText)
|
|
411
|
-
return `【任务】${task.title}(任务 ID: ${task.id})\n\n${state}\n\n${body}\n\n${tail}`
|
|
412
432
|
}
|
|
413
433
|
|
|
414
434
|
/** Move a task back out of in_progress (and release its hold) after a failed start. */
|
package/src/shared/version.ts
CHANGED