@linxin666/dsh-client-ui-task-board 0.4.4 → 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 +42 -11
- package/README.zh.md +42 -11
- package/lib/client.js +4958 -472
- package/lib/client.js.map +1 -1
- package/lib/index.js +2841 -121
- 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 +4 -1
- 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/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/index.d.ts +19 -0
- package/lib/types/client/locales.d.ts +103 -1
- package/lib/types/client/native-panel.d.ts +6 -20
- 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 +37 -1
- 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/tasks.d.ts +84 -3
- 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 +52 -8
- package/lib/types/host-runner.d.ts +23 -1
- package/lib/types/host-service.d.ts +88 -2
- package/lib/types/index.d.ts +80 -26
- package/lib/types/protocol.d.ts +49 -5
- 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 +12 -7
- package/src/client/board/NewTaskModal.tsx +17 -1
- package/src/client/board/TagManagerModal.tsx +235 -0
- package/src/client/board/TaskBoard.tsx +259 -103
- package/src/client/board/TaskCard.tsx +214 -81
- package/src/client/board/TaskDetail.tsx +71 -26
- package/src/client/board/TaskForm.tsx +13 -3
- 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/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 +488 -63
- package/src/client/index.ts +43 -5
- package/src/client/locales.ts +208 -4
- package/src/client/native-panel.tsx +18 -2
- 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 +94 -3
- 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/store.ts +16 -0
- package/src/core/tasks.ts +100 -6
- package/src/core/use-cases/task-parent.ts +3 -2
- package/src/core/use-cases/task-tag.ts +110 -0
- package/src/core/use-cases/task-update.ts +10 -4
- package/src/core/verification.ts +826 -0
- package/src/core/workspace-target.ts +59 -0
- package/src/host/agent-tools.ts +20 -18
- 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 +143 -32
- package/src/host-routes.ts +15 -2
- package/src/host-runner.ts +60 -11
- package/src/host-service.ts +253 -10
- package/src/index.ts +204 -16
- package/src/protocol.ts +102 -8
- package/src/tool-surface.ts +95 -0
package/src/index.ts
CHANGED
|
@@ -19,15 +19,20 @@ import type { ToolDefinition } from '@deepseek-ai/dsh-tools'
|
|
|
19
19
|
import type { LlmRuntime } from '@deepseek-ai/dsh-llm'
|
|
20
20
|
import { TaskBoardHostService, type HostTimerFace, type TaskBoardTeamDispatcher } from './host-service.ts'
|
|
21
21
|
import { parseTaskDraft, TaskParseError } from './host-ai.ts'
|
|
22
|
-
import { TASK_PERMISSIONS, type TaskPermission } from './core/tasks.ts'
|
|
22
|
+
import { TASK_PERMISSIONS, isTaskPermission, type TaskPermission } from './core/tasks.ts'
|
|
23
23
|
import { DEFAULT_SUBTASK_DEPTH, SUBTASK_DEPTH_MAX, SUBTASK_DEPTH_MIN } from './core/subtask.ts'
|
|
24
24
|
import { DEFAULT_SESSION_PERMISSION } from './core/handover.ts'
|
|
25
|
-
import { buildTaskBoardTools } from './host/agent-tools.ts'
|
|
25
|
+
import { buildTaskBoardTools, TASK_BOARD_TOOL_NAMES } from './host/agent-tools.ts'
|
|
26
|
+
import { PLUGIN_TOOL_SECTION_ORDERS, visibleToolText } from './tool-surface.ts'
|
|
26
27
|
import { makeTaskBoardRoutes } from './host-routes.ts'
|
|
28
|
+
import { TASK_BOARD_SERVICE_NAME, type TaskBoardExtension } from './core/extension.ts'
|
|
27
29
|
import { mountOnce } from './mount-once.ts'
|
|
30
|
+
import { createGoalVerificationGate, type GoalFace } from './host/verification-gate.ts'
|
|
31
|
+
import { normalizeCatalog, type ModelCatalogView, type VerificationSettings } from './core/verification.ts'
|
|
32
|
+
import { probeWorkspaceChanges } from './host/workspace-evidence.ts'
|
|
28
33
|
|
|
29
|
-
/** Order of the announcement section within the tool-guidance band. */
|
|
30
|
-
const SECTION_ORDER =
|
|
34
|
+
/** Order of the announcement section within the shared tool-guidance band. */
|
|
35
|
+
const SECTION_ORDER = PLUGIN_TOOL_SECTION_ORDERS['task-board']
|
|
31
36
|
|
|
32
37
|
/** Default environment variable holding the authenticated proxy token. */
|
|
33
38
|
export const DEFAULT_PROXY_TOKEN_ENV = 'DSH_TASK_BOARD_PROXY_TOKEN'
|
|
@@ -68,9 +73,12 @@ export interface Config {
|
|
|
68
73
|
/** Environment variable whose value the authenticated proxy injects upstream. */
|
|
69
74
|
proxyTokenEnv?: string
|
|
70
75
|
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
76
|
+
* An explicit baseline for the permission confirmation gate. Left unset, the
|
|
77
|
+
* board follows the Host's own default permission preset — the value new DSH
|
|
78
|
+
* sessions start at — and falls back to `read-only` when the deployment
|
|
79
|
+
* serves no permission catalog. A card whose effective permission (handover
|
|
80
|
+
* bundle or pin) is above the baseline requires a human confirmation before
|
|
81
|
+
* it may run; cron refuses unconfirmed cards.
|
|
74
82
|
*/
|
|
75
83
|
sessionDefaultPermission?: TaskPermission
|
|
76
84
|
/**
|
|
@@ -86,6 +94,31 @@ export interface Config {
|
|
|
86
94
|
* only team-mode runs use it.
|
|
87
95
|
*/
|
|
88
96
|
teamProvider?: string
|
|
97
|
+
/**
|
|
98
|
+
* Goal acceptance for executions this board starts (default ON). When on,
|
|
99
|
+
* update_goal(action: complete) inside a task execution is refused until an
|
|
100
|
+
* acceptance pass is recorded for that execution: the three coding criteria,
|
|
101
|
+
* a 0.65 threshold, two rounds per criterion with the A/B slots swapped, and
|
|
102
|
+
* at most two acceptances per execution — the first failure returns its
|
|
103
|
+
* findings to the fixing agent, the second ends the cycle. The switch only
|
|
104
|
+
* affects goal-form executions of the board; plain chat and a task pinned to
|
|
105
|
+
* goalRun: false are untouched. When a separate third-party verifier also
|
|
106
|
+
* runs its own automatic acceptance, both judges score independently: this
|
|
107
|
+
* one gates completion, the other only steers, and no public interface lets
|
|
108
|
+
* the two share a verdict.
|
|
109
|
+
*/
|
|
110
|
+
goalVerification?: Volatile<boolean>
|
|
111
|
+
/**
|
|
112
|
+
* Judge model for goal acceptance as provider/model. Blank inherits the HOST's
|
|
113
|
+
* own model catalog default — never the card's pinned execution model.
|
|
114
|
+
*/
|
|
115
|
+
goalVerificationModel?: Volatile<string>
|
|
116
|
+
/**
|
|
117
|
+
* Reasoning effort for the judge. Blank inherits the host default level; an
|
|
118
|
+
* effort the resolved model does not declare is dropped instead of being
|
|
119
|
+
* sent, and the fallback to the model's own default is reported.
|
|
120
|
+
*/
|
|
121
|
+
goalVerificationReasoningEffort?: Volatile<string>
|
|
89
122
|
}
|
|
90
123
|
|
|
91
124
|
/**
|
|
@@ -95,22 +128,57 @@ export interface Config {
|
|
|
95
128
|
export const DEFAULT_TEAM_PROVIDER = 'spawn'
|
|
96
129
|
|
|
97
130
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
131
|
+
* Profile-patch shape of {@link Config}: what the Host validates the row's
|
|
132
|
+
* config against, before the schema turns volatile fields into live references
|
|
133
|
+
* and applies defaults. Declared separately because the two sides no longer
|
|
134
|
+
* share one shape, so the schema is annotated `z<ConfigInput, Config>` — the
|
|
135
|
+
* same split `dsh-git-graph` uses. The annotation is also what keeps the
|
|
136
|
+
* emitted declaration portable: an unannotated schema holding a nested object
|
|
137
|
+
* array infers a type that cannot be named without reaching into a transitive
|
|
138
|
+
* dependency's own types.
|
|
102
139
|
*/
|
|
103
|
-
export
|
|
140
|
+
export interface ConfigInput {
|
|
141
|
+
/** Announce the board in the system prompt. */
|
|
142
|
+
announceToAgent?: boolean
|
|
143
|
+
/** Master switch for the plugin. */
|
|
144
|
+
enabled?: boolean
|
|
145
|
+
/** Prevent idle system sleep while sessions run or schedules are armed. */
|
|
146
|
+
preventIdleSleep?: boolean
|
|
147
|
+
/** Canonical reverse-proxy Host authorities. */
|
|
148
|
+
trustedProxyHosts?: string[]
|
|
149
|
+
/** Environment variable whose value the authenticated proxy injects. */
|
|
150
|
+
proxyTokenEnv?: string
|
|
151
|
+
/** Explicit baseline for the permission confirmation gate; unset follows the Host default. */
|
|
152
|
+
sessionDefaultPermission?: TaskPermission
|
|
153
|
+
/** Subtask depth limit, 1..3. */
|
|
154
|
+
maxSubtaskDepth?: number
|
|
155
|
+
/** Continuable-subagent provider the Agent Teams service composes a teammate from. */
|
|
156
|
+
teamProvider?: string
|
|
157
|
+
/** Goal acceptance switch. */
|
|
158
|
+
goalVerification?: boolean
|
|
159
|
+
/** Judge model route for goal acceptance; blank inherits the host default. */
|
|
160
|
+
goalVerificationModel?: string
|
|
161
|
+
/** Judge reasoning effort; blank inherits the host default. */
|
|
162
|
+
goalVerificationReasoningEffort?: string
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export const Config: z<ConfigInput, Config> = z.object({
|
|
104
166
|
announceToAgent: z.boolean().default(false).volatile(),
|
|
105
167
|
enabled: z.boolean().default(true).volatile(),
|
|
106
168
|
preventIdleSleep: z.boolean().default(false).volatile(),
|
|
107
169
|
trustedProxyHosts: z.array(z.string()).default([]),
|
|
108
170
|
proxyTokenEnv: z.string().min(1).default(DEFAULT_PROXY_TOKEN_ENV),
|
|
109
|
-
sessionDefaultPermission: z.union(TASK_PERMISSIONS)
|
|
171
|
+
sessionDefaultPermission: z.union(TASK_PERMISSIONS),
|
|
110
172
|
maxSubtaskDepth: z.number().min(SUBTASK_DEPTH_MIN).max(SUBTASK_DEPTH_MAX).default(DEFAULT_SUBTASK_DEPTH).volatile(),
|
|
111
173
|
teamProvider: z.string().min(1).default(DEFAULT_TEAM_PROVIDER),
|
|
174
|
+
goalVerification: z.boolean().default(true).volatile(),
|
|
175
|
+
goalVerificationModel: z.string().default('').volatile(),
|
|
176
|
+
goalVerificationReasoningEffort: z.string().default('').volatile(),
|
|
112
177
|
})
|
|
113
178
|
|
|
179
|
+
/** Schema default for the goal-acceptance switch, re-read for hand-built contexts. */
|
|
180
|
+
export const DEFAULT_GOAL_VERIFICATION = true
|
|
181
|
+
|
|
114
182
|
declare module '@deepseek-ai/cordis' {
|
|
115
183
|
interface Events {
|
|
116
184
|
/**
|
|
@@ -160,6 +228,13 @@ export function resolveProxyAccess(config: Config | undefined, env: NodeJS.Proce
|
|
|
160
228
|
/** The registry face the agent tools register into. */
|
|
161
229
|
interface AgentToolRegistry {
|
|
162
230
|
register(definition: ToolDefinition): () => void
|
|
231
|
+
/** Scoped lookup; present on the real registry, absent on a stub that only registers. */
|
|
232
|
+
get?(name: string, scope?: unknown): unknown
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** The lookup half of a resolved registry, when it exposes one. */
|
|
236
|
+
function lookupOf(registry: AgentToolRegistry | undefined): { get: (name: string, scope?: unknown) => unknown } | undefined {
|
|
237
|
+
return registry !== undefined && typeof registry.get === 'function' ? { get: registry.get.bind(registry) } : undefined
|
|
163
238
|
}
|
|
164
239
|
|
|
165
240
|
/**
|
|
@@ -203,6 +278,33 @@ export function resolveHostTimers(ctx: Context): HostTimerFace | undefined {
|
|
|
203
278
|
/** Schema default, re-read for hand-built test contexts (the loader applies them normally). */
|
|
204
279
|
const DEFAULT_ANNOUNCE = false
|
|
205
280
|
|
|
281
|
+
/** The subset of the official permission-preset service the board reads. */
|
|
282
|
+
interface PermissionPresetsFace {
|
|
283
|
+
catalog(): { defaultPreset?: unknown }
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* The Host's own default permission preset, mapped into the board's vocabulary.
|
|
288
|
+
*
|
|
289
|
+
* This is the value new DSH sessions start at (the deployment's `permission`
|
|
290
|
+
* settings row), so a board that follows it treats an ordinary card exactly as
|
|
291
|
+
* the Host would. Absent when the deployment serves no permission catalog, or
|
|
292
|
+
* its default is a current-session identity (`auto`) that names no fixed
|
|
293
|
+
* sandbox mode; the caller then keeps the fail-safe baseline.
|
|
294
|
+
* @param ctx - the plugin context.
|
|
295
|
+
* @returns the Host default as a board permission, or undefined when unreadable.
|
|
296
|
+
*/
|
|
297
|
+
export function resolveHostDefaultPermission(ctx: Context): TaskPermission | undefined {
|
|
298
|
+
try {
|
|
299
|
+
const presets = ctx.get('permissionPresets') as PermissionPresetsFace | undefined
|
|
300
|
+
if (presets === undefined || typeof presets.catalog !== 'function') return undefined
|
|
301
|
+
const preset = presets.catalog()?.defaultPreset
|
|
302
|
+
return isTaskPermission(preset) ? preset : undefined
|
|
303
|
+
} catch {
|
|
304
|
+
return undefined
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
206
308
|
/**
|
|
207
309
|
* How long one teammate may take to reach its durable active or failed edge.
|
|
208
310
|
* Agent Teams provisioning normally resolves in seconds; the bound exists so a
|
|
@@ -317,11 +419,35 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
317
419
|
const preventIdleSleep = (): boolean => readConfigField(config?.preventIdleSleep, false)
|
|
318
420
|
/** Current subtask depth limit (read live: the settings card edits it in place). */
|
|
319
421
|
const maxSubtaskDepth = (): number => readConfigField(config?.maxSubtaskDepth, DEFAULT_SUBTASK_DEPTH)
|
|
422
|
+
/**
|
|
423
|
+
* The baseline the permission confirmation gate judges against: the row's own
|
|
424
|
+
* `sessionDefaultPermission` when the deployment pins one, otherwise the Host's
|
|
425
|
+
* default preset. Read per call through the ledger, so a Host Settings change
|
|
426
|
+
* reaches the running board without remounting the row; the fail-safe default
|
|
427
|
+
* still applies when no catalog is available.
|
|
428
|
+
*/
|
|
429
|
+
const sessionDefaultPermission = (): TaskPermission =>
|
|
430
|
+
config?.sessionDefaultPermission ?? resolveHostDefaultPermission(ctx) ?? DEFAULT_SESSION_PERMISSION
|
|
320
431
|
|
|
432
|
+
/** Live acceptance settings the contract of each new execution freezes. */
|
|
433
|
+
const verificationSettings = (): VerificationSettings => ({
|
|
434
|
+
enabled: readConfigField(config?.goalVerification, DEFAULT_GOAL_VERIFICATION),
|
|
435
|
+
model: readConfigField(config?.goalVerificationModel, ''),
|
|
436
|
+
reasoningEffort: readConfigField(config?.goalVerificationReasoningEffort, ''),
|
|
437
|
+
})
|
|
438
|
+
// The catalog reader needs the service, and the service needs the reader;
|
|
439
|
+
// the reader only runs after start(), so a late holder is enough.
|
|
440
|
+
let hostForCatalog: TaskBoardHostService | undefined
|
|
441
|
+
const verificationCatalog = async (): Promise<ModelCatalogView | undefined> => {
|
|
442
|
+
const runner = hostForCatalog?.runner
|
|
443
|
+
return runner === undefined ? undefined : normalizeCatalog(await runner.modelCatalog())
|
|
444
|
+
}
|
|
321
445
|
const host = new TaskBoardHostService(ctx.typertGateway, {
|
|
322
446
|
workspaceRegistry: ctx.workspaceRegistry,
|
|
323
|
-
sessionDefaultPermission
|
|
447
|
+
sessionDefaultPermission,
|
|
324
448
|
maxSubtaskDepth: maxSubtaskDepth(),
|
|
449
|
+
verificationSettings,
|
|
450
|
+
verificationCatalog,
|
|
325
451
|
timers: resolveHostTimers(ctx),
|
|
326
452
|
team: buildTeamDispatcher(ctx, config?.teamProvider ?? DEFAULT_TEAM_PROVIDER),
|
|
327
453
|
commandDispatcher: {
|
|
@@ -332,22 +458,41 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
332
458
|
},
|
|
333
459
|
},
|
|
334
460
|
})
|
|
461
|
+
hostForCatalog = host
|
|
335
462
|
// Configuration before start(): a disabled row must not take the first
|
|
336
463
|
// scheduler tick, which would roll schedules the board is not running.
|
|
337
464
|
host.setConfiguration(enabled(), preventIdleSleep())
|
|
338
465
|
host.start()
|
|
339
466
|
|
|
467
|
+
// External providers. The board publishes its registration service so any
|
|
468
|
+
// provider package can resolve it and admit itself; the board itself knows
|
|
469
|
+
// no provider vocabulary. A capture-only test context implements no service
|
|
470
|
+
// registry and the board still serves its own surfaces.
|
|
471
|
+
if (typeof (ctx as { provide?: unknown }).provide === 'function') {
|
|
472
|
+
ctx.provide(TASK_BOARD_SERVICE_NAME, {
|
|
473
|
+
registerExtension: (extension: TaskBoardExtension) => host.registerExtension(extension),
|
|
474
|
+
isExtensionEnabled: (extensionId: string) => host.extensions.isActive(extensionId),
|
|
475
|
+
})
|
|
476
|
+
}
|
|
477
|
+
|
|
340
478
|
// Agent tools: the same Host ledger the browser drives, so any session can
|
|
341
479
|
// list, create, link, run and settle board work. Registration follows the
|
|
342
480
|
// master switch (a disabled board answers no tool call), and the mount
|
|
343
|
-
// effect below owns the disposers.
|
|
481
|
+
// effect below owns the disposers. Provider tools register through the
|
|
482
|
+
// extension registry, which follows the same gate and rebinds when the tool
|
|
483
|
+
// registry appears late.
|
|
344
484
|
let disposeTools: (() => void) | undefined
|
|
345
485
|
const setToolsEnabled = (active: boolean): void => {
|
|
346
486
|
if (!active) {
|
|
487
|
+
// Releasing is the whole job on the way down. Rebinding the extension
|
|
488
|
+
// registry here would re-register every provider tool, and the teardown
|
|
489
|
+
// path runs this while the fiber is already unloading, so each of those
|
|
490
|
+
// re-registrations is a request the host can only refuse.
|
|
347
491
|
disposeTools?.()
|
|
348
492
|
disposeTools = undefined
|
|
349
493
|
return
|
|
350
494
|
}
|
|
495
|
+
host.extensions.setToolRegistry(() => resolveToolRegistry(ctx))
|
|
351
496
|
if (disposeTools !== undefined) return
|
|
352
497
|
const registry = resolveToolRegistry(ctx)
|
|
353
498
|
if (registry === undefined) return
|
|
@@ -364,6 +509,9 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
364
509
|
if (typeof scopedInject === 'function') {
|
|
365
510
|
scopedInject.call(ctx, ['tools'], () => {
|
|
366
511
|
setToolsEnabled(enabled())
|
|
512
|
+
// The extension registry follows the same rebinding: a registry that
|
|
513
|
+
// appears (or is replaced) after a provider started adopts its tools.
|
|
514
|
+
host.extensions.setToolRegistry(() => resolveToolRegistry(ctx))
|
|
367
515
|
// Cordis unloads and re-runs this callback when the injected service's
|
|
368
516
|
// provider fiber changes, and the old registry dies with its provider.
|
|
369
517
|
// Releasing the guard here is what lets the callback register into the
|
|
@@ -378,6 +526,42 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
378
526
|
ctx.effect(() => {
|
|
379
527
|
const disposers: Array<() => void> = []
|
|
380
528
|
try {
|
|
529
|
+
// The goal acceptance gate: the OFFICIAL tool pre-execution lifecycle,
|
|
530
|
+
// installed before the tool body runs, so a completion claim cannot take
|
|
531
|
+
// effect without a matching acceptance pass record.
|
|
532
|
+
const gate = createGoalVerificationGate({
|
|
533
|
+
ledger: host.ledger,
|
|
534
|
+
llm: () => resolveLlmRuntime(ctx),
|
|
535
|
+
goals: () => {
|
|
536
|
+
try {
|
|
537
|
+
return ctx.get('goals') as GoalFace | undefined
|
|
538
|
+
} catch {
|
|
539
|
+
return undefined
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
// The host's own observation of what a turn changed on disk, when this
|
|
543
|
+
// deployment records it; the acceptance degrades to the trajectory alone
|
|
544
|
+
// when it does not.
|
|
545
|
+
workspaceChanges: () => probeWorkspaceChanges(ctx),
|
|
546
|
+
logger: {
|
|
547
|
+
warn: (message: string, ...rest: unknown[]) => {
|
|
548
|
+
const logger = (ctx as { logger?: { warn?: (...args: unknown[]) => void } }).logger
|
|
549
|
+
if (typeof logger?.warn === 'function') logger.warn(message, ...rest)
|
|
550
|
+
else console.warn(message, ...rest)
|
|
551
|
+
},
|
|
552
|
+
},
|
|
553
|
+
})
|
|
554
|
+
// A capture-only context may implement no listener registry (it returns
|
|
555
|
+
// no disposer), so the handle is pushed only when it really is one.
|
|
556
|
+
const offGate = ctx.on('tools/pre-execute', async (exec, next) => {
|
|
557
|
+
const decision = await gate({ name: exec.name, arguments: exec.arguments, agent: exec.agent, signal: exec.signal })
|
|
558
|
+
return decision ?? next()
|
|
559
|
+
})
|
|
560
|
+
if (typeof offGate === 'function') disposers.push(offGate)
|
|
561
|
+
else if (typeof (offGate as { dispose?: unknown } | undefined)?.dispose === 'function') {
|
|
562
|
+
const handle = offGate as { dispose(): void }
|
|
563
|
+
disposers.push(() => { handle.dispose() })
|
|
564
|
+
}
|
|
381
565
|
const routes = makeTaskBoardRoutes(host, resolveProxyAccess(config), {
|
|
382
566
|
parseTask: async (request, signal) => {
|
|
383
567
|
const llm = resolveLlmRuntime(ctx)
|
|
@@ -426,7 +610,11 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
426
610
|
disposeSection = ctx.systemPrompt.section({
|
|
427
611
|
name: 'plugin:task-board',
|
|
428
612
|
order: SECTION_ORDER,
|
|
429
|
-
|
|
613
|
+
// The announcement names the task_board_* tools, so it renders only while
|
|
614
|
+
// at least one of them is reachable: a runtime that serves no tool
|
|
615
|
+
// registry, or a restriction that withholds them, must not leave guidance
|
|
616
|
+
// telling the model to call tools this session cannot see.
|
|
617
|
+
text: visibleToolText(lookupOf(resolveToolRegistry(ctx)), TASK_BOARD_TOOL_NAMES, TASK_BOARD_GUIDANCE),
|
|
430
618
|
})
|
|
431
619
|
}
|
|
432
620
|
|
package/src/protocol.ts
CHANGED
|
@@ -1,18 +1,33 @@
|
|
|
1
1
|
import type { TaskUpdatePatch } from './core/use-cases/task-update.ts'
|
|
2
|
-
import { isTaskPermission, isTaskStatus, isTaskTagList, type NewTaskInput, type TaskPermission, type TaskRecord, type TaskStatus } from './core/tasks.ts'
|
|
2
|
+
import { TAG_NAME_MAX_LENGTH, isTaskPermission, isTaskStatus, isTaskTagList, type NewTaskInput, type TaskPermission, type TaskRecord, type TaskStatus } from './core/tasks.ts'
|
|
3
3
|
import { parseLedger } from './core/store.ts'
|
|
4
4
|
import { isValidTimeZone } from './core/schedule.ts'
|
|
5
5
|
import { sanitizeFreezeSnapshot, type FreezeSnapshot } from './core/freeze-snapshot.ts'
|
|
6
6
|
import { sanitizeHandover, type TaskHandoverInput } from './core/handover.ts'
|
|
7
|
+
import { isTaskBoardExtensionPayload, normalizeTaskIntegrations, type TaskBoardExtensionPayload } from './core/extension.ts'
|
|
8
|
+
import { normalizeVerification, type ModelCatalogView, type VerificationContract, type VerificationSettings } from './core/verification.ts'
|
|
7
9
|
|
|
8
10
|
/** Freeze payload carried by create/update actions after the gate (redacted in place). */
|
|
9
11
|
type FreezePayload = FreezeSnapshot & { redacted?: boolean; frozenBy?: string }
|
|
10
12
|
|
|
11
|
-
export const TASK_BOARD_SCHEMA_VERSION =
|
|
12
|
-
/** Ledger documents written before
|
|
13
|
-
export const TASK_BOARD_LEGACY_SCHEMA_VERSION =
|
|
14
|
-
/** Ledger documents written before
|
|
15
|
-
export const TASK_BOARD_OLDER_SCHEMA_VERSION =
|
|
13
|
+
export const TASK_BOARD_SCHEMA_VERSION = 5 as const
|
|
14
|
+
/** Ledger documents written before v5; loaded once and migrated on startup. */
|
|
15
|
+
export const TASK_BOARD_LEGACY_SCHEMA_VERSION = 4 as const
|
|
16
|
+
/** Ledger documents written before v4; migrated through the v4 normalization too. */
|
|
17
|
+
export const TASK_BOARD_OLDER_SCHEMA_VERSION = 3 as const
|
|
18
|
+
/** Ledger documents written before v3; migrated through every later normalization too. */
|
|
19
|
+
export const TASK_BOARD_OLDEST_SCHEMA_VERSION = 2 as const
|
|
20
|
+
/**
|
|
21
|
+
* Every older generation the loader migrates in place. v5 only ADDS the
|
|
22
|
+
* per-execution acceptance block, so the migration is the normalization pass
|
|
23
|
+
* that re-validates each row; a document from any listed generation upgrades
|
|
24
|
+
* losslessly.
|
|
25
|
+
*/
|
|
26
|
+
export const TASK_BOARD_MIGRATABLE_SCHEMA_VERSIONS: readonly number[] = [
|
|
27
|
+
TASK_BOARD_LEGACY_SCHEMA_VERSION,
|
|
28
|
+
TASK_BOARD_OLDER_SCHEMA_VERSION,
|
|
29
|
+
TASK_BOARD_OLDEST_SCHEMA_VERSION,
|
|
30
|
+
]
|
|
16
31
|
export const TASK_BOARD_API_PREFIX = '/api/task-board'
|
|
17
32
|
|
|
18
33
|
export type PowerPhase = 'disabled' | 'idle' | 'acquiring' | 'active' | 'error' | 'unsupported'
|
|
@@ -53,6 +68,25 @@ export interface TaskBoardSnapshot {
|
|
|
53
68
|
* into team execution (Team Lead session plus one teammate per subtask).
|
|
54
69
|
*/
|
|
55
70
|
teamRunAvailable?: boolean
|
|
71
|
+
/**
|
|
72
|
+
* Read-only summaries published by running extensions, keyed by extension id.
|
|
73
|
+
* The board forwards them verbatim; it never interprets their shape.
|
|
74
|
+
*/
|
|
75
|
+
extensions?: Record<string, unknown>
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Body of `GET {TASK_BOARD_API_PREFIX}/verification`: the acceptance settings the
|
|
80
|
+
* card edits, the contract they resolve to right now, and the host model catalog
|
|
81
|
+
* the model and reasoning choices come from. Served through the same loopback /
|
|
82
|
+
* authenticated-proxy guard as the rest of the board API, and it carries no
|
|
83
|
+
* credential material — model routes and level ids only.
|
|
84
|
+
*/
|
|
85
|
+
export interface TaskBoardVerificationOptions {
|
|
86
|
+
settings: VerificationSettings
|
|
87
|
+
/** The RESOLVED contract, so the card can show what the next execution freezes. */
|
|
88
|
+
contract: VerificationContract
|
|
89
|
+
catalog: ModelCatalogView
|
|
56
90
|
}
|
|
57
91
|
|
|
58
92
|
/** SSE event frame: revision/scheduler/power only, never the task list. */
|
|
@@ -110,6 +144,10 @@ export type TaskBoardAction =
|
|
|
110
144
|
| { kind: 'rerun'; taskId: string }
|
|
111
145
|
| { kind: 'confirm-permission'; taskId: string }
|
|
112
146
|
| { kind: 'set-parent'; taskId: string; parentId: string | null }
|
|
147
|
+
/** Ledger-wide label management; see core/use-cases/task-tag.ts. */
|
|
148
|
+
| { kind: 'rename-tag'; from: string; to: string }
|
|
149
|
+
| { kind: 'delete-tag'; name: string }
|
|
150
|
+
| { kind: 'extension-action'; extensionId: string; action: string; taskId?: string; payload?: TaskBoardExtensionPayload }
|
|
113
151
|
|
|
114
152
|
export interface TaskBoardActionEnvelope {
|
|
115
153
|
requestId: string
|
|
@@ -154,6 +192,7 @@ function validImportedKnownFields(value: Record<string, unknown>): boolean {
|
|
|
154
192
|
// must carry a well-formed list or none at all, so a hand-edited export
|
|
155
193
|
// cannot smuggle a malformed tag past the gate.
|
|
156
194
|
if (value.tags !== undefined && !isTaskTagList(value.tags)) return false
|
|
195
|
+
if (value.integrations !== undefined && normalizeTaskIntegrations(value.integrations) === undefined) return false
|
|
157
196
|
if (value.schedule !== undefined) {
|
|
158
197
|
const schedule = record(value.schedule)
|
|
159
198
|
if (schedule === undefined || typeof schedule.enabled !== 'boolean' || typeof schedule.cron !== 'string') return false
|
|
@@ -172,6 +211,10 @@ function validImportedKnownFields(value: Record<string, unknown>): boolean {
|
|
|
172
211
|
if (execution.initiatedBy !== undefined && typeof execution.initiatedBy !== 'string') return false
|
|
173
212
|
if (execution.frozenBy !== undefined && typeof execution.frozenBy !== 'string') return false
|
|
174
213
|
if (execution.frozenAt !== undefined && typeof execution.frozenAt !== 'number') return false
|
|
214
|
+
// A well-formed acceptance block may be imported for inspection, but the
|
|
215
|
+
// import path DROPS it (see importedTask): a fabricated pass record must
|
|
216
|
+
// never let an imported execution settle as verified.
|
|
217
|
+
if (execution.verification !== undefined && normalizeVerification(execution.verification) === undefined) return false
|
|
175
218
|
}
|
|
176
219
|
}
|
|
177
220
|
return true
|
|
@@ -200,6 +243,8 @@ function importedTask(value: unknown): TaskRecord | undefined {
|
|
|
200
243
|
...(execution.initiatedBy === undefined ? {} : { initiatedBy: execution.initiatedBy }),
|
|
201
244
|
...(execution.frozenAt === undefined ? {} : { frozenAt: execution.frozenAt }),
|
|
202
245
|
...(execution.frozenBy === undefined ? {} : { frozenBy: execution.frozenBy }),
|
|
246
|
+
// 安全门(对抗场景 d):验收报告是 Host 的判定,不接受 import 携带——
|
|
247
|
+
// 一条伪造的通过记录会让导入的执行被当成已验收。
|
|
203
248
|
})),
|
|
204
249
|
...(task.schedule === undefined ? {} : {
|
|
205
250
|
schedule: {
|
|
@@ -218,10 +263,16 @@ function importedTask(value: unknown): TaskRecord | undefined {
|
|
|
218
263
|
...(task.permission === undefined ? {} : { permission: task.permission }),
|
|
219
264
|
...(task.reuseSession === undefined ? {} : { reuseSession: task.reuseSession }),
|
|
220
265
|
...(task.goalRun === undefined ? {} : { goalRun: task.goalRun }),
|
|
266
|
+
// The per-card acceptance opt-out rides the import like the /goal opt-in:
|
|
267
|
+
// it is a user-chosen execution preference on their own board, not a
|
|
268
|
+
// verdict. The per-execution `verification` block is still stripped below, so
|
|
269
|
+
// no imported card can arrive already accepted.
|
|
270
|
+
...(task.skipVerification === undefined ? {} : { skipVerification: task.skipVerification }),
|
|
221
271
|
...(task.archivedAt === undefined ? {} : { archivedAt: task.archivedAt }),
|
|
222
272
|
...(task.freeze === undefined ? {} : { freeze: task.freeze }),
|
|
223
273
|
...(task.handover === undefined ? {} : { handover: task.handover }),
|
|
224
274
|
...(task.tags === undefined ? {} : { tags: task.tags }),
|
|
275
|
+
...(task.integrations === undefined ? {} : { integrations: task.integrations }),
|
|
225
276
|
// 安全门(对抗场景 b):import 不是人工确认动作,确认戳一律剥除——
|
|
226
277
|
// 高于会话默认权限的绑定经 import 进入后必须重新武装 confirm-permission 门。
|
|
227
278
|
}
|
|
@@ -255,15 +306,17 @@ function handoverPayload(value: unknown): TaskHandoverInput | undefined {
|
|
|
255
306
|
|
|
256
307
|
function createInput(value: unknown): value is NewTaskInput {
|
|
257
308
|
const input = record(value)
|
|
258
|
-
if (input === undefined || !exactKeys(input, ['title', 'description', 'prompt', 'parentId', 'workspaceId', 'mode', 'permission', 'schedule', 'freeze', 'handover', 'model', 'reuseSession', 'teamRun', 'goalRun', 'tags'])) return false
|
|
309
|
+
if (input === undefined || !exactKeys(input, ['title', 'description', 'prompt', 'parentId', 'workspaceId', 'mode', 'permission', 'schedule', 'freeze', 'handover', 'model', 'reuseSession', 'teamRun', 'goalRun', 'skipVerification', 'tags', 'integrations'])) return false
|
|
259
310
|
if (input.parentId !== undefined && (typeof input.parentId !== 'string' || input.parentId.trim() === '')) return false
|
|
260
311
|
if (typeof input.title !== 'string' || typeof input.description !== 'string' || typeof input.prompt !== 'string') return false
|
|
261
312
|
if (!optionalString(input.workspaceId) || !optionalString(input.mode) || !optionalString(input.model)) return false
|
|
262
313
|
if (input.reuseSession !== undefined && typeof input.reuseSession !== 'boolean') return false
|
|
263
314
|
if (input.teamRun !== undefined && typeof input.teamRun !== 'boolean') return false
|
|
264
315
|
if (input.goalRun !== undefined && typeof input.goalRun !== 'boolean') return false
|
|
316
|
+
if (input.skipVerification !== undefined && typeof input.skipVerification !== 'boolean') return false
|
|
265
317
|
if (input.permission !== undefined && !isTaskPermission(input.permission)) return false
|
|
266
318
|
if (input.tags !== undefined && !isTaskTagList(input.tags)) return false
|
|
319
|
+
if (input.integrations !== undefined && normalizeTaskIntegrations(input.integrations) === undefined) return false
|
|
267
320
|
if (input.freeze !== undefined && freezePayload(input.freeze) === undefined) return false
|
|
268
321
|
if (input.handover !== undefined && handoverPayload(input.handover) === undefined) return false
|
|
269
322
|
if (input.schedule !== undefined) {
|
|
@@ -278,13 +331,16 @@ function createInput(value: unknown): value is NewTaskInput {
|
|
|
278
331
|
|
|
279
332
|
function updatePatch(value: unknown): boolean {
|
|
280
333
|
const patch = record(value)
|
|
281
|
-
if (patch === undefined || !exactKeys(patch, ['title', 'description', 'prompt', 'workspaceId', 'mode', 'permission', 'freeze', 'handover', 'model', 'reuseSession', 'teamRun', 'goalRun', 'tags'])) return false
|
|
334
|
+
if (patch === undefined || !exactKeys(patch, ['title', 'description', 'prompt', 'workspaceId', 'mode', 'permission', 'freeze', 'handover', 'model', 'reuseSession', 'teamRun', 'goalRun', 'skipVerification', 'tags'])) return false
|
|
282
335
|
// null (or false) clears the reuse opt-in; only a real boolean is accepted.
|
|
283
336
|
if (patch.reuseSession !== undefined && patch.reuseSession !== null && typeof patch.reuseSession !== 'boolean') return false
|
|
284
337
|
if (patch.teamRun !== undefined && patch.teamRun !== null && typeof patch.teamRun !== 'boolean') return false
|
|
285
338
|
// The goal opt-in is tri-state: null/true return the card to its default
|
|
286
339
|
// (goal run), false pins a single plain turn.
|
|
287
340
|
if (patch.goalRun !== undefined && patch.goalRun !== null && typeof patch.goalRun !== 'boolean') return false
|
|
341
|
+
// The acceptance opt-out is the mirror tri-state: null/false return the card
|
|
342
|
+
// to inheriting the board-wide switch, true pins it out of the gate.
|
|
343
|
+
if (patch.skipVerification !== undefined && patch.skipVerification !== null && typeof patch.skipVerification !== 'boolean') return false
|
|
288
344
|
for (const key of ['title', 'description', 'prompt', 'workspaceId', 'mode', 'model'] as const) {
|
|
289
345
|
if (!optionalString(patch[key])) return false
|
|
290
346
|
}
|
|
@@ -297,6 +353,15 @@ function updatePatch(value: unknown): boolean {
|
|
|
297
353
|
return patch.handover === undefined || patch.handover === null || handoverPayload(patch.handover) !== undefined
|
|
298
354
|
}
|
|
299
355
|
|
|
356
|
+
/**
|
|
357
|
+
* A label name on the wire: a non-empty trimmed string within the tag cap. The
|
|
358
|
+
* trim/merge/duplicate rules belong to the use case; this gate only keeps a
|
|
359
|
+
* malformed name off the ledger.
|
|
360
|
+
*/
|
|
361
|
+
function isTagName(value: unknown): value is string {
|
|
362
|
+
return typeof value === 'string' && value.trim() !== '' && value.trim().length <= TAG_NAME_MAX_LENGTH
|
|
363
|
+
}
|
|
364
|
+
|
|
300
365
|
function schedulePatch(value: unknown): boolean {
|
|
301
366
|
const patch = record(value)
|
|
302
367
|
return patch !== undefined
|
|
@@ -368,6 +433,16 @@ function parseEnvelopeAction(value: unknown): TaskBoardActionEnvelope | undefine
|
|
|
368
433
|
if (parentId !== null && (typeof parentId !== 'string' || parentId.trim() === '')) return undefined
|
|
369
434
|
return { requestId: envelope.requestId, action: { kind: 'set-parent', taskId, parentId } }
|
|
370
435
|
}
|
|
436
|
+
case 'rename-tag': {
|
|
437
|
+
if (!exactKeys(action, ['kind', 'from', 'to'])) return undefined
|
|
438
|
+
if (!isTagName(action.from) || !isTagName(action.to)) return undefined
|
|
439
|
+
return { requestId: envelope.requestId, action: { kind: 'rename-tag', from: action.from, to: action.to } }
|
|
440
|
+
}
|
|
441
|
+
case 'delete-tag': {
|
|
442
|
+
if (!exactKeys(action, ['kind', 'name'])) return undefined
|
|
443
|
+
if (!isTagName(action.name)) return undefined
|
|
444
|
+
return { requestId: envelope.requestId, action: { kind: 'delete-tag', name: action.name } }
|
|
445
|
+
}
|
|
371
446
|
case 'set-schedule':
|
|
372
447
|
if (!exactKeys(action, ['kind', 'taskId', 'patch'])) return undefined
|
|
373
448
|
return taskId !== undefined && schedulePatch(action.patch)
|
|
@@ -378,6 +453,25 @@ function parseEnvelopeAction(value: unknown): TaskBoardActionEnvelope | undefine
|
|
|
378
453
|
return taskId !== undefined && isTaskStatus(action.status)
|
|
379
454
|
? { requestId: envelope.requestId, action: action as unknown as Extract<TaskBoardAction, { kind: 'move' }> }
|
|
380
455
|
: undefined
|
|
456
|
+
case 'extension-action': {
|
|
457
|
+
// Structural gate only: whether the id names a registered, enabled
|
|
458
|
+
// extension is decided by the host registry, which owns that state.
|
|
459
|
+
if (!exactKeys(action, ['kind', 'extensionId', 'action', 'taskId', 'payload'])) return undefined
|
|
460
|
+
if (typeof action.extensionId !== 'string' || action.extensionId.trim() === '' || action.extensionId.length > 128) return undefined
|
|
461
|
+
if (typeof action.action !== 'string' || action.action.trim() === '' || action.action.length > 128) return undefined
|
|
462
|
+
if (action.taskId !== undefined && (typeof action.taskId !== 'string' || action.taskId === '')) return undefined
|
|
463
|
+
if (action.payload !== undefined && !isTaskBoardExtensionPayload(action.payload)) return undefined
|
|
464
|
+
return {
|
|
465
|
+
requestId: envelope.requestId,
|
|
466
|
+
action: {
|
|
467
|
+
kind: 'extension-action',
|
|
468
|
+
extensionId: action.extensionId,
|
|
469
|
+
action: action.action,
|
|
470
|
+
...(action.taskId === undefined ? {} : { taskId: action.taskId }),
|
|
471
|
+
...(action.payload === undefined ? {} : { payload: action.payload }),
|
|
472
|
+
},
|
|
473
|
+
}
|
|
474
|
+
}
|
|
381
475
|
case 'confirm-permission':
|
|
382
476
|
case 'delete':
|
|
383
477
|
case 'archive':
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Generated by scripts/sync-shared.mjs from shared/host/tool-surface.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
|
|
2
|
+
/**
|
|
3
|
+
* Family-shared tool-surface conventions for the packages that register
|
|
4
|
+
* model-facing tools.
|
|
5
|
+
*
|
|
6
|
+
* One home for the prompt-section placement and the visibility gate this
|
|
7
|
+
* family previously restated per package.
|
|
8
|
+
*
|
|
9
|
+
* Modelled on the official tool packages:
|
|
10
|
+
* - `@deepseek-ai/dsh-tool-subagent` renders its section through
|
|
11
|
+
* `tools.get(name, context.scope) === undefined ? '' : text`, so guidance
|
|
12
|
+
* disappears together with a restricted tool.
|
|
13
|
+
* - 21 of the 22 official tool packages register their tools GLOBALLY and gate
|
|
14
|
+
* them with a setting, not per agent. Only the Agent Teams tool package —
|
|
15
|
+
* whose tools belong to one membership — installs into each `agent.ctx`.
|
|
16
|
+
* This family follows the majority: registration stays global so the
|
|
17
|
+
* registry's per-scope `restrict()` mask (which filters inherited global
|
|
18
|
+
* tools and REJECTS scope-local names) can still withhold these tools.
|
|
19
|
+
*
|
|
20
|
+
* Structural types only: this copy lands in packages that do not all depend on
|
|
21
|
+
* the same SDK service packages.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Prompt-section order for each package's tool guidance.
|
|
26
|
+
*
|
|
27
|
+
* These sit in the family band between the deployment persona and the SDK's
|
|
28
|
+
* `PLAN_POLICY` (500). Values are owned here rather than written per package so
|
|
29
|
+
* the host plugins whose guidance shares one prompt cannot drift into an
|
|
30
|
+
* accidental tie.
|
|
31
|
+
*/
|
|
32
|
+
export const PLUGIN_TOOL_SECTION_ORDERS = {
|
|
33
|
+
ssh: 150,
|
|
34
|
+
'task-board': 200,
|
|
35
|
+
} as const
|
|
36
|
+
|
|
37
|
+
/** One plugin id keyed by {@link PLUGIN_TOOL_SECTION_ORDERS}. */
|
|
38
|
+
export type PluginToolSectionId = keyof typeof PLUGIN_TOOL_SECTION_ORDERS
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Order of an external provider extension's own guidance section, right after
|
|
42
|
+
* the band of the plugin it extends.
|
|
43
|
+
*
|
|
44
|
+
* An extension names itself, not the plugin it plugs into, so its order stays
|
|
45
|
+
* out of {@link PLUGIN_TOOL_SECTION_ORDERS}: every package's copy of this module
|
|
46
|
+
* then carries no provider id. The slot still has one owner and cannot collide
|
|
47
|
+
* with a host plugin's band.
|
|
48
|
+
*/
|
|
49
|
+
export const EXTENSION_TOOL_SECTION_ORDER = 210
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The scoped tool lookup a section provider needs: the registry read for one
|
|
53
|
+
* assembly's scope. Matches `ctx.tools.get(name, scope)`.
|
|
54
|
+
*/
|
|
55
|
+
export interface ScopedToolLookup {
|
|
56
|
+
get(name: string, scope?: unknown): unknown
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** One assembly context carrying the scope the section is rendered for. */
|
|
60
|
+
export interface SectionScopeContext {
|
|
61
|
+
scope?: unknown
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Build a section provider that renders \`text\` only while at least one of
|
|
66
|
+
* \`toolNames\` is visible to the rendering scope.
|
|
67
|
+
*
|
|
68
|
+
* The native convention: a tool that a restriction, a preset, or a missing
|
|
69
|
+
* registry removed must not leave guidance behind telling the model to call it.
|
|
70
|
+
* An unreadable registry renders the text unchanged — guidance that outlives a
|
|
71
|
+
* momentary read failure is recoverable; guidance for a tool that is gone is
|
|
72
|
+
* not.
|
|
73
|
+
* @param tools - the scoped tool registry, when the deployment serves one.
|
|
74
|
+
* @param toolNames - registered names the guidance describes.
|
|
75
|
+
* @param text - the guidance rendered while they are reachable.
|
|
76
|
+
* @returns the section text provider.
|
|
77
|
+
*/
|
|
78
|
+
export function visibleToolText(
|
|
79
|
+
tools: ScopedToolLookup | undefined,
|
|
80
|
+
toolNames: readonly string[],
|
|
81
|
+
text: string,
|
|
82
|
+
): (context: SectionScopeContext) => string {
|
|
83
|
+
if (tools === undefined) return () => text
|
|
84
|
+
return (context: SectionScopeContext) => {
|
|
85
|
+
for (const name of toolNames) {
|
|
86
|
+
try {
|
|
87
|
+
if (tools.get(name, context.scope) !== undefined) return text
|
|
88
|
+
} catch {
|
|
89
|
+
// A refused lookup is not proof the tool is gone; keep the guidance.
|
|
90
|
+
return text
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return ''
|
|
94
|
+
}
|
|
95
|
+
}
|