@linxin666/dsh-client-ui-task-board 0.4.5 → 0.4.7
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 +34 -18
- package/README.zh.md +34 -18
- package/lib/client.js +2804 -1024
- package/lib/client.js.map +1 -1
- package/lib/index.js +2576 -416
- package/lib/types/client/CardDefaultPresetField.d.ts +35 -0
- package/lib/types/client/TaskBoardSettingsCard.d.ts +61 -0
- package/lib/types/client/board/ReasoningEffortField.d.ts +25 -0
- package/lib/types/client/board/model-options.d.ts +67 -0
- package/lib/types/client/board/preset-label.d.ts +21 -0
- package/lib/types/client/board/preset-roster.d.ts +45 -0
- package/lib/types/client/board/team-run-promotion.d.ts +26 -0
- package/lib/types/client/index.d.ts +4 -5
- package/lib/types/client/locales.d.ts +81 -0
- package/lib/types/client/plugin-card-seat.d.ts +5 -0
- package/lib/types/client/schedule-zone.d.ts +50 -0
- package/lib/types/core/card-defaults.d.ts +78 -0
- package/lib/types/core/controller.d.ts +41 -17
- package/lib/types/core/launch-width.d.ts +31 -0
- package/lib/types/core/poll-cadence.d.ts +31 -0
- package/lib/types/core/reasoning-effort.d.ts +57 -0
- package/lib/types/core/subtask.d.ts +30 -8
- package/lib/types/core/tasks.d.ts +163 -14
- package/lib/types/core/use-cases/task-create.d.ts +10 -5
- package/lib/types/core/use-cases/task-schedule.d.ts +68 -10
- package/lib/types/core/use-cases/task-update.d.ts +1 -1
- package/lib/types/core/verification-budget.d.ts +52 -0
- package/lib/types/core/verification.d.ts +376 -9
- package/lib/types/host/launch-admission.d.ts +72 -0
- package/lib/types/host/verification-gate.d.ts +24 -0
- package/lib/types/host/verification-runner.d.ts +38 -7
- package/lib/types/host-ledger.d.ts +91 -11
- package/lib/types/host-runner.d.ts +111 -13
- package/lib/types/host-service.d.ts +199 -8
- package/lib/types/index.d.ts +141 -0
- package/lib/types/protocol.d.ts +63 -12
- package/package.json +29 -33
- package/src/client/CardDefaultPresetField.tsx +101 -0
- package/src/client/TaskBoardSettingsCard.tsx +288 -17
- package/src/client/board/LinkSubtaskModal.tsx +8 -0
- package/src/client/board/NewTaskModal.tsx +237 -60
- package/src/client/board/ReasoningEffortField.tsx +50 -0
- package/src/client/board/TaskCard.tsx +3 -3
- package/src/client/board/TaskDetail.tsx +261 -24
- package/src/client/board/VerificationReport.tsx +132 -13
- package/src/client/board/model-options.ts +178 -0
- package/src/client/board/preset-label.ts +32 -0
- package/src/client/board/preset-roster.ts +58 -0
- package/src/client/board/status-key.ts +3 -0
- package/src/client/board/team-run-promotion.ts +39 -0
- package/src/client/index.ts +105 -108
- package/src/client/locales.ts +171 -6
- package/src/client/plugin-card-seat.ts +7 -0
- package/src/client/schedule-zone.ts +100 -2
- package/src/client/settings-entry-form.ts +0 -1
- package/src/core/card-defaults.ts +132 -0
- package/src/core/controller.ts +55 -16
- package/src/core/launch-width.ts +38 -0
- package/src/core/poll-cadence.ts +41 -0
- package/src/core/reasoning-effort.ts +92 -0
- package/src/core/store.ts +69 -15
- package/src/core/subtask.ts +48 -13
- package/src/core/tasks.ts +213 -39
- package/src/core/use-cases/task-create.ts +57 -25
- package/src/core/use-cases/task-schedule.ts +190 -25
- package/src/core/use-cases/task-update.ts +9 -4
- package/src/core/verification-budget.ts +115 -0
- package/src/core/verification.ts +785 -17
- package/src/host/agent-tools.ts +127 -29
- package/src/host/launch-admission.ts +140 -0
- package/src/host/verification-gate.ts +172 -16
- package/src/host/verification-runner.ts +207 -40
- package/src/host-ledger.ts +346 -48
- package/src/host-runner.ts +234 -35
- package/src/host-service.ts +539 -52
- package/src/index.ts +250 -7
- package/src/protocol.ts +160 -32
- package/lib/invariant.js +0 -6
- package/lib/types/client/board-card-seat.d.ts +0 -34
- package/lib/types/invariant.d.ts +0 -3
- package/src/client/board-card-seat.ts +0 -102
- package/src/invariant.ts +0 -4
package/src/host/agent-tools.ts
CHANGED
|
@@ -23,11 +23,13 @@ import {
|
|
|
23
23
|
TASK_PERMISSIONS,
|
|
24
24
|
type ExecutionRecord,
|
|
25
25
|
type NewTaskInput,
|
|
26
|
+
type ScheduleMode,
|
|
26
27
|
type ScheduleRule,
|
|
27
28
|
type TaskPermission,
|
|
28
29
|
type TaskRecord,
|
|
29
30
|
type TaskTag,
|
|
30
31
|
} from '../core/tasks.ts'
|
|
32
|
+
import type { SetSchedulePatch } from '../core/use-cases/task-schedule.ts'
|
|
31
33
|
import type { TaskUpdatePatch } from '../core/use-cases/task-update.ts'
|
|
32
34
|
import type { TaskBoardAction, TaskBoardSnapshot } from '../protocol.ts'
|
|
33
35
|
|
|
@@ -93,11 +95,22 @@ export function callingSessionId(exec: { agent?: unknown }): string | undefined
|
|
|
93
95
|
function scheduleView(schedule: ScheduleRule): Record<string, unknown> {
|
|
94
96
|
return {
|
|
95
97
|
enabled: schedule.enabled,
|
|
96
|
-
|
|
98
|
+
mode: schedule.mode,
|
|
99
|
+
// A one-shot has no cron, and carries its planned instant instead.
|
|
100
|
+
...(schedule.cron === undefined ? {} : { cron: schedule.cron }),
|
|
101
|
+
...(schedule.at === undefined ? {} : { at: schedule.at }),
|
|
97
102
|
// Absent means the rule follows the Host zone, which the board view reports.
|
|
98
103
|
...(schedule.timeZone === undefined ? {} : { timeZone: schedule.timeZone }),
|
|
99
104
|
...(schedule.nextRunAt === undefined ? {} : { nextRunAt: schedule.nextRunAt }),
|
|
100
105
|
...(schedule.lastTriggeredAt === undefined ? {} : { lastTriggeredAt: schedule.lastTriggeredAt }),
|
|
106
|
+
// The run budget: what the scheduler has already opened, its cap (absent =
|
|
107
|
+
// unlimited), and how the rule stopped or what it last skipped.
|
|
108
|
+
runCount: schedule.runCount,
|
|
109
|
+
...(schedule.maxRuns === undefined ? {} : { maxRuns: schedule.maxRuns }),
|
|
110
|
+
...(schedule.endedAt === undefined ? {} : { endedAt: schedule.endedAt }),
|
|
111
|
+
...(schedule.endedReason === undefined ? {} : { endedReason: schedule.endedReason }),
|
|
112
|
+
...(schedule.skippedAt === undefined ? {} : { skippedAt: schedule.skippedAt }),
|
|
113
|
+
...(schedule.skippedReason === undefined ? {} : { skippedReason: schedule.skippedReason }),
|
|
101
114
|
}
|
|
102
115
|
}
|
|
103
116
|
|
|
@@ -114,8 +127,11 @@ function executionView(execution: ExecutionRecord): Record<string, unknown> {
|
|
|
114
127
|
...(execution.result === undefined ? {} : { result: execution.result }),
|
|
115
128
|
...(execution.error === undefined ? {} : { error: execution.error }),
|
|
116
129
|
// The session that asked for the run (audit only; client-asserted by the
|
|
117
|
-
// browser, the calling session here).
|
|
130
|
+
// browser, the calling session here). On an external record it names the
|
|
131
|
+
// outside agent instead, which is what distinguishes it in the history.
|
|
118
132
|
...(execution.initiatedBy === undefined ? {} : { initiatedBy: execution.initiatedBy }),
|
|
133
|
+
// Set only by record-external-outcome: the Host observed no session for it.
|
|
134
|
+
...(execution.external === true ? { external: true } : {}),
|
|
119
135
|
...(execution.runGroupId === undefined ? {} : { cascade: true }),
|
|
120
136
|
// A deferred cascade parent has settled its own turn but still waits for
|
|
121
137
|
// its subtasks; that is not a stuck card and the model must not retry it.
|
|
@@ -150,10 +166,11 @@ function taskSummary(
|
|
|
150
166
|
...(task.mode === undefined ? {} : { mode: task.mode }),
|
|
151
167
|
...(task.permission === undefined ? {} : { permission: task.permission }),
|
|
152
168
|
...(task.model === undefined ? {} : { model: task.model }),
|
|
169
|
+
...(task.reasoningEffort === undefined ? {} : { reasoningEffort: task.reasoningEffort }),
|
|
153
170
|
...(task.reuseSession === true ? { reuseSession: true } : {}),
|
|
154
171
|
...(task.teamRun === true ? { teamRun: true } : {}),
|
|
155
|
-
// Default-
|
|
156
|
-
...(task.goalRun ===
|
|
172
|
+
// Default-OFF opt-in: only a card that asked for the goal run reports it.
|
|
173
|
+
...(task.goalRun === true ? { goalRun: true } : {}),
|
|
157
174
|
// Default-OFF opt-out: the deviation is what a reader must be told.
|
|
158
175
|
...(task.skipVerification === true ? { skipVerification: true } : {}),
|
|
159
176
|
...(task.schedule === undefined ? {} : { schedule: scheduleView(task.schedule) }),
|
|
@@ -261,7 +278,7 @@ function buildSetParentTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
261
278
|
'Attach an existing task under a parent (making it a subtask) or detach it back to the root board by setting its parentId.',
|
|
262
279
|
'Pass an empty parentId to detach.',
|
|
263
280
|
'The Host refuses the link when the parent is unknown or archived, when the moved task is one of its own descendants, when the resulting depth exceeds the deployment subtask limit (default one level, maximum three), or while the task has an unsettled execution (its link may still be a running cascade parent pointer).',
|
|
264
|
-
'Running a task
|
|
281
|
+
'Running a task opens one run group over its subtree concurrently and leaves a subtask that already sits in the done column alone; a parent card settles only after its own turn and every direct subtask it opened have settled.',
|
|
265
282
|
].join(' '),
|
|
266
283
|
parameters: {
|
|
267
284
|
taskId: { type: 'string', required: true, description: 'The task whose parent link changes.' },
|
|
@@ -297,7 +314,7 @@ function buildRunTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
297
314
|
'Executions consume real API quota. Sessions are created asynchronously, so read their ids and outcomes back with task_board_get.',
|
|
298
315
|
].join(' '),
|
|
299
316
|
parameters: {
|
|
300
|
-
taskId: { type: 'string', required: true, description: 'The task to run; running a root task runs its whole subtree.' },
|
|
317
|
+
taskId: { type: 'string', required: true, description: 'The task to run; running a root task runs its whole subtree, minus the subtasks that are already in the done column.' },
|
|
301
318
|
rerun: { type: 'boolean', description: 'Re-run a settled task: reset its column to todo before starting (default false).' },
|
|
302
319
|
},
|
|
303
320
|
output: { schema: { type: 'json' }, render: renderJson },
|
|
@@ -341,7 +358,9 @@ function buildManageTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
341
358
|
'Move, archive, restore, delete, or settle one task board card.',
|
|
342
359
|
'move-backlog, move-todo, move-running, move-done and move-failed are the manual column moves and cover every column; a card the runner is executing (one with an open execution) cannot be moved.',
|
|
343
360
|
'A manual move writes the card column only and never fabricates an execution record, so the card reports a declaration rather than evidence of a run: done/failed declare work finished (or failed) outside a Host-run execution — human work, an external system, a decision made elsewhere — and running says the work is under way without a tracked session. Use task_board_run when the work should actually run in a session here.',
|
|
361
|
+
'record-external-outcome is the record-backed version of move-done and move-failed: use it when YOU, or another agent outside this Host, actually finished or failed the card with your own model, so the terminal column and the run history both derive from one execution record. It takes result (succeeded or failed) and initiatedBy (who completed it, for example your own agent name) plus an optional summary, and it is refused while the card still has an open execution: the Host stays the only authority over a run it is executing. It cannot record running or cancelled — those remain Host-only.',
|
|
344
362
|
'settle force-closes the open execution of a card the board can no longer observe (a stuck running card) and records it cancelled with the caller as the reason, so the card returns to the todo column and can be run again.',
|
|
363
|
+
'reset-verification clears the acceptance anomalies (timeouts, authentication failures, an unresolvable judge route, or an attempt the time budget ended) recorded on a card open execution, so the next update_goal(action: complete) runs a fresh acceptance. It is refused unless that execution really carries such anomalies, it never clears a quality verdict, and a spent anomaly budget otherwise HOLDS the card open instead of failing it: tell the user the acceptance environment is unusable and that this action is how they clear it once they have fixed it.',
|
|
345
364
|
'archive takes the whole subtask tree off the board and is refused while any member has an unsettled execution; restore brings the task, its ancestors and its subtree back; delete removes one card and is refused while it still has subtasks (detach or delete them first) or while it runs.',
|
|
346
365
|
'It cannot confirm a permission binding: the confirmation gate is a human act performed in the board UI.',
|
|
347
366
|
].join(' '),
|
|
@@ -350,13 +369,59 @@ function buildManageTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
350
369
|
action: {
|
|
351
370
|
type: 'string',
|
|
352
371
|
required: true,
|
|
353
|
-
enum: ['move-todo', 'move-backlog', 'move-running', 'move-done', 'move-failed', 'archive', 'restore', 'delete', 'settle'],
|
|
372
|
+
enum: ['move-todo', 'move-backlog', 'move-running', 'move-done', 'move-failed', 'archive', 'restore', 'delete', 'settle', 'reset-verification', 'record-external-outcome'],
|
|
354
373
|
description: 'The lifecycle operation to perform.',
|
|
355
374
|
},
|
|
375
|
+
// Only record-external-outcome reads these two; every other action
|
|
376
|
+
// ignores them, so the model can pass them without a second tool call.
|
|
377
|
+
result: {
|
|
378
|
+
type: 'string',
|
|
379
|
+
enum: ['succeeded', 'failed'],
|
|
380
|
+
description: 'record-external-outcome only: the verdict the outside agent actually reached.',
|
|
381
|
+
},
|
|
382
|
+
initiatedBy: {
|
|
383
|
+
type: 'string',
|
|
384
|
+
description: 'record-external-outcome only: who finished the work outside this Host (for example your own agent name).',
|
|
385
|
+
},
|
|
386
|
+
summary: {
|
|
387
|
+
type: 'string',
|
|
388
|
+
description: 'record-external-outcome only: optional free-text note stored with the recorded outcome.',
|
|
389
|
+
},
|
|
356
390
|
},
|
|
357
391
|
output: { schema: { type: 'json' }, render: renderJson },
|
|
358
392
|
async execute(args, exec) {
|
|
359
393
|
const initiator = callingSessionId(exec)
|
|
394
|
+
if (args.action === 'record-external-outcome') {
|
|
395
|
+
const result = args.result
|
|
396
|
+
const by = typeof args.initiatedBy === 'string' ? args.initiatedBy.trim() : ''
|
|
397
|
+
if (result !== 'succeeded' && result !== 'failed') {
|
|
398
|
+
return refused('missing-result', 'record-external-outcome needs result=succeeded or result=failed')
|
|
399
|
+
}
|
|
400
|
+
if (by === '') {
|
|
401
|
+
return refused('missing-initiator', 'record-external-outcome needs initiatedBy naming who completed the work outside this Host')
|
|
402
|
+
}
|
|
403
|
+
if (!host.snapshot().tasks.some(task => task.id === args.taskId)) {
|
|
404
|
+
return refused('task-not-found', 'no task with id ' + args.taskId)
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
const snapshot = await host.apply(crypto.randomUUID(), {
|
|
408
|
+
kind: 'record-external-outcome',
|
|
409
|
+
taskId: args.taskId,
|
|
410
|
+
result,
|
|
411
|
+
initiatedBy: by,
|
|
412
|
+
...(typeof args.summary === 'string' && args.summary.trim() !== '' ? { summary: args.summary } : {}),
|
|
413
|
+
}, initiator)
|
|
414
|
+
const task = snapshot.tasks.find(item => item.id === args.taskId)
|
|
415
|
+
return json({
|
|
416
|
+
ok: true,
|
|
417
|
+
action: args.action,
|
|
418
|
+
...(task === undefined ? {} : { task: taskSummary(task, snapshot.tasks, snapshot.sessionDefaultPermission) }),
|
|
419
|
+
note: 'The outcome is recorded as its own execution, so the terminal column and the run history agree. Read it back with task_board_get.',
|
|
420
|
+
})
|
|
421
|
+
} catch (error) {
|
|
422
|
+
return refused('refused', messageOf(error))
|
|
423
|
+
}
|
|
424
|
+
}
|
|
360
425
|
const actions: Record<string, TaskBoardAction> = {
|
|
361
426
|
'move-todo': { kind: 'move', taskId: args.taskId, status: 'todo' },
|
|
362
427
|
'move-backlog': { kind: 'move', taskId: args.taskId, status: 'backlog' },
|
|
@@ -365,6 +430,7 @@ function buildManageTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
365
430
|
'move-failed': { kind: 'move', taskId: args.taskId, status: 'failed' },
|
|
366
431
|
archive: { kind: 'archive', taskId: args.taskId },
|
|
367
432
|
settle: { kind: 'settle', taskId: args.taskId },
|
|
433
|
+
'reset-verification': { kind: 'reset-verification', taskId: args.taskId },
|
|
368
434
|
restore: { kind: 'restore', taskId: args.taskId },
|
|
369
435
|
delete: { kind: 'delete', taskId: args.taskId },
|
|
370
436
|
}
|
|
@@ -390,37 +456,46 @@ function buildScheduleTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
390
456
|
return defineTool({
|
|
391
457
|
name: 'task_board_schedule',
|
|
392
458
|
description: [
|
|
393
|
-
'Arm, change, or disarm a task scheduled runs
|
|
394
|
-
'
|
|
395
|
-
'
|
|
396
|
-
'A
|
|
397
|
-
'
|
|
459
|
+
'Arm, change, or disarm a task scheduled runs. Two kinds of plan exist: a recurring 5-field cron rule (day-of-month and day-of-week follow Vixie semantics: both restricted means OR, otherwise AND), and a one-shot that runs once at a future instant.',
|
|
460
|
+
'For a one-shot pass mode "once" with at (the planned instant, ms since epoch UTC). For a recurring rule pass mode "cron" (or omit mode) with a cron expression, optionally capped by maxRuns (a positive whole number of scheduled executions; omit for unlimited, pass 0 to clear a cap).',
|
|
461
|
+
'The wall clock is read in the rule stored IANA time zone; omit timeZone to use the Host zone reported by task_board_list/task_board_get. DST gaps are skipped and an ambiguous fall-back time fires once, at the earlier instant.',
|
|
462
|
+
'A due scheduled task runs the same cascade a manual run does, so a root task runs its whole subtask tree minus the subtasks already in the done column.',
|
|
463
|
+
'The scheduler counts executions it actually starts; once a rule reaches its run budget it stops itself in the ledger, and a manual run never consumes the budget. A schedule whose tree contains an unconfirmed above-default permission is refused and does not consume the budget; the reason is reported in the list board summary as schedulerError.',
|
|
464
|
+
'Missed occurrences during Host downtime are skipped, never queued (a one-shot whose instant passed while the Host was down is stopped as missed); a task that is already running skips its occurrence without consuming the budget.',
|
|
398
465
|
].join(' '),
|
|
399
466
|
parameters: {
|
|
400
467
|
taskId: { type: 'string', required: true, description: 'The task whose schedule changes.' },
|
|
401
468
|
enabled: { type: 'boolean', description: 'Arm (true) or disarm (false) the schedule.' },
|
|
402
|
-
|
|
469
|
+
mode: { type: 'string', enum: ['cron', 'once'], description: 'The kind of plan: "cron" for a recurring rule, "once" for a single future instant. Omit to keep the stored kind.' },
|
|
470
|
+
cron: { type: 'string', description: '5-field cron expression of a recurring rule: minute hour day-of-month month day-of-week.' },
|
|
471
|
+
at: { type: 'integer', description: 'Planned instant of a one-shot, in milliseconds since the Unix epoch (UTC). Must be in the future.' },
|
|
403
472
|
timeZone: {
|
|
404
473
|
type: 'string',
|
|
405
|
-
description: 'IANA zone the
|
|
474
|
+
description: 'IANA zone the wall clock is read in (for example Asia/Shanghai). Omit to keep the stored zone, or pass an empty string to clear it back to the Host zone.',
|
|
406
475
|
},
|
|
476
|
+
maxRuns: { type: 'integer', description: 'Maximum scheduled executions of a recurring rule (positive whole number). Omit to keep the stored cap; pass 0 to clear it back to unlimited.' },
|
|
407
477
|
},
|
|
408
478
|
output: { schema: { type: 'json' }, render: renderJson },
|
|
409
479
|
async execute(args, exec) {
|
|
410
|
-
if (args.enabled === undefined && args.cron === undefined && args.
|
|
411
|
-
|
|
480
|
+
if (args.enabled === undefined && args.mode === undefined && args.cron === undefined && args.at === undefined
|
|
481
|
+
&& args.timeZone === undefined && args.maxRuns === undefined) {
|
|
482
|
+
return refused('nothing-to-change', 'pass enabled, mode, cron, at, timeZone and/or maxRuns')
|
|
412
483
|
}
|
|
413
484
|
// A missing card must read as a missing card: the ledger reports an
|
|
414
485
|
// unknown id through the same refusal as a malformed cron.
|
|
415
486
|
if (!host.snapshot().tasks.some(task => task.id === args.taskId)) {
|
|
416
487
|
return refused('task-not-found', 'no task with id ' + args.taskId)
|
|
417
488
|
}
|
|
418
|
-
const patch:
|
|
489
|
+
const patch: SetSchedulePatch = {
|
|
419
490
|
...(args.enabled === undefined ? {} : { enabled: args.enabled }),
|
|
491
|
+
...(args.mode === undefined ? {} : { mode: args.mode as ScheduleMode }),
|
|
420
492
|
...(args.cron === undefined ? {} : { cron: args.cron }),
|
|
493
|
+
...(args.at === undefined ? {} : { at: args.at }),
|
|
421
494
|
// An empty string is the documented way to clear the stored zone, so
|
|
422
495
|
// the model never has to emit an explicit null through JSON args.
|
|
423
496
|
...(args.timeZone === undefined ? {} : { timeZone: args.timeZone === '' ? null : args.timeZone }),
|
|
497
|
+
// 0 is the documented way to clear a run cap back to unlimited.
|
|
498
|
+
...(args.maxRuns === undefined ? {} : { maxRuns: args.maxRuns <= 0 ? null : args.maxRuns }),
|
|
424
499
|
}
|
|
425
500
|
try {
|
|
426
501
|
const snapshot = await host.apply(crypto.randomUUID(), { kind: 'set-schedule', taskId: args.taskId, patch }, callingSessionId(exec))
|
|
@@ -512,7 +587,7 @@ function buildCreateTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
512
587
|
name: 'task_board_create',
|
|
513
588
|
description: [
|
|
514
589
|
'Create a task on the DSH task board, or a subtask by passing parentId.',
|
|
515
|
-
'A subtask inherits every execution target it does not override (workspace, agent preset, permission, model) from its parent, and inherits the parent human permission confirmation together with an inherited binding.',
|
|
590
|
+
'A subtask inherits every execution target it does not override (workspace, agent preset, permission, model, reasoning effort) from its parent, and inherits the parent human permission confirmation together with an inherited binding.',
|
|
516
591
|
'The deployment caps subtask depth (default one level, maximum three): a subtask of a subtask is refused unless the board is configured deeper.',
|
|
517
592
|
'The task lands in the todo column and does NOT run; call task_board_run to execute it.',
|
|
518
593
|
].join(' '),
|
|
@@ -522,12 +597,13 @@ function buildCreateTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
522
597
|
prompt: { type: 'string', description: 'The instruction sent to the execution agent; the title is used when blank.' },
|
|
523
598
|
parentId: { type: 'string', description: 'Parent task id, making this a subtask. Omit for a root task.' },
|
|
524
599
|
workspaceId: { type: 'string', description: 'Workspace id the execution must run in; omit to inherit the workspace this session is in (root task) or the parent value (subtask).' },
|
|
525
|
-
mode: { type: 'string', description: 'Agent preset id the execution session is composed from; omit
|
|
526
|
-
permission: { type: 'string', enum: [...TASK_PERMISSIONS], description: 'Permission preset for the execution session
|
|
527
|
-
model: { type: 'string', description: 'Pinned model as provider/model (or a model id); omit
|
|
600
|
+
mode: { type: 'string', description: 'Agent preset id the execution session is composed from; omit to adopt the board new-card default, and the deployment default when none is configured.' },
|
|
601
|
+
permission: { type: 'string', enum: [...TASK_PERMISSIONS], description: 'Permission preset for the execution session; omit to adopt the board new-card default. An omitted permission is never taken from a parent: the launch resolves the parent binding instead. A value above the session default needs a human confirmation in the board UI before the card can run.' },
|
|
602
|
+
model: { type: 'string', description: 'Pinned model as provider/model (or a model id); omit to adopt the board new-card default, and the host default when none is configured.' },
|
|
603
|
+
reasoningEffort: { type: 'string', description: 'Reasoning effort id for that model (an adapter-owned level such as "high"); omit to adopt the board new-card default for that model, and the model own default when none applies. Only an effort the target model declares is sent; an empty string clears it back to the default.' },
|
|
528
604
|
reuseSession: { type: 'boolean', description: 'Continue later runs in the previous execution session instead of a fresh conversation.' },
|
|
529
605
|
teamRun: { type: 'boolean', description: 'Run this task as an Agent Team: running it starts one Team Lead session and the Host spawns a teammate per subtask inside it. Omit or false for one independent session per member. Refused when the deployment serves no Agent Teams service.' },
|
|
530
|
-
goalRun: { type: 'boolean', description: 'Start each run with dsh built-in /goal so the session keeps working continuation rounds until the goal completes.
|
|
606
|
+
goalRun: { type: 'boolean', description: 'Start each run with dsh built-in /goal so the session keeps working continuation rounds until the goal completes. Off by default: omit (or pass false) for a single plain turn, and pass true to opt this card in. It only takes effect while the deployment global native /goal switch is on.' },
|
|
531
607
|
skipVerification: { type: 'boolean', description: 'Run this task without the board acceptance gate: update_goal may mark the goal complete without a passing acceptance, and the execution record is marked skipped. Default false (omit to inherit the board-wide acceptance switch). Only affects executions started afterwards.' },
|
|
532
608
|
tags: {
|
|
533
609
|
type: 'array',
|
|
@@ -544,10 +620,13 @@ function buildCreateTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
544
620
|
schedule: {
|
|
545
621
|
type: 'object',
|
|
546
622
|
additionalProperties: false,
|
|
547
|
-
description: 'Arm a
|
|
623
|
+
description: 'Arm a schedule at creation time. Use mode "once" with at for a single future run, or mode "cron" (or omit mode) with a 5-field cron expression, optionally capped by maxRuns.',
|
|
548
624
|
properties: {
|
|
549
625
|
enabled: { type: 'boolean', required: true, description: 'Whether the schedule is armed.' },
|
|
550
|
-
|
|
626
|
+
mode: { type: 'string', enum: ['cron', 'once'], description: 'The kind of plan: "cron" for a recurring rule, "once" for a single future instant.' },
|
|
627
|
+
cron: { type: 'string', description: '5-field cron of a recurring rule: minute hour day-of-month month day-of-week.' },
|
|
628
|
+
at: { type: 'integer', description: 'Planned instant of a one-shot, in milliseconds since the Unix epoch (UTC). Must be in the future.' },
|
|
629
|
+
maxRuns: { type: 'integer', description: 'Maximum scheduled executions of a recurring rule (positive whole number); omit for unlimited.' },
|
|
551
630
|
},
|
|
552
631
|
},
|
|
553
632
|
},
|
|
@@ -568,12 +647,26 @@ function buildCreateTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
568
647
|
...(args.mode === undefined || args.mode === '' ? {} : { mode: args.mode }),
|
|
569
648
|
...(args.permission === undefined ? {} : { permission: args.permission }),
|
|
570
649
|
...(args.model === undefined || args.model === '' ? {} : { model: args.model }),
|
|
650
|
+
...(args.reasoningEffort === undefined || args.reasoningEffort === '' ? {} : { reasoningEffort: args.reasoningEffort }),
|
|
651
|
+
// This tool creates cards on behalf of a deployment that configured its
|
|
652
|
+
// new-card defaults once: an omitted target adopts them instead of
|
|
653
|
+
// leaving the card to resolve the host default at run time. Only the
|
|
654
|
+
// targets the caller omitted are filled.
|
|
655
|
+
applyCardDefaults: true,
|
|
571
656
|
...(args.reuseSession === true ? { reuseSession: true } : {}),
|
|
572
657
|
...(args.teamRun === true ? { teamRun: true } : {}),
|
|
573
|
-
...(args.goalRun ===
|
|
658
|
+
...(args.goalRun === true ? { goalRun: true } : {}),
|
|
574
659
|
...(args.skipVerification === true ? { skipVerification: true } : {}),
|
|
575
660
|
...(tags.length === 0 ? {} : { tags }),
|
|
576
|
-
...(args.schedule === undefined ? {} : {
|
|
661
|
+
...(args.schedule === undefined ? {} : {
|
|
662
|
+
schedule: {
|
|
663
|
+
enabled: args.schedule.enabled,
|
|
664
|
+
...(args.schedule.mode === undefined ? {} : { mode: args.schedule.mode as ScheduleMode }),
|
|
665
|
+
...(args.schedule.cron === undefined ? {} : { cron: args.schedule.cron }),
|
|
666
|
+
...(args.schedule.at === undefined ? {} : { at: args.schedule.at }),
|
|
667
|
+
...(args.schedule.maxRuns === undefined ? {} : { maxRuns: args.schedule.maxRuns }),
|
|
668
|
+
},
|
|
669
|
+
}),
|
|
577
670
|
}
|
|
578
671
|
try {
|
|
579
672
|
const snapshot = await host.apply(crypto.randomUUID(), { kind: 'create', id, input }, callingSessionId(exec))
|
|
@@ -591,7 +684,7 @@ function buildUpdateTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
591
684
|
return defineTool({
|
|
592
685
|
name: 'task_board_update',
|
|
593
686
|
description: [
|
|
594
|
-
'Edit a task board card: title, description, prompt, labels, and the execution targets (workspace, agent preset, permission, model, session reuse).',
|
|
687
|
+
'Edit a task board card: title, description, prompt, labels, and the execution targets (workspace, agent preset, permission, model, reasoning effort, session reuse).',
|
|
595
688
|
'Only the fields you pass change; pass an empty string to clear a target, or an empty labels array to clear the labels.',
|
|
596
689
|
'Title, description and prompt freeze once the task has started executing; the execution targets stay editable because they only affect future runs.',
|
|
597
690
|
'Changing the pinned permission re-arms the permission confirmation gate.',
|
|
@@ -619,9 +712,10 @@ function buildUpdateTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
619
712
|
description: 'New permission preset; an empty string clears it back to the session default.',
|
|
620
713
|
},
|
|
621
714
|
model: { type: 'string', description: 'New pinned model; an empty string clears it.' },
|
|
715
|
+
reasoningEffort: { type: 'string', description: 'New reasoning effort id for the pinned model; an empty string clears it back to the model own default. Only an effort the target model declares is sent at run time.' },
|
|
622
716
|
reuseSession: { type: 'boolean', description: 'Continue later runs in the previous execution session.' },
|
|
623
717
|
teamRun: { type: 'boolean', description: 'Switches this task between a plain cascade (one session per member) and an Agent Team run (Lead session plus a teammate per subtask).' },
|
|
624
|
-
goalRun: { type: 'boolean', description: 'Whether each run starts with dsh built-in /goal (default
|
|
718
|
+
goalRun: { type: 'boolean', description: 'Whether each run starts with dsh built-in /goal (default false, one plain turn). Pass true to opt this card in, or false to return it to the default.' },
|
|
625
719
|
skipVerification: { type: 'boolean', description: 'Whether this task runs without the board acceptance gate (default false, inherit the board-wide switch). Pass true to opt out; pass false to opt back in. Only affects executions started afterwards.' },
|
|
626
720
|
tags: {
|
|
627
721
|
type: 'array',
|
|
@@ -645,13 +739,17 @@ function buildUpdateTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
645
739
|
if (has(args, 'workspaceId') && typeof args.workspaceId === 'string') patch.workspaceId = args.workspaceId
|
|
646
740
|
if (has(args, 'mode') && typeof args.mode === 'string') patch.mode = args.mode
|
|
647
741
|
if (has(args, 'model') && typeof args.model === 'string') patch.model = args.model
|
|
742
|
+
// An empty effort is the wire's clearing value (back to the model's own default).
|
|
743
|
+
if (has(args, 'reasoningEffort') && typeof args.reasoningEffort === 'string') {
|
|
744
|
+
patch.reasoningEffort = args.reasoningEffort
|
|
745
|
+
}
|
|
648
746
|
// An empty permission is the wire's clearing value (back to the session default).
|
|
649
747
|
if (has(args, 'permission') && typeof args.permission === 'string') {
|
|
650
748
|
patch.permission = args.permission === '' ? undefined : args.permission
|
|
651
749
|
}
|
|
652
750
|
if (has(args, 'reuseSession')) patch.reuseSession = args.reuseSession === true
|
|
653
751
|
if (has(args, 'teamRun')) patch.teamRun = args.teamRun === true
|
|
654
|
-
if (has(args, 'goalRun')) patch.goalRun = args.goalRun ===
|
|
752
|
+
if (has(args, 'goalRun')) patch.goalRun = args.goalRun === true ? true : false
|
|
655
753
|
if (has(args, 'skipVerification')) patch.skipVerification = args.skipVerification === true ? true : false
|
|
656
754
|
if (has(args, 'tags')) {
|
|
657
755
|
const tags = args.tags ?? []
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admission control for the session-creating half of a run.
|
|
3
|
+
*
|
|
4
|
+
* One board action can open a whole subtree, and a team run hands every member
|
|
5
|
+
* of it to the Lead at once. Both paths used to fire every launch in the same
|
|
6
|
+
* tick, so the Host asked DSH for N sessions at once — the burst the repository's
|
|
7
|
+
* measured guidance caps at three heavy implementation subagents
|
|
8
|
+
* (docs/multi-agent-resources.md), with no mechanism enforcing it.
|
|
9
|
+
*
|
|
10
|
+
* The queue is SERVICE-wide, not per run group: the pressure it bounds is the
|
|
11
|
+
* machine's (the Host process's memory and the swap it drives), and two run
|
|
12
|
+
* groups opened from two tabs share one Host. Admission is round-robin across
|
|
13
|
+
* run groups — FIFO inside a group, so a group's first run (the Lead, which
|
|
14
|
+
* carries the run shape and the teammate namespace) always starts before its
|
|
15
|
+
* members — so a large subtree cannot starve a run opened after it, and no
|
|
16
|
+
* entry can be starved.
|
|
17
|
+
*
|
|
18
|
+
* The width is read through a live callback, so a settings edit takes effect on
|
|
19
|
+
* the next admission without remounting anything.
|
|
20
|
+
*
|
|
21
|
+
* @module dsh-task-board/host/launch-admission
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** One launch the queue has not started yet. */
|
|
25
|
+
export interface QueuedLaunch {
|
|
26
|
+
/** Run group this launch belongs to; the unit of round-robin fairness. */
|
|
27
|
+
readonly groupId: string
|
|
28
|
+
/**
|
|
29
|
+
* Perform the launch. The slot is held until this promise settles, which
|
|
30
|
+
* makes "in flight" mean "still creating and prompting its session".
|
|
31
|
+
* A rejection is swallowed here so one broken launch can never wedge the
|
|
32
|
+
* slot; the caller owns reporting.
|
|
33
|
+
*/
|
|
34
|
+
start(): Promise<void>
|
|
35
|
+
/**
|
|
36
|
+
* Called by {@link LaunchAdmissionQueue.drain} for a launch that never
|
|
37
|
+
* started, so the caller can take its execution out of the running column.
|
|
38
|
+
*/
|
|
39
|
+
abandon(): void
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class LaunchAdmissionQueue {
|
|
43
|
+
/** Waiting launches per run group, in arrival order. */
|
|
44
|
+
private readonly waiting = new Map<string, QueuedLaunch[]>()
|
|
45
|
+
/** Run groups in round-robin order. */
|
|
46
|
+
private rotation: string[] = []
|
|
47
|
+
/** Slot holders of the launches in flight, one per admitted launch. */
|
|
48
|
+
private readonly inFlight = new Set<() => void>()
|
|
49
|
+
/** Highest number of launches in flight at once; observability and tests. */
|
|
50
|
+
peakInFlight = 0
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param width - live admission width (already normalized by the caller).
|
|
54
|
+
*/
|
|
55
|
+
constructor(private readonly width: () => number) {}
|
|
56
|
+
|
|
57
|
+
/** Launches the queue has not admitted yet. */
|
|
58
|
+
get pendingCount(): number {
|
|
59
|
+
let total = 0
|
|
60
|
+
for (const queue of this.waiting.values()) total += queue.length
|
|
61
|
+
return total
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Launches currently holding a slot. */
|
|
65
|
+
get inFlightCount(): number {
|
|
66
|
+
return this.inFlight.size
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Admit one launch, immediately if a slot is free. */
|
|
70
|
+
enqueue(launch: QueuedLaunch): void {
|
|
71
|
+
const queue = this.waiting.get(launch.groupId)
|
|
72
|
+
if (queue === undefined) {
|
|
73
|
+
this.waiting.set(launch.groupId, [launch])
|
|
74
|
+
this.rotation.push(launch.groupId)
|
|
75
|
+
} else {
|
|
76
|
+
queue.push(launch)
|
|
77
|
+
}
|
|
78
|
+
this.pump()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Abandon every waiting launch: the queue is being torn down, so an entry
|
|
83
|
+
* that never started can never start. Launches already in flight are left
|
|
84
|
+
* alone — their session may already exist, and the callers that own them
|
|
85
|
+
* settle them through the normal path.
|
|
86
|
+
* @returns the abandoned launches, in abandonment order.
|
|
87
|
+
*/
|
|
88
|
+
drain(): QueuedLaunch[] {
|
|
89
|
+
const abandoned: QueuedLaunch[] = []
|
|
90
|
+
for (const groupId of this.rotation) {
|
|
91
|
+
for (const launch of this.waiting.get(groupId) ?? []) abandoned.push(launch)
|
|
92
|
+
}
|
|
93
|
+
this.rotation = []
|
|
94
|
+
this.waiting.clear()
|
|
95
|
+
for (const launch of abandoned) launch.abandon()
|
|
96
|
+
return abandoned
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private pump(): void {
|
|
100
|
+
while (this.inFlight.size < this.width()) {
|
|
101
|
+
const launch = this.takeNext()
|
|
102
|
+
if (launch === undefined) return
|
|
103
|
+
this.admit(launch)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The next launch by round-robin across groups and FIFO inside one. */
|
|
108
|
+
private takeNext(): QueuedLaunch | undefined {
|
|
109
|
+
while (this.rotation.length > 0) {
|
|
110
|
+
const groupId = this.rotation.shift()
|
|
111
|
+
if (groupId === undefined) return undefined
|
|
112
|
+
const queue = this.waiting.get(groupId)
|
|
113
|
+
if (queue === undefined || queue.length === 0) {
|
|
114
|
+
this.waiting.delete(groupId)
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
const launch = queue.shift()
|
|
118
|
+
if (launch === undefined) continue
|
|
119
|
+
if (queue.length === 0) this.waiting.delete(groupId)
|
|
120
|
+
// A group that still has work goes to the back of the rotation, so every
|
|
121
|
+
// other run group gets its turn before this one's next member.
|
|
122
|
+
else this.rotation.push(groupId)
|
|
123
|
+
return launch
|
|
124
|
+
}
|
|
125
|
+
return undefined
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private admit(launch: QueuedLaunch): void {
|
|
129
|
+
const release = (): void => {
|
|
130
|
+
this.inFlight.delete(release)
|
|
131
|
+
this.pump()
|
|
132
|
+
}
|
|
133
|
+
this.inFlight.add(release)
|
|
134
|
+
if (this.inFlight.size > this.peakInFlight) this.peakInFlight = this.inFlight.size
|
|
135
|
+
void Promise.resolve()
|
|
136
|
+
.then(() => launch.start())
|
|
137
|
+
.catch(() => { /* a rejected launch still releases its slot; the caller reports it */ })
|
|
138
|
+
.then(release)
|
|
139
|
+
}
|
|
140
|
+
}
|