@linxin666/dsh-client-ui-task-board 0.4.3 → 0.4.5
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.i18n.yaml +2 -2
- package/README.md +56 -18
- package/README.zh.md +56 -18
- package/lib/client.js +6242 -958
- package/lib/client.js.map +1 -1
- package/lib/index.js +3475 -234
- package/lib/types/client/PluginSettingsCard.d.ts +7 -0
- package/lib/types/client/TaskBoardSettingsCard.d.ts +19 -3
- package/lib/types/client/board/ClampedMarkdown.d.ts +3 -0
- package/lib/types/client/board/ConfirmDialog.d.ts +4 -1
- package/lib/types/client/board/EditTaskModal.d.ts +5 -2
- package/lib/types/client/board/LinkSubtaskModal.d.ts +5 -2
- package/lib/types/client/board/NewTaskModal.d.ts +4 -1
- package/lib/types/client/board/TagManagerModal.d.ts +26 -0
- package/lib/types/client/board/TaskBoard.d.ts +10 -1
- package/lib/types/client/board/TaskCard.d.ts +14 -1
- package/lib/types/client/board/TaskDetail.d.ts +3 -1
- package/lib/types/client/board/TaskForm.d.ts +23 -2
- package/lib/types/client/board/VerificationReport.d.ts +23 -0
- package/lib/types/client/board/card-view.d.ts +113 -0
- package/lib/types/client/board/icons.d.ts +32 -0
- package/lib/types/client/board/overlay.d.ts +69 -0
- package/lib/types/client/board/preset-label.d.ts +22 -0
- package/lib/types/client/board/status-key.d.ts +15 -0
- package/lib/types/client/board/task-markdown.d.ts +19 -0
- package/lib/types/client/board-card-seat.d.ts +34 -0
- package/lib/types/client/host-api.d.ts +24 -1
- package/lib/types/client/index.d.ts +19 -0
- package/lib/types/client/locales.d.ts +141 -3
- package/lib/types/client/native-panel.d.ts +6 -20
- package/lib/types/client/schedule-zone.d.ts +47 -0
- package/lib/types/client/seats.d.ts +18 -0
- package/lib/types/client/service.d.ts +32 -0
- package/lib/types/client/settings-form.d.ts +16 -0
- package/lib/types/core/controller.d.ts +38 -1
- package/lib/types/core/degraded-reason.d.ts +46 -0
- package/lib/types/core/extension.d.ts +283 -0
- package/lib/types/core/handover.d.ts +13 -1
- package/lib/types/core/issue-form.d.ts +16 -0
- package/lib/types/core/package-name.d.ts +9 -0
- package/lib/types/core/schedule.d.ts +95 -20
- package/lib/types/core/subtask.d.ts +30 -11
- package/lib/types/core/tasks.d.ts +110 -5
- package/lib/types/core/use-cases/task-create.d.ts +2 -1
- package/lib/types/core/use-cases/task-schedule.d.ts +8 -4
- package/lib/types/core/use-cases/task-tag.d.ts +48 -0
- package/lib/types/core/use-cases/task-update.d.ts +5 -3
- package/lib/types/core/verification.d.ts +386 -0
- package/lib/types/core/workspace-target.d.ts +41 -0
- package/lib/types/host/agent-tools.d.ts +1 -1
- package/lib/types/host/extension-registry.d.ts +111 -0
- package/lib/types/host/llm-dispatch.d.ts +63 -0
- package/lib/types/host/verification-gate.d.ts +71 -0
- package/lib/types/host/verification-runner.d.ts +108 -0
- package/lib/types/host/workspace-evidence.d.ts +53 -0
- package/lib/types/host-ledger.d.ts +107 -11
- package/lib/types/host-runner.d.ts +99 -4
- package/lib/types/host-service.d.ts +114 -2
- package/lib/types/index.d.ts +80 -26
- package/lib/types/protocol.d.ts +53 -3
- package/lib/types/tool-surface.d.ts +71 -0
- package/package.json +27 -26
- package/src/client/PluginSettingsCard.tsx +9 -0
- package/src/client/TaskBoardSettingsCard.tsx +304 -65
- package/src/client/board/ClampedMarkdown.tsx +69 -0
- package/src/client/board/ConfirmDialog.tsx +7 -3
- package/src/client/board/EditTaskModal.tsx +5 -2
- package/src/client/board/LinkSubtaskModal.tsx +30 -15
- package/src/client/board/NewTaskModal.tsx +288 -151
- package/src/client/board/TagManagerModal.tsx +235 -0
- package/src/client/board/TaskBoard.tsx +259 -103
- package/src/client/board/TaskCard.tsx +238 -86
- package/src/client/board/TaskDetail.tsx +154 -59
- package/src/client/board/TaskForm.tsx +61 -6
- package/src/client/board/VerificationReport.tsx +203 -0
- package/src/client/board/card-view.ts +220 -0
- package/src/client/board/icons.tsx +89 -0
- package/src/client/board/overlay.tsx +189 -0
- package/src/client/board/preset-label.ts +44 -0
- package/src/client/board/status-key.ts +27 -0
- package/src/client/board/task-markdown.module.css +137 -0
- package/src/client/board/task-markdown.tsx +106 -0
- package/src/client/board-card-seat.ts +102 -0
- package/src/client/board-settings.module.css +66 -0
- package/src/client/board.module.css +602 -68
- package/src/client/host-api.ts +109 -6
- package/src/client/index.ts +48 -6
- package/src/client/locales.ts +290 -12
- package/src/client/native-panel.tsx +18 -2
- package/src/client/schedule-zone.ts +135 -0
- package/src/client/seats.tsx +58 -0
- package/src/client/service.ts +94 -0
- package/src/client/settings-card.module.css +31 -5
- package/src/client/settings-form.ts +27 -1
- package/src/core/controller.ts +101 -5
- package/src/core/degraded-reason.ts +81 -0
- package/src/core/extension.ts +376 -0
- package/src/core/freeze-snapshot.ts +2 -2
- package/src/core/handover.ts +16 -1
- package/src/core/issue-form.ts +19 -0
- package/src/core/package-name.ts +9 -0
- package/src/core/schedule.ts +211 -55
- package/src/core/store.ts +26 -1
- package/src/core/subtask.ts +77 -22
- package/src/core/tasks.ts +138 -10
- package/src/core/use-cases/task-create.ts +14 -2
- package/src/core/use-cases/task-parent.ts +3 -2
- package/src/core/use-cases/task-schedule.ts +17 -6
- package/src/core/use-cases/task-tag.ts +110 -0
- package/src/core/use-cases/task-update.ts +13 -4
- package/src/core/verification.ts +826 -0
- package/src/core/workspace-target.ts +59 -0
- package/src/host/agent-tools.ts +58 -24
- package/src/host/extension-registry.ts +509 -0
- package/src/host/llm-dispatch.ts +87 -0
- package/src/host/verification-gate.ts +331 -0
- package/src/host/verification-runner.ts +535 -0
- package/src/host/workspace-evidence.ts +176 -0
- package/src/host-ai.ts +15 -9
- package/src/host-ledger.ts +342 -51
- package/src/host-routes.ts +15 -2
- package/src/host-runner.ts +219 -20
- package/src/host-service.ts +346 -26
- package/src/index.ts +204 -16
- package/src/protocol.ts +122 -9
- package/src/tool-surface.ts +95 -0
- package/lib/types/host/run-guarded.d.ts +0 -39
- package/src/host/run-guarded.ts +0 -76
package/src/host-routes.ts
CHANGED
|
@@ -208,7 +208,7 @@ export function makeTaskBoardRoutes(
|
|
|
208
208
|
if (parsed.action.kind !== 'import' && Buffer.byteLength(body.raw) > ACTION_LIMIT) {
|
|
209
209
|
return writeJson(res, 413, { ok: false, error: 'body-too-large' }, { 'cache-control': 'no-store' })
|
|
210
210
|
}
|
|
211
|
-
writeJson(res, 200, service.apply(parsed.requestId, parsed.action, parsed.initiator), { 'cache-control': 'no-store' })
|
|
211
|
+
writeJson(res, 200, await service.apply(parsed.requestId, parsed.action, parsed.initiator), { 'cache-control': 'no-store' })
|
|
212
212
|
} catch (error) {
|
|
213
213
|
const message = error instanceof Error ? error.message : String(error)
|
|
214
214
|
writeJson(res, message === 'body-too-large' ? 413 : 400, { ok: false, error: message }, { 'cache-control': 'no-store' })
|
|
@@ -289,5 +289,18 @@ export function makeTaskBoardRoutes(
|
|
|
289
289
|
}
|
|
290
290
|
},
|
|
291
291
|
}
|
|
292
|
-
|
|
292
|
+
const verification: WebRoute = {
|
|
293
|
+
kind: 'exact',
|
|
294
|
+
path: `${TASK_BOARD_API_PREFIX}/verification`,
|
|
295
|
+
handler: async (req, res): Promise<void> => {
|
|
296
|
+
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'method-not-allowed' }, { 'cache-control': 'no-store' })
|
|
297
|
+
if (!guard(req, res)) return
|
|
298
|
+
try {
|
|
299
|
+
writeJson(res, 200, await service.verificationOptions(), { 'cache-control': 'no-store' })
|
|
300
|
+
} catch (error) {
|
|
301
|
+
writeJson(res, 500, { ok: false, error: error instanceof Error ? error.message : String(error) }, { 'cache-control': 'no-store' })
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
}
|
|
305
|
+
return [state, action, events, parse, verification]
|
|
293
306
|
}
|
package/src/host-runner.ts
CHANGED
|
@@ -3,9 +3,13 @@ import type { SessionAddress, SessionHistoryRecord, SessionListValue, SessionPag
|
|
|
3
3
|
import type { CommandResult } from '@deepseek-ai/dsh-commands/types'
|
|
4
4
|
import type { Workspace } from '@deepseek-ai/dsh-workspace/types'
|
|
5
5
|
import { teammateName } from './core/subtask.ts'
|
|
6
|
+
import { mostRecentWorkspaceId } from './core/workspace-target.ts'
|
|
6
7
|
import type { TaskPermission, TaskRecord } from './core/tasks.ts'
|
|
7
8
|
|
|
8
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Host services needed to validate a task's workspace before creating a
|
|
11
|
+
* session, and to resolve the workspace an unpinned card runs in.
|
|
12
|
+
*/
|
|
9
13
|
export interface TaskBoardWorkspaceRegistry {
|
|
10
14
|
list(): readonly Workspace[]
|
|
11
15
|
}
|
|
@@ -60,6 +64,17 @@ function isDefinitionWithdrawn(error: unknown): boolean {
|
|
|
60
64
|
|
|
61
65
|
const SERVICE_UNAVAILABLE_ATTEMPTS = 5
|
|
62
66
|
const SERVICE_UNAVAILABLE_BACKOFF_MS = 2_000
|
|
67
|
+
/**
|
|
68
|
+
* Largest number of history pages one inspection walks back. A session whose
|
|
69
|
+
* window is wider than this cannot be resolved from its log, which the caller
|
|
70
|
+
* treats as an unreadable history rather than as pending work.
|
|
71
|
+
*/
|
|
72
|
+
const HISTORY_PAGE_LIMIT = 100
|
|
73
|
+
|
|
74
|
+
/** One-line text of a gateway or stream failure, for a decideable reason. */
|
|
75
|
+
function failureText(error: unknown): string {
|
|
76
|
+
return error instanceof Error ? error.message : String(error)
|
|
77
|
+
}
|
|
63
78
|
|
|
64
79
|
function delay(ms: number): Promise<void> {
|
|
65
80
|
return new Promise(resolve => { setTimeout(resolve, ms) })
|
|
@@ -87,7 +102,14 @@ export interface SessionCommandDispatcher {
|
|
|
87
102
|
}
|
|
88
103
|
|
|
89
104
|
export type ExecutionInspection =
|
|
90
|
-
|
|
105
|
+
/**
|
|
106
|
+
* No verdict yet. `unreadable` separates "the work may still be in progress"
|
|
107
|
+
* from "this session's history could not be read at all": a reader failure is
|
|
108
|
+
* not progress, and the caller decides how long to keep waiting before it
|
|
109
|
+
* reports the execution as undeterminable instead of holding a card open
|
|
110
|
+
* forever.
|
|
111
|
+
*/
|
|
112
|
+
| { outcome: 'pending'; unreadable?: true; reason?: string }
|
|
91
113
|
| { outcome: 'succeeded' }
|
|
92
114
|
| { outcome: 'failed'; error: string }
|
|
93
115
|
| { outcome: 'cancelled'; error: string }
|
|
@@ -136,6 +158,13 @@ export interface PromptContext {
|
|
|
136
158
|
peers?: readonly PromptPeer[]
|
|
137
159
|
/** True when those members run as teammates inside this session (Team Lead). */
|
|
138
160
|
team?: boolean
|
|
161
|
+
/**
|
|
162
|
+
* Scheduled-run provenance: the instant the cron rule actually fired and the
|
|
163
|
+
* IANA zone its wall clock was read in. Present only for a cron-triggered
|
|
164
|
+
* run, so the model can resolve an unqualified date in the run's own terms
|
|
165
|
+
* instead of guessing from the Host clock (issue #1722).
|
|
166
|
+
*/
|
|
167
|
+
schedule?: { triggeredAt: number; timeZone: string; cron: string }
|
|
139
168
|
}
|
|
140
169
|
|
|
141
170
|
/**
|
|
@@ -147,13 +176,27 @@ function peerPromptPreamble(context: PromptContext): string | undefined {
|
|
|
147
176
|
const peers = context.peers ?? []
|
|
148
177
|
if (peers.length === 0) return undefined
|
|
149
178
|
if (context.team === true) {
|
|
150
|
-
const lines = peers.map(peer => `- ${escapeProvenanceDelimiter(peer.title)}(teammate: ${peer.name ?? teammateName(peer.title, peer.id)})`)
|
|
179
|
+
const lines = peers.map(peer => `- ${escapeProvenanceDelimiter(peer.title)}(teammate: ${peer.name ?? teammateName(peer.title, peer.id, peer.id)})`)
|
|
151
180
|
return `本任务是 Agent Team 的 Lead:本次运行不额外开启独立会话,以下 ${peers.length} 个子任务成员已在本会话中作为 teammate 启动。用 list_agents / send_message / wait_agent 协调它们,用任务看板工具读写它们在看板上的卡片:\n${lines.join('\n')}`
|
|
152
181
|
}
|
|
153
182
|
const lines = peers.map(peer => `- ${escapeProvenanceDelimiter(peer.title)}(任务 ${peer.id})`)
|
|
154
183
|
return `本次运行同时并发开启 ${peers.length} 个独立 DSH 会话执行下列子任务成员(可用 task_board_* 工具查看它们的进度):\n${lines.join('\n')}`
|
|
155
184
|
}
|
|
156
185
|
|
|
186
|
+
/**
|
|
187
|
+
* The scheduled-run section: this execution was triggered by the card's cron
|
|
188
|
+
* rule rather than by a person. It states the firing instant, the rule's zone,
|
|
189
|
+
* and the rule text, so the run's own clock is unambiguous — the board's
|
|
190
|
+
* scheduler fires on a wall clock in a specific IANA zone, which is not
|
|
191
|
+
* necessarily the zone of whatever session the run lands in.
|
|
192
|
+
*/
|
|
193
|
+
function schedulePromptPreamble(context: PromptContext): string | undefined {
|
|
194
|
+
const schedule = context.schedule
|
|
195
|
+
if (schedule === undefined) return undefined
|
|
196
|
+
const stamp = new Date(schedule.triggeredAt).toISOString()
|
|
197
|
+
return `本次执行由任务看板的定时规则自动触发:触发时间 ${stamp}(UTC),规则时区 ${schedule.timeZone},cron 表达式 ${schedule.cron}。需要判断「今天」「现在」或计算时间窗口时,以该时区的触发时间为准。`
|
|
198
|
+
}
|
|
199
|
+
|
|
157
200
|
/**
|
|
158
201
|
* Build the execution prompt for one task.
|
|
159
202
|
* @param task - the task being launched.
|
|
@@ -167,11 +210,14 @@ export function promptText(task: TaskRecord, context: PromptContext = {}): strin
|
|
|
167
210
|
? undefined
|
|
168
211
|
: `交接包引用(来自任务看板续接卡片,冻结于 ${new Date(handover.bundledAt).toISOString()}):\n${handover.references.map(reference => `- ${reference}`).join('\n')}`
|
|
169
212
|
// Tag prompts come first: they are the run's standing context (business line,
|
|
170
|
-
// output location), the handover preamble is a per-card note,
|
|
171
|
-
// body is the instruction
|
|
213
|
+
// output location), the handover preamble is a per-card note, the scheduled
|
|
214
|
+
// section states why this run exists, and the task body is the instruction.
|
|
172
215
|
const tagPreamble = tagPromptPreamble(task)
|
|
216
|
+
const handoverSection = handoverPreamble
|
|
217
|
+
const schedulePreamble = schedulePromptPreamble(context)
|
|
173
218
|
const runPreamble = peerPromptPreamble(context)
|
|
174
|
-
const preambles = [tagPreamble,
|
|
219
|
+
const preambles = [tagPreamble, handoverSection, schedulePreamble, runPreamble]
|
|
220
|
+
.filter((part): part is string => part !== undefined)
|
|
175
221
|
const preamble = preambles.length === 0 ? undefined : preambles.join('\n\n')
|
|
176
222
|
const freeze = task.freeze
|
|
177
223
|
if (freeze === undefined) {
|
|
@@ -182,6 +228,27 @@ export function promptText(task: TaskRecord, context: PromptContext = {}): strin
|
|
|
182
228
|
return preamble === undefined ? declaration : `${preamble}\n\n${declaration}`
|
|
183
229
|
}
|
|
184
230
|
|
|
231
|
+
/**
|
|
232
|
+
* The parts of an objective the built-in `/goal` command reads as an operation
|
|
233
|
+
* rather than as an objective: an exact `clear`/`pause`/`resume`/`edit`
|
|
234
|
+
* (case-insensitive) and a leading `edit <objective>`.
|
|
235
|
+
*/
|
|
236
|
+
const GOAL_COMMAND_KEYWORD = /^(?:clear|pause|resume|edit)$|^edit\s/i
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Build the objective the built-in `/goal` command receives. A prompt whose
|
|
240
|
+
* text would be read as a goal operation is prefixed with a neutral label: the
|
|
241
|
+
* instruction stays intact, and the command arms an objective instead of
|
|
242
|
+
* clearing, pausing, or editing an unrelated goal (or refusing for a missing
|
|
243
|
+
* one).
|
|
244
|
+
* @param prompt - the composed execution prompt.
|
|
245
|
+
* @returns the objective text for `/goal <objective>`.
|
|
246
|
+
*/
|
|
247
|
+
export function goalObjective(prompt: string): string {
|
|
248
|
+
const text = prompt.trim()
|
|
249
|
+
return GOAL_COMMAND_KEYWORD.test(text) ? `Goal: ${text}` : text
|
|
250
|
+
}
|
|
251
|
+
|
|
185
252
|
/**
|
|
186
253
|
* Build the tag section of the execution prompt (issue #1521). Only tags with
|
|
187
254
|
* a non-blank `promptPrefix` contribute; a task whose tags are all bare names
|
|
@@ -235,6 +302,8 @@ function nonCompletedTurnEnd(data: unknown): string | undefined {
|
|
|
235
302
|
*/
|
|
236
303
|
function invokeWireArgs(namespace: string, method: string, request: Record<string, unknown>): Record<string, unknown> {
|
|
237
304
|
if (namespace === 'agentPresets' && method === 'list') return {}
|
|
305
|
+
// session/modelCatalog declares zero parameters, so its args must be {}.
|
|
306
|
+
if (namespace === 'session' && method === 'modelCatalog') return {}
|
|
238
307
|
if (namespace === 'session' && method === 'list') return { _request: request }
|
|
239
308
|
return { request }
|
|
240
309
|
}
|
|
@@ -265,6 +334,18 @@ export class HostExecutionRunner {
|
|
|
265
334
|
return this.gateway.stream({ namespace, method, args: { request }, ...(signal === undefined ? {} : { signal }) })
|
|
266
335
|
}
|
|
267
336
|
|
|
337
|
+
/**
|
|
338
|
+
* The workspace an unpinned run lands in: the deployment's most recently used
|
|
339
|
+
* one. Undefined when the deployment serves no workspace registry or has
|
|
340
|
+
* registered none yet; the launch then keeps the "let the Host decide" shape,
|
|
341
|
+
* which is the only remaining path to the Host's own working directory.
|
|
342
|
+
* @returns the workspace id to create the session in, when one is known.
|
|
343
|
+
*/
|
|
344
|
+
private recentWorkspaceId(): string | undefined {
|
|
345
|
+
if (this.workspaceRegistry === undefined) return undefined
|
|
346
|
+
return mostRecentWorkspaceId(this.workspaceRegistry.list())
|
|
347
|
+
}
|
|
348
|
+
|
|
268
349
|
/**
|
|
269
350
|
* Launch one execution. Without `options.reuseSessionId` a fresh session is
|
|
270
351
|
* created, renamed, pinned, and prompted (the historical contract). With it,
|
|
@@ -275,10 +356,14 @@ export class HostExecutionRunner {
|
|
|
275
356
|
* @param options - optional session to continue in.
|
|
276
357
|
* @returns the session id the execution runs in.
|
|
277
358
|
*/
|
|
278
|
-
async launch(task: TaskRecord, options: { reuseSessionId?: string; promptContext?: PromptContext } = {}): Promise<string> {
|
|
359
|
+
async launch(task: TaskRecord, options: { reuseSessionId?: string; promptContext?: PromptContext; onSession?: (sessionId: string) => void; onGoalArmed?: (armed: boolean) => void } = {}): Promise<string> {
|
|
279
360
|
// A handover bundle overrides the legacy pin fields: the bundle is the
|
|
280
|
-
// authoritative execution triplet for a continuation card (issue #5).
|
|
281
|
-
|
|
361
|
+
// authoritative execution triplet for a continuation card (issue #5). A
|
|
362
|
+
// card that pins nothing is resolved to the deployment's most recently used
|
|
363
|
+
// workspace instead of being left to `session.create`, whose own fallback is
|
|
364
|
+
// the Host process working directory (the desktop app's profile directory) —
|
|
365
|
+
// never the project the user was in when the card was written.
|
|
366
|
+
const workspaceId = task.handover?.workspaceId ?? task.workspaceId ?? this.recentWorkspaceId()
|
|
282
367
|
const mode = task.handover?.mode ?? task.mode
|
|
283
368
|
const permission = task.handover?.permission ?? task.permission
|
|
284
369
|
|
|
@@ -306,8 +391,12 @@ export class HostExecutionRunner {
|
|
|
306
391
|
// whose session was composed from a different preset now fails closed
|
|
307
392
|
// instead of silently running under the wrong composition, matching
|
|
308
393
|
// the fresh branch's `agentPreset` assertion (issue #1708).
|
|
394
|
+
// Report the session before anything is queued into it, so the board
|
|
395
|
+
// can bind the execution first and a completion claim can never arrive
|
|
396
|
+
// at the gate without its binding.
|
|
397
|
+
options.onSession?.(reused)
|
|
309
398
|
await this.assertReusedPreset(reused, mode)
|
|
310
|
-
await this.pinAndPrompt(reused, task, permission, options.promptContext)
|
|
399
|
+
await this.pinAndPrompt(reused, task, permission, options.promptContext, options.onGoalArmed)
|
|
311
400
|
} catch (error) {
|
|
312
401
|
throw new SessionLaunchError(reused, error)
|
|
313
402
|
}
|
|
@@ -318,9 +407,10 @@ export class HostExecutionRunner {
|
|
|
318
407
|
...(mode === undefined ? {} : { agentPreset: mode }),
|
|
319
408
|
}) as { sessionId: ExecutionSessionId }
|
|
320
409
|
const sessionId = created.sessionId
|
|
410
|
+
options.onSession?.(sessionId)
|
|
321
411
|
try {
|
|
322
412
|
await this.invoke('session', 'rename', { sessionId, title: task.title })
|
|
323
|
-
await this.pinAndPrompt(sessionId, task, permission, options.promptContext)
|
|
413
|
+
await this.pinAndPrompt(sessionId, task, permission, options.promptContext, options.onGoalArmed)
|
|
324
414
|
} catch (error) {
|
|
325
415
|
throw new SessionLaunchError(sessionId, error)
|
|
326
416
|
}
|
|
@@ -365,6 +455,7 @@ export class HostExecutionRunner {
|
|
|
365
455
|
task: TaskRecord,
|
|
366
456
|
permission: TaskPermission | undefined,
|
|
367
457
|
context: PromptContext = {},
|
|
458
|
+
onGoalArmed?: (armed: boolean) => void,
|
|
368
459
|
): Promise<void> {
|
|
369
460
|
if (permission !== undefined) {
|
|
370
461
|
if (this.commands === undefined) throw new Error('permission command dispatcher is unavailable')
|
|
@@ -393,6 +484,103 @@ export class HostExecutionRunner {
|
|
|
393
484
|
mode: 'queue' as const,
|
|
394
485
|
content: [{ type: 'text' as const, text: promptText(task, context) }],
|
|
395
486
|
})
|
|
487
|
+
const armed = await this.armGoal(sessionId, task, context)
|
|
488
|
+
onGoalArmed?.(armed)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Read the host's model catalog: the default route an unconfigured session
|
|
493
|
+
* starts at, and each provider's models with the reasoning metadata its
|
|
494
|
+
* adapter exposes. The acceptance settings card and the frozen contract both
|
|
495
|
+
* resolve against it, so "inherit host" means the host's own configuration
|
|
496
|
+
* rather than a session's or a card's pinned model.
|
|
497
|
+
* @returns the raw catalog value, or undefined when this cohort serves none.
|
|
498
|
+
*/
|
|
499
|
+
async modelCatalog(): Promise<unknown> {
|
|
500
|
+
try {
|
|
501
|
+
return await this.invoke('session', 'modelCatalog', {})
|
|
502
|
+
} catch (error) {
|
|
503
|
+
console.warn('[dsh-task-board] session/modelCatalog failed; the acceptance settings fall back to the configured route', error)
|
|
504
|
+
return undefined
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Arm dsh's built-in goal on the session. The task prompt is queued FIRST, so
|
|
510
|
+
* the session's first turn carries the instruction verbatim; `/goal
|
|
511
|
+
* <objective>` then turns the same text into a persistent objective, and
|
|
512
|
+
* dsh's goal-round driver keeps starting continuation rounds until the agent
|
|
513
|
+
* marks it complete (see {@link goalVerdict} for how the run settles).
|
|
514
|
+
*
|
|
515
|
+
* Opt-out: a task whose `goalRun` is an explicit false runs one plain turn.
|
|
516
|
+
* A refusal (no command dispatcher, no `/goal` command in this cohort, an
|
|
517
|
+
* objective the command rejects) is reported and the run continues as that
|
|
518
|
+
* plain turn: the task was asked to run, and a missing goal mode must not
|
|
519
|
+
* lose the work.
|
|
520
|
+
*/
|
|
521
|
+
private async armGoal(sessionId: ExecutionSessionId, task: TaskRecord, context: PromptContext): Promise<boolean> {
|
|
522
|
+
if (task.goalRun === false) return false
|
|
523
|
+
if (this.commands === undefined) {
|
|
524
|
+
console.warn('[dsh-task-board] no command dispatcher is available; task ' + task.id + ' runs without /goal')
|
|
525
|
+
return false
|
|
526
|
+
}
|
|
527
|
+
try {
|
|
528
|
+
const command = await this.commands.execute(
|
|
529
|
+
sessionId,
|
|
530
|
+
'/goal ' + goalObjective(promptText(task, context)),
|
|
531
|
+
AbortSignal.timeout(30_000),
|
|
532
|
+
)
|
|
533
|
+
if (command === undefined) {
|
|
534
|
+
console.warn('[dsh-task-board] /goal was not acknowledged for session ' + sessionId + '; the run continues as a plain turn')
|
|
535
|
+
return false
|
|
536
|
+
}
|
|
537
|
+
if (command.kind !== 'success') {
|
|
538
|
+
console.warn('[dsh-task-board] /goal was refused for session ' + sessionId + ': ' + (command.text ?? 'no reason reported') + '; the run continues as a plain turn')
|
|
539
|
+
return false
|
|
540
|
+
}
|
|
541
|
+
return true
|
|
542
|
+
} catch (error) {
|
|
543
|
+
console.warn('[dsh-task-board] could not arm /goal on session ' + sessionId + '; the run continues as a plain turn', error)
|
|
544
|
+
return false
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* The verdict an armed dsh goal imposes on an otherwise completed turn.
|
|
550
|
+
*
|
|
551
|
+
* A goal run is one session working many automatic continuation rounds:
|
|
552
|
+
* dsh's goal-round driver queues the next round whenever the agent goes
|
|
553
|
+
* idle, so the first completed `turn/end` is not the execution's result.
|
|
554
|
+
* Settling there would leave the card in `done` while the session keeps
|
|
555
|
+
* working, and a scheduled task would fire a second, concurrent session.
|
|
556
|
+
* The session projection is the authority: an `active` goal keeps the
|
|
557
|
+
* execution pending, a `blocked` goal fails it with the recorded reason, and
|
|
558
|
+
* any other phase (complete, paused, or no goal at all) leaves the turn
|
|
559
|
+
* verdict in place.
|
|
560
|
+
*
|
|
561
|
+
* An unreadable projection falls back to that turn verdict: a cohort whose
|
|
562
|
+
* gateway withdrew `session/projections` must not hang every execution.
|
|
563
|
+
*/
|
|
564
|
+
private async goalVerdict(sessionId: string): Promise<ExecutionInspection | undefined> {
|
|
565
|
+
let projections: { values?: { goal?: unknown } } | null
|
|
566
|
+
try {
|
|
567
|
+
projections = await this.invoke('session', 'projections', { sessionId }) as { values?: { goal?: unknown } } | null
|
|
568
|
+
} catch (error) {
|
|
569
|
+
console.warn('[dsh-task-board] session/projections failed while reading the goal phase; settling from the turn result', error)
|
|
570
|
+
return undefined
|
|
571
|
+
}
|
|
572
|
+
const goal = projections?.values?.goal
|
|
573
|
+
if (goal === undefined || goal === null) return undefined
|
|
574
|
+
const view = (goal as { goal?: { phase?: unknown; blockedReason?: { message?: unknown } } }).goal
|
|
575
|
+
if (view?.phase === 'active') return { outcome: 'pending' }
|
|
576
|
+
if (view?.phase === 'blocked') {
|
|
577
|
+
const message = view.blockedReason?.message
|
|
578
|
+
return {
|
|
579
|
+
outcome: 'failed',
|
|
580
|
+
error: 'goal is blocked: ' + (typeof message === 'string' && message !== '' ? message : 'no reason recorded'),
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return undefined
|
|
396
584
|
}
|
|
397
585
|
|
|
398
586
|
async listRunning(): Promise<{ known: true; count: number; items: SessionSummary[] } | { known: false }> {
|
|
@@ -424,8 +612,17 @@ export class HostExecutionRunner {
|
|
|
424
612
|
}
|
|
425
613
|
}
|
|
426
614
|
|
|
427
|
-
/**
|
|
428
|
-
|
|
615
|
+
/**
|
|
616
|
+
* Resolve an execution outcome from the session list and bounded history pages.
|
|
617
|
+
* @param sessionId - the session the execution runs in.
|
|
618
|
+
* @param startedAt - instant the execution opened; earlier events are not its own.
|
|
619
|
+
* @param sessions - roster from the caller's own poll, when it already has one.
|
|
620
|
+
* @param options.whileRunning - read the first completed turn even while the
|
|
621
|
+
* roster reports the session as running. A durable Agent Teams teammate
|
|
622
|
+
* stays alive after its turn ends, so `running` never clears for it and the
|
|
623
|
+
* turn it completed is the only verdict it will ever expose.
|
|
624
|
+
*/
|
|
625
|
+
async inspect(sessionId: string, startedAt = 0, sessions?: readonly SessionSummary[], options: { whileRunning?: boolean } = {}): Promise<ExecutionInspection> {
|
|
429
626
|
let items: readonly SessionSummary[]
|
|
430
627
|
if (sessions !== undefined) {
|
|
431
628
|
items = sessions
|
|
@@ -455,7 +652,7 @@ export class HostExecutionRunner {
|
|
|
455
652
|
this.scanMemos.delete(sessionId)
|
|
456
653
|
return { outcome: 'cancelled', error: 'execution session no longer exists' }
|
|
457
654
|
}
|
|
458
|
-
if (summary.running) return { outcome: 'pending' }
|
|
655
|
+
if (summary.running && options.whileRunning !== true) return { outcome: 'pending' }
|
|
459
656
|
|
|
460
657
|
let opening: { cursor: number; records: readonly SessionHistoryRecord[]; hasMore: boolean }
|
|
461
658
|
try {
|
|
@@ -465,12 +662,12 @@ export class HostExecutionRunner {
|
|
|
465
662
|
if (typeof iterator.return === 'function') await iterator.return()
|
|
466
663
|
const follow = next.done === true ? undefined : next.value as { type?: string; cursor?: number; records?: readonly SessionHistoryRecord[]; hasMore?: boolean }
|
|
467
664
|
if (follow === undefined || follow.type !== 'snapshot' || typeof follow.cursor !== 'number' || follow.records === undefined || typeof follow.hasMore !== 'boolean') {
|
|
468
|
-
return { outcome: 'pending' }
|
|
665
|
+
return { outcome: 'pending', unreadable: true, reason: 'session/follow returned no opening frame' }
|
|
469
666
|
}
|
|
470
667
|
opening = { cursor: follow.cursor, records: follow.records, hasMore: follow.hasMore }
|
|
471
668
|
} catch (error) {
|
|
472
669
|
console.warn('[dsh-task-board] session/follow failed during execution inspection; keeping the outcome pending', error)
|
|
473
|
-
return { outcome: 'pending' }
|
|
670
|
+
return { outcome: 'pending', unreadable: true, reason: 'session/follow failed: ' + failureText(error) }
|
|
474
671
|
}
|
|
475
672
|
const openingEvents = opening.records.map(record => ({ event: recordEvent(record) }))
|
|
476
673
|
const newestSeq = openingEvents.reduce<number | undefined>((newest, entry) => newest === undefined ? entry.event.seq : Math.max(newest, entry.event.seq), undefined)
|
|
@@ -478,7 +675,7 @@ export class HostExecutionRunner {
|
|
|
478
675
|
const events: Array<{ event: { type: string; seq: number; time: number; data: unknown } }> = [...openingEvents]
|
|
479
676
|
let beforeSeq: number | undefined
|
|
480
677
|
let reachedExecutionBoundary = !opening.hasMore
|
|
481
|
-
for (let page = 0; page <
|
|
678
|
+
for (let page = 0; page < HISTORY_PAGE_LIMIT && !reachedExecutionBoundary; page += 1) {
|
|
482
679
|
let history: SessionPage
|
|
483
680
|
try {
|
|
484
681
|
history = await this.invoke('session', 'page', {
|
|
@@ -489,7 +686,7 @@ export class HostExecutionRunner {
|
|
|
489
686
|
}) as SessionPage
|
|
490
687
|
} catch (error) {
|
|
491
688
|
console.warn('[dsh-task-board] session/page failed during execution inspection; keeping the outcome pending', error)
|
|
492
|
-
return { outcome: 'pending' }
|
|
689
|
+
return { outcome: 'pending', unreadable: true, reason: 'session/page failed: ' + failureText(error) }
|
|
493
690
|
}
|
|
494
691
|
const pageEntries = pageEvents(history)
|
|
495
692
|
events.push(...pageEntries)
|
|
@@ -502,7 +699,9 @@ export class HostExecutionRunner {
|
|
|
502
699
|
if (oldestSeq === undefined || oldestSeq === beforeSeq) return { outcome: 'pending' }
|
|
503
700
|
beforeSeq = oldestSeq
|
|
504
701
|
}
|
|
505
|
-
if (!reachedExecutionBoundary)
|
|
702
|
+
if (!reachedExecutionBoundary) {
|
|
703
|
+
return { outcome: 'pending', unreadable: true, reason: 'history scan did not reach the execution start within ' + HISTORY_PAGE_LIMIT + ' pages' }
|
|
704
|
+
}
|
|
506
705
|
const turnEnd = events
|
|
507
706
|
.filter(entry => entry.event.type === 'turn/end' && (startedAt <= 0 || entry.event.time >= startedAt))
|
|
508
707
|
.sort((a, b) => a.event.seq - b.event.seq)[0]
|
|
@@ -512,7 +711,7 @@ export class HostExecutionRunner {
|
|
|
512
711
|
}
|
|
513
712
|
this.scanMemos.delete(sessionId)
|
|
514
713
|
const reason = nonCompletedTurnEnd(turnEnd.event.data)
|
|
515
|
-
if (reason === undefined) return { outcome: 'succeeded' }
|
|
714
|
+
if (reason === undefined) return await this.goalVerdict(sessionId) ?? { outcome: 'succeeded' }
|
|
516
715
|
// `error` keeps its historical wording; every other non-completed reason
|
|
517
716
|
// (aborted / blocked / max-tokens / interrupted) now reports instead of
|
|
518
717
|
// silently counting as success.
|