@linxin666/dsh-client-ui-task-board 0.4.6 → 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 +14 -6
- package/README.zh.md +14 -6
- package/lib/client.js +1502 -820
- package/lib/client.js.map +1 -1
- package/lib/index.js +674 -75
- package/lib/types/client/CardDefaultPresetField.d.ts +35 -0
- package/lib/types/client/TaskBoardSettingsCard.d.ts +49 -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/locales.d.ts +16 -0
- package/lib/types/core/card-defaults.d.ts +78 -0
- package/lib/types/core/controller.d.ts +32 -7
- package/lib/types/core/launch-width.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 +19 -0
- package/lib/types/core/use-cases/task-create.d.ts +10 -5
- package/lib/types/core/use-cases/task-update.d.ts +1 -1
- package/lib/types/host/launch-admission.d.ts +72 -0
- package/lib/types/host-ledger.d.ts +9 -4
- package/lib/types/host-runner.d.ts +79 -4
- package/lib/types/host-service.d.ts +85 -2
- package/lib/types/index.d.ts +87 -0
- package/lib/types/protocol.d.ts +13 -0
- package/package.json +28 -28
- package/src/client/CardDefaultPresetField.tsx +101 -0
- package/src/client/TaskBoardSettingsCard.tsx +214 -16
- package/src/client/board/LinkSubtaskModal.tsx +8 -0
- package/src/client/board/NewTaskModal.tsx +86 -40
- package/src/client/board/ReasoningEffortField.tsx +50 -0
- package/src/client/board/TaskDetail.tsx +42 -6
- 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/team-run-promotion.ts +39 -0
- package/src/client/index.ts +98 -94
- package/src/client/locales.ts +34 -2
- package/src/client/settings-entry-form.ts +0 -1
- package/src/core/card-defaults.ts +132 -0
- package/src/core/controller.ts +44 -9
- package/src/core/launch-width.ts +38 -0
- package/src/core/reasoning-effort.ts +92 -0
- package/src/core/store.ts +5 -0
- package/src/core/subtask.ts +48 -13
- package/src/core/tasks.ts +20 -0
- package/src/core/use-cases/task-create.ts +27 -16
- package/src/core/use-cases/task-update.ts +6 -1
- package/src/host/agent-tools.ts +21 -8
- package/src/host/launch-admission.ts +140 -0
- package/src/host-ledger.ts +18 -6
- package/src/host-runner.ts +184 -18
- package/src/host-service.ts +247 -23
- package/src/index.ts +149 -2
- package/src/protocol.ts +28 -4
package/src/core/controller.ts
CHANGED
|
@@ -127,6 +127,17 @@ export interface ExecutionModelOption {
|
|
|
127
127
|
name?: string
|
|
128
128
|
label?: string
|
|
129
129
|
provider?: string
|
|
130
|
+
/**
|
|
131
|
+
* Reasoning metadata the adapter declares for this exact route. An effort
|
|
132
|
+
* picker is offered only while `supported`, and its choices are `efforts` —
|
|
133
|
+
* the ids the target model really accepts, so the UI never offers (or keeps)
|
|
134
|
+
* a level the model would reject. Absent means the deployment published no
|
|
135
|
+
* capability for this model; the card value is then left untouched.
|
|
136
|
+
*/
|
|
137
|
+
reasoning?: {
|
|
138
|
+
supported: boolean
|
|
139
|
+
efforts?: readonly string[]
|
|
140
|
+
}
|
|
130
141
|
}
|
|
131
142
|
|
|
132
143
|
/** The execution-target option sets the UI feeds into the controller. */
|
|
@@ -141,7 +152,7 @@ export interface ExecutionOptionsSnapshot {
|
|
|
141
152
|
* (revision/scheduler/power); a full snapshot also carries the deployment
|
|
142
153
|
* constants the UI reads (session default permission, subtask depth).
|
|
143
154
|
*/
|
|
144
|
-
export type HostMirror = Pick<TaskBoardSnapshot, 'revision' | 'scheduler' | 'power' | 'sessionDefaultPermission' | 'maxSubtaskDepth' | 'teamRunAvailable' | 'goalRunEnabled' | 'extensions'>
|
|
155
|
+
export type HostMirror = Pick<TaskBoardSnapshot, 'revision' | 'scheduler' | 'power' | 'sessionDefaultPermission' | 'maxSubtaskDepth' | 'teamRunAvailable' | 'goalRunEnabled' | 'cardDefaults' | 'extensions'>
|
|
145
156
|
|
|
146
157
|
/** Immutable controller snapshot for UI subscriptions. */
|
|
147
158
|
export interface ControllerSnapshot {
|
|
@@ -318,6 +329,22 @@ export class BoardController {
|
|
|
318
329
|
return await this.initializeRemote()
|
|
319
330
|
}
|
|
320
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Re-read the authoritative Host snapshot.
|
|
334
|
+
*
|
|
335
|
+
* The board's volatile deployment configuration — the new-card defaults among
|
|
336
|
+
* it — is republished the moment the settings document is written, but the SSE
|
|
337
|
+
* frame that republish emits carries the value as of BEFORE the write (the
|
|
338
|
+
* loader hands the update to the fiber while the new document is not yet
|
|
339
|
+
* visible to it), so a mirror that followed frames alone would stay one edit
|
|
340
|
+
* behind and the create dialog would prefill from the previous defaults for
|
|
341
|
+
* the rest of the session. The surface that owns the write asks for the
|
|
342
|
+
* snapshot again instead of trusting the frame.
|
|
343
|
+
*/
|
|
344
|
+
refreshHostState(): void {
|
|
345
|
+
void this.refreshRemote()
|
|
346
|
+
}
|
|
347
|
+
|
|
321
348
|
// --- view state -------------------------------------------------------------
|
|
322
349
|
|
|
323
350
|
/**
|
|
@@ -407,7 +434,7 @@ export class BoardController {
|
|
|
407
434
|
|
|
408
435
|
createTask(input: NewTaskInput): TaskRecord | undefined {
|
|
409
436
|
const id = this.uuid()
|
|
410
|
-
const { task, tasks } = applyCreateTask(this.tasks, input, this.now(), id, this.mirrorDepth())
|
|
437
|
+
const { task, tasks } = applyCreateTask(this.tasks, input, this.now(), id, this.mirrorDepth(), undefined, this.hostState?.cardDefaults)
|
|
411
438
|
if (task === undefined) return undefined
|
|
412
439
|
this.tasks = [...tasks]
|
|
413
440
|
this.persistAndNotify()
|
|
@@ -421,7 +448,7 @@ export class BoardController {
|
|
|
421
448
|
// The preview must obey the SAME lineage gate the Host will: a browser-side
|
|
422
449
|
// default of one level would reject a legal subtask-of-subtask before the
|
|
423
450
|
// request is ever sent.
|
|
424
|
-
const preview = applyCreateTask(this.tasks, input, this.now(), id, this.mirrorDepth()).task
|
|
451
|
+
const preview = applyCreateTask(this.tasks, input, this.now(), id, this.mirrorDepth(), undefined, this.hostState?.cardDefaults).task
|
|
425
452
|
if (preview === undefined) return undefined
|
|
426
453
|
return await this.commitRemote({ kind: 'create', id, input }, id)
|
|
427
454
|
? this.tasks.find(task => task.id === id)
|
|
@@ -804,18 +831,25 @@ export class BoardController {
|
|
|
804
831
|
}
|
|
805
832
|
|
|
806
833
|
/**
|
|
807
|
-
* Project a Host snapshot onto the browser's mirror.
|
|
808
|
-
*
|
|
809
|
-
* deployment constants the UI reads
|
|
810
|
-
*
|
|
811
|
-
*
|
|
812
|
-
*
|
|
834
|
+
* Project a Host snapshot onto the browser's mirror. Every snapshot the
|
|
835
|
+
* transport serves is a full document (bootstrap/state/action all answer
|
|
836
|
+
* `TaskBoardHostService.snapshot()`), so the deployment constants the UI reads
|
|
837
|
+
* — the session-default permission the confirmation banner compares against,
|
|
838
|
+
* the subtask depth limit, the platform switches and the extensions a provider
|
|
839
|
+
* published — are taken as published, carried over only where the snapshot
|
|
840
|
+
* itself omits the key.
|
|
813
841
|
*/
|
|
814
842
|
private mirrorOf(snapshot: TaskBoardSnapshot): HostMirror {
|
|
815
843
|
const sessionDefaultPermission = snapshot.sessionDefaultPermission ?? this.hostState?.sessionDefaultPermission
|
|
816
844
|
const maxSubtaskDepth = snapshot.maxSubtaskDepth ?? this.hostState?.maxSubtaskDepth
|
|
817
845
|
const teamRunAvailable = snapshot.teamRunAvailable ?? this.hostState?.teamRunAvailable
|
|
818
846
|
const goalRunEnabled = snapshot.goalRunEnabled ?? this.hostState?.goalRunEnabled
|
|
847
|
+
// The new-card defaults are NOT carried over: the Host publishes a target
|
|
848
|
+
// nothing configures by omitting it (protocol.ts states that explicitly), so
|
|
849
|
+
// an absent key means "this deployment configures none". Carrying the last
|
|
850
|
+
// value over would keep a CLEARED default alive in the create dialog, which
|
|
851
|
+
// is the mirror image of the staleness this key is re-read for.
|
|
852
|
+
const cardDefaults = snapshot.cardDefaults
|
|
819
853
|
const extensions = snapshot.extensions ?? this.hostState?.extensions
|
|
820
854
|
return {
|
|
821
855
|
revision: snapshot.revision,
|
|
@@ -825,6 +859,7 @@ export class BoardController {
|
|
|
825
859
|
...(maxSubtaskDepth === undefined ? {} : { maxSubtaskDepth }),
|
|
826
860
|
...(teamRunAvailable === undefined ? {} : { teamRunAvailable }),
|
|
827
861
|
...(goalRunEnabled === undefined ? {} : { goalRunEnabled }),
|
|
862
|
+
...(cardDefaults === undefined ? {} : { cardDefaults }),
|
|
828
863
|
...(extensions === undefined ? {} : { extensions }),
|
|
829
864
|
}
|
|
830
865
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run-launch admission width: the one bound on how many sessions this board
|
|
3
|
+
* starts at the same time.
|
|
4
|
+
*
|
|
5
|
+
* One board action opens a whole subtree, and a team run hands every member of
|
|
6
|
+
* it to the Lead at once, so without a bound the Host asks DSH for dozens of
|
|
7
|
+
* sessions in a single tick. The repository's own measured guidance caps heavy
|
|
8
|
+
* implementation subagents at three (docs/multi-agent-resources.md) and no
|
|
9
|
+
* runtime mechanism enforced it; this setting is that mechanism.
|
|
10
|
+
*
|
|
11
|
+
* The bounds and the default live here rather than in either half, exactly like
|
|
12
|
+
* the roster cadence: the settings card validates a draft against the same
|
|
13
|
+
* range the Host clamps a hand-edited configuration to, and a programmatic
|
|
14
|
+
* mount that names no width gets the default.
|
|
15
|
+
*
|
|
16
|
+
* @module dsh-task-board/core/launch-width
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Width used when neither the settings document nor the mount names one. */
|
|
20
|
+
export const DEFAULT_RUN_LAUNCH_WIDTH = 3
|
|
21
|
+
|
|
22
|
+
/** Narrowest accepted width: one launch at a time. */
|
|
23
|
+
export const RUN_LAUNCH_WIDTH_MIN = 1
|
|
24
|
+
|
|
25
|
+
/** Widest accepted width. */
|
|
26
|
+
export const RUN_LAUNCH_WIDTH_MAX = 16
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Clamp a configured width into the supported range. A value the schema would
|
|
30
|
+
* refuse (non-finite, below the floor, above the ceiling) still leaves the
|
|
31
|
+
* board launching: an out-of-range setting never stalls a run.
|
|
32
|
+
* @param width - configured width, or undefined for the default.
|
|
33
|
+
* @returns the effective width.
|
|
34
|
+
*/
|
|
35
|
+
export function normalizeRunLaunchWidth(width: number | undefined): number {
|
|
36
|
+
if (width === undefined || !Number.isFinite(width)) return DEFAULT_RUN_LAUNCH_WIDTH
|
|
37
|
+
return Math.min(Math.max(Math.trunc(width), RUN_LAUNCH_WIDTH_MIN), RUN_LAUNCH_WIDTH_MAX)
|
|
38
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which reasoning effort one execution may actually send.
|
|
3
|
+
*
|
|
4
|
+
* The rule lives here, in one pure place, so the launch path cannot drift from
|
|
5
|
+
* what the picker promises: an effort is sent ONLY for the exact model route
|
|
6
|
+
* whose adapter declares that id. An unsupported value is dropped rather than
|
|
7
|
+
* passed through — the Host validates the selection against the same
|
|
8
|
+
* capability (`resolveCallConfig` rejects an unsupported effort) and the whole
|
|
9
|
+
* model selection would fail with it. An absent effort stays absent: the SDK
|
|
10
|
+
* then materializes the model's own default, which is exactly what "inherit
|
|
11
|
+
* the model" means.
|
|
12
|
+
*
|
|
13
|
+
* A card that pins a bare model id also gets its provider resolved here: the
|
|
14
|
+
* session wire contract requires `provider` (ModelSelection), so the catalog is
|
|
15
|
+
* the only place that can complete the documented `provider/model` or model-id
|
|
16
|
+
* pin.
|
|
17
|
+
*/
|
|
18
|
+
import type { CatalogModel, ModelCatalogView } from './verification.ts'
|
|
19
|
+
|
|
20
|
+
/** One execution route an effort resolves against: `provider/model`, or a bare model id. */
|
|
21
|
+
export interface ReasoningRoute {
|
|
22
|
+
provider?: string
|
|
23
|
+
model: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The catalog entry a route names, once the catalog can complete it. */
|
|
27
|
+
export interface DeclaredReasoningRoute {
|
|
28
|
+
provider: string
|
|
29
|
+
model: string
|
|
30
|
+
/** The adapter's reasoning metadata; absent when it exposes none. */
|
|
31
|
+
reasoning?: CatalogModel['reasoning']
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** What one card's effort resolves to for one route. */
|
|
35
|
+
export interface ReasoningEffortDecision {
|
|
36
|
+
/** The effort to send; absent means "send none". */
|
|
37
|
+
effort?: string
|
|
38
|
+
/** The card's own value when it will NOT be sent. */
|
|
39
|
+
dropped?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Trim one optional effort: blank text means "not set", like every other pinned target. */
|
|
43
|
+
export function normalizeReasoningEffort(value: string | undefined): string | undefined {
|
|
44
|
+
const trimmed = value?.trim()
|
|
45
|
+
return trimmed === undefined || trimmed === '' ? undefined : trimmed
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Complete one route through the host catalog: a qualified route must match its
|
|
50
|
+
* provider group, and a bare model id matches the first group carrying it —
|
|
51
|
+
* which is the provider the request then names.
|
|
52
|
+
* @param catalog - the host model catalog, when this deployment serves one.
|
|
53
|
+
* @param route - the route as the card pins it.
|
|
54
|
+
* @returns the completed route with its reasoning metadata, or undefined when
|
|
55
|
+
* the catalog declares no such model.
|
|
56
|
+
*/
|
|
57
|
+
export function declaredRoute(
|
|
58
|
+
catalog: ModelCatalogView | undefined,
|
|
59
|
+
route: ReasoningRoute,
|
|
60
|
+
): DeclaredReasoningRoute | undefined {
|
|
61
|
+
const groups = catalog?.groups ?? []
|
|
62
|
+
const matched = route.provider === undefined || route.provider === ''
|
|
63
|
+
? groups.find(group => group.models.some(model => model.id === route.model))
|
|
64
|
+
: groups.find(group => group.id === route.provider)
|
|
65
|
+
const model = matched?.models.find(candidate => candidate.id === route.model)
|
|
66
|
+
if (matched === undefined || model === undefined) return undefined
|
|
67
|
+
return {
|
|
68
|
+
provider: matched.id,
|
|
69
|
+
model: model.id,
|
|
70
|
+
...(model.reasoning === undefined ? {} : { reasoning: model.reasoning }),
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolve the effort one execution sends.
|
|
76
|
+
* @param requested - the card's stored effort (absent or blank = inherit).
|
|
77
|
+
* @param catalog - the host model catalog, when this deployment serves one.
|
|
78
|
+
* @param route - the route the execution will actually run at.
|
|
79
|
+
* @returns the effort to send, or the dropped value that will not be sent.
|
|
80
|
+
*/
|
|
81
|
+
export function resolveReasoningEffort(
|
|
82
|
+
requested: string | undefined,
|
|
83
|
+
catalog: ModelCatalogView | undefined,
|
|
84
|
+
route: ReasoningRoute | undefined,
|
|
85
|
+
): ReasoningEffortDecision {
|
|
86
|
+
const effort = normalizeReasoningEffort(requested)
|
|
87
|
+
if (effort === undefined) return {}
|
|
88
|
+
if (route === undefined) return { dropped: effort }
|
|
89
|
+
const declared = declaredRoute(catalog, route)
|
|
90
|
+
const supported = declared?.reasoning?.efforts.some(option => option.id === effort) === true
|
|
91
|
+
return supported ? { effort } : { dropped: effort }
|
|
92
|
+
}
|
package/src/core/store.ts
CHANGED
|
@@ -68,6 +68,7 @@ function isTaskRecordShape(value: unknown): value is Omit<TaskRecord, 'status'>
|
|
|
68
68
|
if (record.parentId !== undefined && typeof record.parentId !== 'string') return false
|
|
69
69
|
if (record.workspaceId !== undefined && typeof record.workspaceId !== 'string') return false
|
|
70
70
|
if (record.mode !== undefined && typeof record.mode !== 'string') return false
|
|
71
|
+
if (record.reasoningEffort !== undefined && typeof record.reasoningEffort !== 'string') return false
|
|
71
72
|
if (record.permission !== undefined && typeof record.permission !== 'string') return false
|
|
72
73
|
if (record.integrations !== undefined && (typeof record.integrations !== 'object' || record.integrations === null || Array.isArray(record.integrations))) return false
|
|
73
74
|
if (record.hidden !== undefined && typeof record.hidden !== 'boolean') return false
|
|
@@ -265,6 +266,10 @@ export function parseLedger(raw: string | null): TaskRecord[] {
|
|
|
265
266
|
// fall back to the session default instead of dropping the row.
|
|
266
267
|
task.workspaceId = normalizeTargetId(row.workspaceId)
|
|
267
268
|
task.mode = normalizeTargetId(row.mode)
|
|
269
|
+
// The effort is repaired like the other execution-target strings: blank text
|
|
270
|
+
// from a hand-edited document clears the pin instead of dropping the row,
|
|
271
|
+
// and the card then runs at its model's own default level.
|
|
272
|
+
task.reasoningEffort = normalizeTargetId(row.reasoningEffort)
|
|
268
273
|
task.archivedAt = typeof row.archivedAt === 'number' && Number.isFinite(row.archivedAt) ? row.archivedAt : undefined
|
|
269
274
|
task.permission = isTaskPermission(row.permission) ? row.permission as TaskPermission : undefined
|
|
270
275
|
task.reuseSession = row.reuseSession === true ? true : undefined
|
package/src/core/subtask.ts
CHANGED
|
@@ -183,9 +183,12 @@ export function checkParentLink(
|
|
|
183
183
|
|
|
184
184
|
/**
|
|
185
185
|
* The tasks one run request opens executions for: the requested task first,
|
|
186
|
-
* then its on-board descendants within the configured depth.
|
|
187
|
-
*
|
|
188
|
-
* the
|
|
186
|
+
* then its on-board descendants within the configured depth. This is the
|
|
187
|
+
* SHAPE of a cascade — every walk that must see the whole tree (settlement,
|
|
188
|
+
* archive/restore, the running lock) reads it. The run path filters it with
|
|
189
|
+
* {@link joinsCascadeRun}: archived tasks never run, a participant that already
|
|
190
|
+
* has an open execution is skipped by the caller, which cannot start it twice,
|
|
191
|
+
* and a finished descendant is left alone.
|
|
189
192
|
*/
|
|
190
193
|
export function cascadeTargets(
|
|
191
194
|
tasks: readonly TaskRecord[],
|
|
@@ -197,6 +200,27 @@ export function cascadeTargets(
|
|
|
197
200
|
return [root, ...descendantTasks(tasks, rootId, maxSubtaskDepth)]
|
|
198
201
|
}
|
|
199
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Whether a cascade member takes part in a new run.
|
|
205
|
+
*
|
|
206
|
+
* The task the user ran is always a member: the request names that card, and
|
|
207
|
+
* the operator asked for it to run whatever its column says. A descendant does
|
|
208
|
+
* not join once it sits in the `done` column — its work is finished, so running
|
|
209
|
+
* the parent again is a run of the parent, never a second execution of work
|
|
210
|
+
* that is already done. The rule keys on the column alone, so it covers both a
|
|
211
|
+
* subtask that ran and one an operator moved to done by hand, and it holds for
|
|
212
|
+
* every trigger: a manual run, a re-run, and a due occurrence of a schedule.
|
|
213
|
+
* A card whose own schedule is enabled settles back into `todo`, so a scheduled
|
|
214
|
+
* subtask stays a participant; to make a finished one run again, move it back
|
|
215
|
+
* to `todo` or run it on its own.
|
|
216
|
+
* @param task - the candidate member.
|
|
217
|
+
* @param rootId - the task the run request named.
|
|
218
|
+
* @returns false when this member is left alone by the run.
|
|
219
|
+
*/
|
|
220
|
+
export function joinsCascadeRun(task: TaskRecord, rootId: string): boolean {
|
|
221
|
+
return task.id === rootId || task.status !== 'done'
|
|
222
|
+
}
|
|
223
|
+
|
|
200
224
|
/** One still-open execution belonging to a cascade run group. */
|
|
201
225
|
export interface OpenCascadeChild {
|
|
202
226
|
taskId: string
|
|
@@ -263,11 +287,12 @@ export function effectiveTaskPermission(task: TaskRecord): TaskRecord['permissio
|
|
|
263
287
|
/**
|
|
264
288
|
* Resolve the execution targets a (sub)task actually runs with: its own value
|
|
265
289
|
* wins, and an unset target inherits the nearest ancestor up to the root.
|
|
266
|
-
* Workspace, agent preset and
|
|
267
|
-
* handover bundle is a per-card continuation artefact and is never
|
|
268
|
-
* The permission inherits the ancestor's EFFECTIVE binding, and its
|
|
269
|
-
* confirmation travels with it, so a subtask of an already-confirmed
|
|
270
|
-
* card is runnable while a subtask's own unconfirmed binding stays
|
|
290
|
+
* Workspace, agent preset, model and reasoning effort inherit the ancestor's
|
|
291
|
+
* PIN only — a handover bundle is a per-card continuation artefact and is never
|
|
292
|
+
* inherited. The permission inherits the ancestor's EFFECTIVE binding, and its
|
|
293
|
+
* human confirmation travels with it, so a subtask of an already-confirmed
|
|
294
|
+
* elevated card is runnable while a subtask's own unconfirmed binding stays
|
|
295
|
+
* gated.
|
|
271
296
|
*/
|
|
272
297
|
/**
|
|
273
298
|
* The immutable teammate name for one team-mode member. Teammate names are
|
|
@@ -317,13 +342,22 @@ export function resolveExecutionTargets(
|
|
|
317
342
|
): TaskRecord {
|
|
318
343
|
if (task.parentId === undefined) return task
|
|
319
344
|
const chain = [task, ...ancestorChain(tasks, task).slice(0, maxSubtaskDepth)]
|
|
320
|
-
const first = (key: 'workspaceId' | 'mode' | 'model'): string | undefined => {
|
|
321
|
-
for (const candidate of
|
|
345
|
+
const first = (key: 'workspaceId' | 'mode' | 'model' | 'reasoningEffort', candidates: readonly TaskRecord[]): string | undefined => {
|
|
346
|
+
for (const candidate of candidates) {
|
|
322
347
|
const value = candidate[key]
|
|
323
348
|
if (value !== undefined) return value
|
|
324
349
|
}
|
|
325
350
|
return undefined
|
|
326
351
|
}
|
|
352
|
+
// The model pin is the boundary of the effort: an effort is only ever taken
|
|
353
|
+
// from the card the model came from, or from the card that pinned it below
|
|
354
|
+
// that one. A card with a model of its own thus sends NO effort when it stores
|
|
355
|
+
// none — the model's own default — instead of quietly running at an ancestor
|
|
356
|
+
// level chosen for a different model, even when both models declare that
|
|
357
|
+
// level. A card with no model of its own still inherits the nearest model
|
|
358
|
+
// together with the nearest level pinned at or below it.
|
|
359
|
+
const boundary = chain.findIndex(candidate => candidate.model !== undefined)
|
|
360
|
+
const effortChain = boundary === -1 ? chain : chain.slice(0, boundary + 1)
|
|
327
361
|
const own = effectiveTaskPermission(task)
|
|
328
362
|
// An own binding (pin or handover) keeps its own confirmation stamp; only an
|
|
329
363
|
// absent one borrows the ancestor's binding AND that ancestor's stamp.
|
|
@@ -332,9 +366,10 @@ export function resolveExecutionTargets(
|
|
|
332
366
|
: undefined
|
|
333
367
|
return {
|
|
334
368
|
...task,
|
|
335
|
-
workspaceId: first('workspaceId'),
|
|
336
|
-
mode: first('mode'),
|
|
337
|
-
model: first('model'),
|
|
369
|
+
workspaceId: first('workspaceId', chain),
|
|
370
|
+
mode: first('mode', chain),
|
|
371
|
+
model: first('model', chain),
|
|
372
|
+
reasoningEffort: first('reasoningEffort', effortChain),
|
|
338
373
|
permission: own ?? (owner === undefined ? undefined : effectiveTaskPermission(owner)),
|
|
339
374
|
permissionConfirmedAt: own === undefined ? owner?.permissionConfirmedAt : task.permissionConfirmedAt,
|
|
340
375
|
}
|
package/src/core/tasks.ts
CHANGED
|
@@ -406,6 +406,15 @@ export interface TaskRecord {
|
|
|
406
406
|
* absent falls back to the host default (agent-default-model).
|
|
407
407
|
*/
|
|
408
408
|
model?: string
|
|
409
|
+
/**
|
|
410
|
+
* Reasoning effort pinned for that execution route (an adapter-owned id such
|
|
411
|
+
* as "high"). Absent (or blank) means INHERIT: the session runs at the
|
|
412
|
+
* model's own default level, exactly as a card written before the field
|
|
413
|
+
* existed does. The launch sends the value only when the target model's
|
|
414
|
+
* adapter declares that exact id, so switching a card to a model without
|
|
415
|
+
* that level never pushes an invalid selection (see core/reasoning-effort.ts).
|
|
416
|
+
*/
|
|
417
|
+
reasoningEffort?: string
|
|
409
418
|
/**
|
|
410
419
|
* Whether later executions continue in the previous execution's session
|
|
411
420
|
* (issue #1419) instead of minting a fresh conversation per run. Absent or
|
|
@@ -535,6 +544,16 @@ export interface NewTaskInput {
|
|
|
535
544
|
permission?: TaskPermission
|
|
536
545
|
/** Optional pinned model for the execution session; absent = host default. */
|
|
537
546
|
model?: string
|
|
547
|
+
/** Optional reasoning effort for that model route; empty/absent = the model's own default. */
|
|
548
|
+
reasoningEffort?: string
|
|
549
|
+
/**
|
|
550
|
+
* Opt in to the deployment's new-card defaults: a target this input leaves
|
|
551
|
+
* out is filled from the configured default (agent preset, permission, model
|
|
552
|
+
* and the level bound to that model). Absent means the create stores exactly
|
|
553
|
+
* what it was given, which is what the extension provider's own create path
|
|
554
|
+
* and the legacy import rely on.
|
|
555
|
+
*/
|
|
556
|
+
applyCardDefaults?: boolean
|
|
538
557
|
/** Reuse the previous execution's session for later runs (issue #1419). */
|
|
539
558
|
reuseSession?: boolean
|
|
540
559
|
/** Run the subtree as an Agent Team (Team Lead session plus one teammate per direct subtask). */
|
|
@@ -699,6 +718,7 @@ export function createTask(input: NewTaskInput, now: number, id: string): TaskRe
|
|
|
699
718
|
mode: normalizeTargetId(input.mode),
|
|
700
719
|
permission: isTaskPermission(input.permission) ? input.permission : undefined,
|
|
701
720
|
model: normalizeTargetId(input.model),
|
|
721
|
+
reasoningEffort: normalizeTargetId(input.reasoningEffort),
|
|
702
722
|
reuseSession: input.reuseSession === true ? true : undefined,
|
|
703
723
|
teamRun: input.teamRun === true ? true : undefined,
|
|
704
724
|
// Default OFF: only an explicit true is stored, so a card that never
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { isValidCron, isValidTimeZone, nextRunAtMs, resolveHostTimeZone } from '../schedule.ts'
|
|
8
8
|
import { DEFAULT_SUBTASK_DEPTH, normalizeSubtaskDepth, taskDepth } from '../subtask.ts'
|
|
9
|
-
import {
|
|
9
|
+
import { resolveCardTargets, type CardDefaults } from '../card-defaults.ts'
|
|
10
|
+
import { createTask, isTaskPermission, withSchedule, type NewTaskInput, type TaskRecord } from '../tasks.ts'
|
|
10
11
|
|
|
11
12
|
/** Result of a create transition: the new task (when accepted) + the next ledger. */
|
|
12
13
|
export interface CreateTaskResult {
|
|
@@ -23,16 +24,20 @@ export interface CreateTaskResult {
|
|
|
23
24
|
* appended ledger, or the unchanged ledger when the title is blank or the
|
|
24
25
|
* requested parent link is refused.
|
|
25
26
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
* The execution targets resolve through {@link resolveCardTargets}: what the
|
|
28
|
+
* caller sent wins, then the parent's own pins, then - only when the caller
|
|
29
|
+
* opted in with `applyCardDefaults` - the deployment's configured new-card
|
|
30
|
+
* defaults. Without the flag the resolution sees no defaults at all, which is
|
|
31
|
+
* exactly the inline parent inheritance this path has always applied. The
|
|
32
|
+
* workspace is not part of that resolution: a subtask still takes the parent's
|
|
33
|
+
* workspace, and the defaults never name one.
|
|
30
34
|
* @param tasks - current ledger.
|
|
31
35
|
* @param input - raw user input (title/description/prompt + optional schedule/parent).
|
|
32
36
|
* @param now - clock instant (ms epoch).
|
|
33
37
|
* @param id - minted task id.
|
|
34
38
|
* @param maxSubtaskDepth - deployment subtask depth limit.
|
|
35
39
|
* @param hostTimeZone - zone a rule that stores no zone of its own follows.
|
|
40
|
+
* @param cardDefaults - the deployment's new-card defaults, read live at creation.
|
|
36
41
|
*/
|
|
37
42
|
export function applyCreateTask(
|
|
38
43
|
tasks: readonly TaskRecord[],
|
|
@@ -41,6 +46,7 @@ export function applyCreateTask(
|
|
|
41
46
|
id: string,
|
|
42
47
|
maxSubtaskDepth: number = DEFAULT_SUBTASK_DEPTH,
|
|
43
48
|
hostTimeZone: string = resolveHostTimeZone(),
|
|
49
|
+
cardDefaults?: CardDefaults,
|
|
44
50
|
): CreateTaskResult {
|
|
45
51
|
if (input.title.trim() === '') return { task: undefined, tasks, error: 'title is required' }
|
|
46
52
|
const limit = normalizeSubtaskDepth(maxSubtaskDepth)
|
|
@@ -53,18 +59,23 @@ export function applyCreateTask(
|
|
|
53
59
|
return { task: undefined, tasks, error: `subtask depth limit (${limit}) exceeded` }
|
|
54
60
|
}
|
|
55
61
|
}
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
// The execution targets resolve through the one shared rule: the caller's own
|
|
63
|
+
// choice, then the parent's own pins (never its permission, see
|
|
64
|
+
// core/card-defaults.ts), then the deployment's new-card defaults — and only
|
|
65
|
+
// when the caller asked for them, so a create that never opts in stores
|
|
66
|
+
// exactly what it sent, as it always has.
|
|
67
|
+
const seed = resolveCardTargets(
|
|
68
|
+
input.applyCardDefaults === true ? cardDefaults : undefined,
|
|
69
|
+
parent === undefined ? undefined : { kind: 'parent', task: parent },
|
|
70
|
+
{ mode: input.mode, permission: input.permission, model: input.model, reasoningEffort: input.reasoningEffort },
|
|
71
|
+
)
|
|
72
|
+
const effective: NewTaskInput = {
|
|
64
73
|
...input,
|
|
65
|
-
workspaceId: input.workspaceId ?? parent
|
|
66
|
-
mode:
|
|
67
|
-
|
|
74
|
+
workspaceId: input.workspaceId ?? parent?.workspaceId,
|
|
75
|
+
mode: seed.mode,
|
|
76
|
+
permission: isTaskPermission(seed.permission) ? seed.permission : undefined,
|
|
77
|
+
model: seed.model,
|
|
78
|
+
reasoningEffort: seed.reasoningEffort,
|
|
68
79
|
}
|
|
69
80
|
let task = createTask(effective, now, id)
|
|
70
81
|
// Arm the requested schedule (new-task dialog). The Host wire gate already
|
|
@@ -16,7 +16,7 @@ import type { TaskHandoverInput } from '../handover.ts'
|
|
|
16
16
|
* Editable fields on a task (the update patch surface). `freeze` replaces the
|
|
17
17
|
* continuation-card snapshot (restamping frozenAt); an explicit null clears it.
|
|
18
18
|
*/
|
|
19
|
-
export type TaskUpdatePatch = Partial<Pick<TaskRecord, 'title' | 'description' | 'prompt' | 'workspaceId' | 'mode' | 'permission' | 'model' | 'reuseSession' | 'teamRun' | 'goalRun' | 'skipVerification'>> & {
|
|
19
|
+
export type TaskUpdatePatch = Partial<Pick<TaskRecord, 'title' | 'description' | 'prompt' | 'workspaceId' | 'mode' | 'permission' | 'model' | 'reasoningEffort' | 'reuseSession' | 'teamRun' | 'goalRun' | 'skipVerification'>> & {
|
|
20
20
|
freeze?: FreezeSnapshot & { redacted?: boolean } | null
|
|
21
21
|
/** Replaces the handover bundle (restamping bundledAt); an explicit null clears it. */
|
|
22
22
|
handover?: TaskHandoverInput | null
|
|
@@ -82,6 +82,7 @@ export function applyUpdateTask(
|
|
|
82
82
|
const mode = 'mode' in patch ? normalizeTargetId(patch.mode) : undefined
|
|
83
83
|
const permission = 'permission' in patch ? normalizePermission(task.permission, patch.permission) : undefined
|
|
84
84
|
const model = 'model' in patch ? normalizeTargetId(patch.model) : undefined
|
|
85
|
+
const reasoningEffort = 'reasoningEffort' in patch ? normalizeTargetId(patch.reasoningEffort) : undefined
|
|
85
86
|
const next: TaskRecord = { ...task, ...rest, updatedAt: now }
|
|
86
87
|
// Content fields normalize like creation does (trimmed); an explicit
|
|
87
88
|
// undefined keeps the current value — content cannot be cleared.
|
|
@@ -121,6 +122,10 @@ export function applyUpdateTask(
|
|
|
121
122
|
if (mode !== undefined || 'mode' in patch) next.mode = mode
|
|
122
123
|
if (permission !== undefined || 'permission' in patch) next.permission = permission
|
|
123
124
|
if (model !== undefined || 'model' in patch) next.model = model
|
|
125
|
+
// The effort is bound to the model route above it: blank text (or an
|
|
126
|
+
// explicit undefined) returns the card to the model's own default level,
|
|
127
|
+
// and only the launch decides whether the value can be sent at all.
|
|
128
|
+
if (reasoningEffort !== undefined || 'reasoningEffort' in patch) next.reasoningEffort = reasoningEffort
|
|
124
129
|
return next
|
|
125
130
|
})
|
|
126
131
|
}
|
package/src/host/agent-tools.ts
CHANGED
|
@@ -166,6 +166,7 @@ function taskSummary(
|
|
|
166
166
|
...(task.mode === undefined ? {} : { mode: task.mode }),
|
|
167
167
|
...(task.permission === undefined ? {} : { permission: task.permission }),
|
|
168
168
|
...(task.model === undefined ? {} : { model: task.model }),
|
|
169
|
+
...(task.reasoningEffort === undefined ? {} : { reasoningEffort: task.reasoningEffort }),
|
|
169
170
|
...(task.reuseSession === true ? { reuseSession: true } : {}),
|
|
170
171
|
...(task.teamRun === true ? { teamRun: true } : {}),
|
|
171
172
|
// Default-OFF opt-in: only a card that asked for the goal run reports it.
|
|
@@ -277,7 +278,7 @@ function buildSetParentTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
277
278
|
'Attach an existing task under a parent (making it a subtask) or detach it back to the root board by setting its parentId.',
|
|
278
279
|
'Pass an empty parentId to detach.',
|
|
279
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).',
|
|
280
|
-
'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.',
|
|
281
282
|
].join(' '),
|
|
282
283
|
parameters: {
|
|
283
284
|
taskId: { type: 'string', required: true, description: 'The task whose parent link changes.' },
|
|
@@ -313,7 +314,7 @@ function buildRunTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
313
314
|
'Executions consume real API quota. Sessions are created asynchronously, so read their ids and outcomes back with task_board_get.',
|
|
314
315
|
].join(' '),
|
|
315
316
|
parameters: {
|
|
316
|
-
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.' },
|
|
317
318
|
rerun: { type: 'boolean', description: 'Re-run a settled task: reset its column to todo before starting (default false).' },
|
|
318
319
|
},
|
|
319
320
|
output: { schema: { type: 'json' }, render: renderJson },
|
|
@@ -458,7 +459,7 @@ function buildScheduleTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
458
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.',
|
|
459
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).',
|
|
460
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.',
|
|
461
|
-
'A due scheduled task runs the same cascade a manual run does, so a root task runs its whole subtask tree.',
|
|
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.',
|
|
462
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.',
|
|
463
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.',
|
|
464
465
|
].join(' '),
|
|
@@ -586,7 +587,7 @@ function buildCreateTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
586
587
|
name: 'task_board_create',
|
|
587
588
|
description: [
|
|
588
589
|
'Create a task on the DSH task board, or a subtask by passing parentId.',
|
|
589
|
-
'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.',
|
|
590
591
|
'The deployment caps subtask depth (default one level, maximum three): a subtask of a subtask is refused unless the board is configured deeper.',
|
|
591
592
|
'The task lands in the todo column and does NOT run; call task_board_run to execute it.',
|
|
592
593
|
].join(' '),
|
|
@@ -596,9 +597,10 @@ function buildCreateTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
596
597
|
prompt: { type: 'string', description: 'The instruction sent to the execution agent; the title is used when blank.' },
|
|
597
598
|
parentId: { type: 'string', description: 'Parent task id, making this a subtask. Omit for a root task.' },
|
|
598
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).' },
|
|
599
|
-
mode: { type: 'string', description: 'Agent preset id the execution session is composed from; omit
|
|
600
|
-
permission: { type: 'string', enum: [...TASK_PERMISSIONS], description: 'Permission preset for the execution session
|
|
601
|
-
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.' },
|
|
602
604
|
reuseSession: { type: 'boolean', description: 'Continue later runs in the previous execution session instead of a fresh conversation.' },
|
|
603
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.' },
|
|
604
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.' },
|
|
@@ -645,6 +647,12 @@ function buildCreateTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
645
647
|
...(args.mode === undefined || args.mode === '' ? {} : { mode: args.mode }),
|
|
646
648
|
...(args.permission === undefined ? {} : { permission: args.permission }),
|
|
647
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,
|
|
648
656
|
...(args.reuseSession === true ? { reuseSession: true } : {}),
|
|
649
657
|
...(args.teamRun === true ? { teamRun: true } : {}),
|
|
650
658
|
...(args.goalRun === true ? { goalRun: true } : {}),
|
|
@@ -676,7 +684,7 @@ function buildUpdateTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
676
684
|
return defineTool({
|
|
677
685
|
name: 'task_board_update',
|
|
678
686
|
description: [
|
|
679
|
-
'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).',
|
|
680
688
|
'Only the fields you pass change; pass an empty string to clear a target, or an empty labels array to clear the labels.',
|
|
681
689
|
'Title, description and prompt freeze once the task has started executing; the execution targets stay editable because they only affect future runs.',
|
|
682
690
|
'Changing the pinned permission re-arms the permission confirmation gate.',
|
|
@@ -704,6 +712,7 @@ function buildUpdateTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
704
712
|
description: 'New permission preset; an empty string clears it back to the session default.',
|
|
705
713
|
},
|
|
706
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.' },
|
|
707
716
|
reuseSession: { type: 'boolean', description: 'Continue later runs in the previous execution session.' },
|
|
708
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).' },
|
|
709
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.' },
|
|
@@ -730,6 +739,10 @@ function buildUpdateTool(host: TaskBoardToolHost): ToolDefinition {
|
|
|
730
739
|
if (has(args, 'workspaceId') && typeof args.workspaceId === 'string') patch.workspaceId = args.workspaceId
|
|
731
740
|
if (has(args, 'mode') && typeof args.mode === 'string') patch.mode = args.mode
|
|
732
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
|
+
}
|
|
733
746
|
// An empty permission is the wire's clearing value (back to the session default).
|
|
734
747
|
if (has(args, 'permission') && typeof args.permission === 'string') {
|
|
735
748
|
patch.permission = args.permission === '' ? undefined : args.permission
|