@cat-factory/app 0.186.0 → 0.188.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/app/components/consensus/ConsensusSessionWindow.vue +11 -0
- package/app/components/fragments/FragmentLibraryManager.vue +39 -2
- package/app/components/palettes/AgentPalette.vue +12 -1
- package/app/components/palettes/AgentTierSelect.vue +67 -0
- package/app/components/pipeline/PipelineBuilder.vue +166 -92
- package/app/components/settings/ConsensusGroupsSection.vue +498 -0
- package/app/components/settings/ModelConfigurationPanel.vue +34 -2
- package/app/composables/api/presets.ts +22 -0
- package/app/modular/agent-kinds.spec.ts +9 -3
- package/app/modular/agent-kinds.ts +4 -0
- package/app/stores/agentTier.spec.ts +24 -0
- package/app/stores/agentTier.ts +42 -0
- package/app/stores/consensusGroups.ts +77 -0
- package/app/stores/pipelines/draftActions.ts +11 -113
- package/app/stores/pipelines/draftStepConfig.ts +157 -0
- package/app/stores/workspace/hydrate.ts +2 -0
- package/app/types/consensus.ts +3 -0
- package/app/types/domain.ts +8 -1
- package/app/utils/agentTier.spec.ts +59 -0
- package/app/utils/agentTier.ts +49 -0
- package/app/utils/catalog.spec.ts +12 -0
- package/app/utils/catalog.ts +58 -2
- package/i18n/locales/de.json +61 -2
- package/i18n/locales/en.json +68 -2
- package/i18n/locales/es.json +61 -2
- package/i18n/locales/fr.json +61 -2
- package/i18n/locales/he.json +61 -2
- package/i18n/locales/it.json +61 -2
- package/i18n/locales/ja.json +61 -2
- package/i18n/locales/pl.json +61 -2
- package/i18n/locales/tr.json +61 -2
- package/i18n/locales/uk.json +61 -2
- package/package.json +2 -2
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
import type { AgentTier } from '~/types/domain'
|
|
4
|
+
import { DEFAULT_AGENT_TIER_LEVEL, isAgentTier } from '~/utils/agentTier'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* How deep into the agent catalog the two catalog surfaces reach — the pipeline builder's
|
|
8
|
+
* palette and the model preset's per-agent override list.
|
|
9
|
+
*
|
|
10
|
+
* ONE persisted preference shared by both, not a per-surface one: the two are halves of the
|
|
11
|
+
* same job (pick the agents a pipeline runs, then pick what each of them runs on), so a user
|
|
12
|
+
* who widened the palette to reach a specialist kind expects to find that same kind when they
|
|
13
|
+
* go to pin its model. Persisted like the interface mode's own preference, so the choice
|
|
14
|
+
* survives a reload.
|
|
15
|
+
*
|
|
16
|
+
* Distinct from `uiMode` (the SPA-wide basic/advanced interface tier) on purpose — see
|
|
17
|
+
* `utils/agentTier.ts` for why the axes stay separate.
|
|
18
|
+
*/
|
|
19
|
+
export const useAgentTierStore = defineStore(
|
|
20
|
+
'agentTier',
|
|
21
|
+
() => {
|
|
22
|
+
/** The selected level, persisted. Seeded to the everyday-loop default. */
|
|
23
|
+
const storedTier = ref<AgentTier>(DEFAULT_AGENT_TIER_LEVEL)
|
|
24
|
+
|
|
25
|
+
// The restored value is untrusted input (a blob written by an older build, or hand-edited),
|
|
26
|
+
// and a catalog filtered on an unknown level would show nothing at all — so fall back
|
|
27
|
+
// rather than trust it, exactly as `uiMode` does with its persisted mode.
|
|
28
|
+
const tier = computed<AgentTier>(() =>
|
|
29
|
+
isAgentTier(storedTier.value) ? storedTier.value : DEFAULT_AGENT_TIER_LEVEL,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
/** Whether the widest level is selected, i.e. the whole catalog is showing. */
|
|
33
|
+
const showsAll = computed(() => tier.value === 'advanced')
|
|
34
|
+
|
|
35
|
+
function setTier(next: AgentTier) {
|
|
36
|
+
storedTier.value = next
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { tier, showsAll, storedTier, setTier }
|
|
40
|
+
},
|
|
41
|
+
{ persist: { pick: ['storedTier'] } },
|
|
42
|
+
)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
|
+
import type {
|
|
4
|
+
ConsensusGroup,
|
|
5
|
+
CreateConsensusGroupInput,
|
|
6
|
+
UpdateConsensusGroupInput,
|
|
7
|
+
} from '~/types/consensus'
|
|
8
|
+
import { useWorkspaceStore } from '~/stores/workspace'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The workspace's consensus-GROUP library: the reusable, estimate-gated review panels a pipeline
|
|
12
|
+
* step escalates to. Hydrated from the workspace snapshot (like the model presets) and managed
|
|
13
|
+
* from the Model Configuration settings screen; the pipeline builder reads it to offer a step's
|
|
14
|
+
* tier set.
|
|
15
|
+
*
|
|
16
|
+
* A workspace that has authored no group simply has nothing to escalate to, and every consensus
|
|
17
|
+
* step runs the inline participants written on it — so an empty library is a valid, quiet state,
|
|
18
|
+
* not an error.
|
|
19
|
+
*/
|
|
20
|
+
export const useConsensusGroupsStore = defineStore('consensusGroups', () => {
|
|
21
|
+
const api = useApi()
|
|
22
|
+
|
|
23
|
+
const groups = ref<ConsensusGroup[]>([])
|
|
24
|
+
|
|
25
|
+
function hydrate(list: ConsensusGroup[]) {
|
|
26
|
+
groups.value = [...list].sort((a, b) => a.createdAt - b.createdAt)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Whether the workspace has any tier to escalate to (drives the builder's empty state). */
|
|
30
|
+
const hasGroups = computed(() => groups.value.length > 0)
|
|
31
|
+
|
|
32
|
+
/** Resolve ids to groups, dropping any the library no longer holds. */
|
|
33
|
+
function resolve(ids: readonly string[] | undefined): ConsensusGroup[] {
|
|
34
|
+
if (!ids?.length) return []
|
|
35
|
+
return ids
|
|
36
|
+
.map((id) => groups.value.find((g) => g.id === id))
|
|
37
|
+
.filter((g): g is ConsensusGroup => !!g)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The estimate bar a group sets, as a display number: the highest threshold it names, or null
|
|
42
|
+
* when it is ungated (the unconditional floor tier). Mirrors kernel's `consensusGroupBar`,
|
|
43
|
+
* which returns -1 for the same case — the sentinel exists so an ungated group SORTS below
|
|
44
|
+
* every gated one, which is a ranking concern the UI doesn't share.
|
|
45
|
+
*/
|
|
46
|
+
function barFor(group: ConsensusGroup): number | null {
|
|
47
|
+
if (!group.gating.enabled) return null
|
|
48
|
+
const thresholds = [
|
|
49
|
+
group.gating.minComplexity,
|
|
50
|
+
group.gating.minRisk,
|
|
51
|
+
group.gating.minImpact,
|
|
52
|
+
].filter((t): t is number => t !== undefined)
|
|
53
|
+
return thresholds.length ? Math.max(...thresholds) : null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function create(input: CreateConsensusGroupInput) {
|
|
57
|
+
const ws = useWorkspaceStore()
|
|
58
|
+
const created = await api.createConsensusGroup(ws.requireId(), input)
|
|
59
|
+
await ws.refresh()
|
|
60
|
+
return created
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function update(groupId: string, patch: UpdateConsensusGroupInput) {
|
|
64
|
+
const ws = useWorkspaceStore()
|
|
65
|
+
const updated = await api.updateConsensusGroup(ws.requireId(), groupId, patch)
|
|
66
|
+
await ws.refresh()
|
|
67
|
+
return updated
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function remove(groupId: string) {
|
|
71
|
+
const ws = useWorkspaceStore()
|
|
72
|
+
await api.deleteConsensusGroup(ws.requireId(), groupId)
|
|
73
|
+
await ws.refresh()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { groups, hasGroups, hydrate, resolve, barFor, create, update, remove }
|
|
77
|
+
})
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { computed } from 'vue'
|
|
2
2
|
import type { AgentKind, Pipeline } from '~/types/domain'
|
|
3
|
-
import type { ConsensusStepConfig } from '~/types/consensus'
|
|
4
|
-
import type { StepOptions } from '@cat-factory/contracts'
|
|
5
3
|
import { companionForProducer } from '~/utils/catalog'
|
|
6
|
-
import {
|
|
4
|
+
import type { PipelinesContext } from './context'
|
|
5
|
+
import { createPipelineStepConfigActions } from './draftStepConfig'
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
|
-
* The pipeline-builder draft
|
|
10
|
-
*
|
|
11
|
-
* factory closing over the shared {@link PipelinesContext}
|
|
12
|
-
*
|
|
8
|
+
* The pipeline-builder draft's STRUCTURE: inserting/removing/reordering steps, the companion
|
|
9
|
+
* folding that turns the flat arrays into renderable units, and `clearDraft` / `loadForEdit`.
|
|
10
|
+
* Extracted from the store setup into a factory closing over the shared {@link PipelinesContext}.
|
|
11
|
+
*
|
|
12
|
+
* The per-step CONFIG toggles (consensus + its group tiers, gates, companions, step options) are
|
|
13
|
+
* the sibling factory in `./draftStepConfig` — a different concern on the same context, and the
|
|
14
|
+
* seam this file was split along when it outgrew the per-function budget. Both are spread into
|
|
15
|
+
* the store, so callers see one flat API exactly as before.
|
|
13
16
|
*/
|
|
14
17
|
export function createPipelineDraftActions(ctx: PipelinesContext) {
|
|
15
18
|
const {
|
|
@@ -97,13 +100,6 @@ export function createPipelineDraftActions(ctx: PipelinesContext) {
|
|
|
97
100
|
else insertAt(index + 1, companion)
|
|
98
101
|
}
|
|
99
102
|
|
|
100
|
-
/** Toggle estimate gating on/off for the (companion) step at `index`. */
|
|
101
|
-
function toggleDraftGating(index: number) {
|
|
102
|
-
draftGating.value[index] = draftGating.value[index]?.enabled
|
|
103
|
-
? null
|
|
104
|
-
: { enabled: true, minRisk: 0.5, minImpact: 0.5, onMissingEstimate: 'run' }
|
|
105
|
-
}
|
|
106
|
-
|
|
107
103
|
/**
|
|
108
104
|
* The draft as a list of "units" for rendering: each step is one unit, EXCEPT a companion
|
|
109
105
|
* that sits immediately after its producer — that companion is folded into the producer's
|
|
@@ -157,93 +153,6 @@ export function createPipelineDraftActions(ctx: PipelinesContext) {
|
|
|
157
153
|
draftStepOptions.value = reorder(draftStepOptions.value)
|
|
158
154
|
}
|
|
159
155
|
|
|
160
|
-
/** Toggle the consensus mechanism on the draft step at `index` (default config / off). */
|
|
161
|
-
function toggleDraftConsensus(index: number) {
|
|
162
|
-
draftConsensus.value[index] = draftConsensus.value[index] ? null : defaultConsensusConfig()
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** Replace the consensus config of the draft step at `index` (builder editor edits). */
|
|
166
|
-
function setDraftConsensus(index: number, config: ConsensusStepConfig | null) {
|
|
167
|
-
draftConsensus.value[index] = config
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/** Toggle the approval gate on the draft step at `index`. */
|
|
171
|
-
function toggleDraftGate(index: number) {
|
|
172
|
-
draftGates.value[index] = !draftGates.value[index]
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/** Toggle the Follow-up companion on the draft (coder) step at `index` (default on → off). */
|
|
176
|
-
function toggleDraftFollowUps(index: number) {
|
|
177
|
-
// Default (null/true) is enabled, so the first toggle disables it (false); toggle back to null.
|
|
178
|
-
draftFollowUps.value[index] = draftFollowUps.value[index] === false ? null : false
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Toggle the test quality-control companion on the draft (Tester) step at `index`. The
|
|
183
|
-
* companion is enabled by default (a `null` entry), so the first toggle disables it
|
|
184
|
-
* (`{ enabled: false }`, dropping any gating) and the next restores the default.
|
|
185
|
-
*/
|
|
186
|
-
function toggleDraftTesterQuality(index: number) {
|
|
187
|
-
draftTesterQuality.value[index] =
|
|
188
|
-
draftTesterQuality.value[index]?.enabled === false ? null : { enabled: false }
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Toggle estimate gating on/off for the QC companion on the draft (Tester) step at `index`.
|
|
193
|
-
* A no-op while the companion is disabled (nothing to gate). Enabling gating pins the config
|
|
194
|
-
* to `{ enabled: true, gating }` so the thresholds are editable; disabling drops back to the
|
|
195
|
-
* default `null` (enabled, ungated).
|
|
196
|
-
*/
|
|
197
|
-
function toggleDraftTesterQualityGating(index: number) {
|
|
198
|
-
const cur = draftTesterQuality.value[index]
|
|
199
|
-
if (cur?.enabled === false) return
|
|
200
|
-
draftTesterQuality.value[index] = cur?.gating?.enabled
|
|
201
|
-
? null
|
|
202
|
-
: {
|
|
203
|
-
enabled: true,
|
|
204
|
-
gating: { enabled: true, minRisk: 0.5, minImpact: 0.5, onMissingEstimate: 'run' },
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/** Enable/disable the draft step at `index` without removing it. */
|
|
209
|
-
function toggleDraftEnabled(index: number) {
|
|
210
|
-
draftEnabled.value[index] = draftEnabled.value[index] === false
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/** Whether auto-recommendation is on for the draft (requirements-review) step at `index`. */
|
|
214
|
-
function draftAutoRecommendEnabled(index: number): boolean {
|
|
215
|
-
return draftStepOptions.value[index]?.autoRecommend !== false
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Toggle the requirements-review auto-recommendation on the draft step at `index`. It is on by
|
|
220
|
-
* default, so we store ONLY the opt-out (`{ autoRecommend: false }`); toggling back drops the
|
|
221
|
-
* flag. Merges with any other future StepOptions fields rather than clobbering the whole bag.
|
|
222
|
-
*/
|
|
223
|
-
function toggleDraftAutoRecommend(index: number) {
|
|
224
|
-
const next: StepOptions = { ...draftStepOptions.value[index] }
|
|
225
|
-
if (draftAutoRecommendEnabled(index)) next.autoRecommend = false
|
|
226
|
-
else delete next.autoRecommend
|
|
227
|
-
draftStepOptions.value[index] = Object.keys(next).length ? next : null
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/** The skill picked for the draft `skill` step at `index` (its `stepOptions.skillId`). */
|
|
231
|
-
function draftSkillId(index: number): string | undefined {
|
|
232
|
-
return draftStepOptions.value[index]?.skillId
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Set (or clear) the picked skill on the draft `skill` step at `index`. Merges into the
|
|
237
|
-
* step's `StepOptions` bag rather than clobbering it; clearing drops the field and, if the
|
|
238
|
-
* bag empties, the whole entry (so it normalizes away like the other options).
|
|
239
|
-
*/
|
|
240
|
-
function setDraftSkillId(index: number, skillId: string | undefined) {
|
|
241
|
-
const next: StepOptions = { ...draftStepOptions.value[index] }
|
|
242
|
-
if (skillId) next.skillId = skillId
|
|
243
|
-
else delete next.skillId
|
|
244
|
-
draftStepOptions.value[index] = Object.keys(next).length ? next : null
|
|
245
|
-
}
|
|
246
|
-
|
|
247
156
|
function clearDraft() {
|
|
248
157
|
draft.value = []
|
|
249
158
|
draftGates.value = []
|
|
@@ -282,25 +191,14 @@ export function createPipelineDraftActions(ctx: PipelinesContext) {
|
|
|
282
191
|
}
|
|
283
192
|
|
|
284
193
|
return {
|
|
194
|
+
...createPipelineStepConfigActions(ctx),
|
|
285
195
|
addToDraft,
|
|
286
196
|
removeFromDraft,
|
|
287
197
|
moveInDraft,
|
|
288
198
|
hasCompanion,
|
|
289
199
|
toggleCompanion,
|
|
290
|
-
toggleDraftGating,
|
|
291
200
|
units,
|
|
292
201
|
moveUnit,
|
|
293
|
-
toggleDraftConsensus,
|
|
294
|
-
setDraftConsensus,
|
|
295
|
-
toggleDraftGate,
|
|
296
|
-
toggleDraftFollowUps,
|
|
297
|
-
toggleDraftTesterQuality,
|
|
298
|
-
toggleDraftTesterQualityGating,
|
|
299
|
-
toggleDraftEnabled,
|
|
300
|
-
draftAutoRecommendEnabled,
|
|
301
|
-
toggleDraftAutoRecommend,
|
|
302
|
-
draftSkillId,
|
|
303
|
-
setDraftSkillId,
|
|
304
202
|
clearDraft,
|
|
305
203
|
loadForEdit,
|
|
306
204
|
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { StepOptions } from '@cat-factory/contracts'
|
|
2
|
+
import type { ConsensusStepConfig } from '~/types/consensus'
|
|
3
|
+
import { defaultConsensusConfig, type PipelinesContext } from './context'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The pipeline-builder draft's PER-STEP CONFIG toggles: consensus (inline panel and the workspace
|
|
7
|
+
* consensus-GROUP tier set), the human approval gate, the estimate gate on a companion step, the
|
|
8
|
+
* follow-up and test-QC companions, the per-step enable flag, and the `StepOptions` bag
|
|
9
|
+
* (requirements auto-recommendation, the picked skill).
|
|
10
|
+
*
|
|
11
|
+
* Split out of `./draftActions`, which owns the draft's STRUCTURE (insert / remove / reorder /
|
|
12
|
+
* units). Every function here reads and writes one of the parallel per-step arrays at an index and
|
|
13
|
+
* touches nothing else, which is what makes the two independent; both are spread into the store,
|
|
14
|
+
* so the store's API is unchanged.
|
|
15
|
+
*/
|
|
16
|
+
export function createPipelineStepConfigActions(ctx: PipelinesContext) {
|
|
17
|
+
const {
|
|
18
|
+
draftGates,
|
|
19
|
+
draftEnabled,
|
|
20
|
+
draftConsensus,
|
|
21
|
+
draftGating,
|
|
22
|
+
draftFollowUps,
|
|
23
|
+
draftTesterQuality,
|
|
24
|
+
draftStepOptions,
|
|
25
|
+
} = ctx
|
|
26
|
+
|
|
27
|
+
/** Toggle estimate gating on/off for the (companion) step at `index`. */
|
|
28
|
+
function toggleDraftGating(index: number) {
|
|
29
|
+
draftGating.value[index] = draftGating.value[index]?.enabled
|
|
30
|
+
? null
|
|
31
|
+
: { enabled: true, minRisk: 0.5, minImpact: 0.5, onMissingEstimate: 'run' }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Toggle the consensus mechanism on the draft step at `index` (default config / off). */
|
|
35
|
+
function toggleDraftConsensus(index: number) {
|
|
36
|
+
draftConsensus.value[index] = draftConsensus.value[index] ? null : defaultConsensusConfig()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Replace the consensus config of the draft step at `index` (builder editor edits). */
|
|
40
|
+
function setDraftConsensus(index: number, config: ConsensusStepConfig | null) {
|
|
41
|
+
draftConsensus.value[index] = config
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Add/remove a workspace consensus GROUP from the draft step's tier set. The array is a SET,
|
|
46
|
+
* not a precedence list — the engine ranks candidates by the bar each group sets — so this
|
|
47
|
+
* appends without ceremony.
|
|
48
|
+
*
|
|
49
|
+
* An empty tier set falls back to the step's inline participants; a non-empty one takes over,
|
|
50
|
+
* which is why the builder shows only one of the two editors at a time.
|
|
51
|
+
*/
|
|
52
|
+
function toggleDraftConsensusGroup(index: number, groupId: string) {
|
|
53
|
+
const config = draftConsensus.value[index]
|
|
54
|
+
if (!config) return
|
|
55
|
+
const current = config.groupIds ?? []
|
|
56
|
+
const next = current.includes(groupId)
|
|
57
|
+
? current.filter((id) => id !== groupId)
|
|
58
|
+
: [...current, groupId]
|
|
59
|
+
// Drop the key entirely when the set empties, so a step that never used tiers persists the
|
|
60
|
+
// same shape it always did rather than an empty array that reads as "tiered, but none".
|
|
61
|
+
if (next.length) config.groupIds = next
|
|
62
|
+
else delete config.groupIds
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Toggle the approval gate on the draft step at `index`. */
|
|
66
|
+
function toggleDraftGate(index: number) {
|
|
67
|
+
draftGates.value[index] = !draftGates.value[index]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Toggle the Follow-up companion on the draft (coder) step at `index` (default on → off). */
|
|
71
|
+
function toggleDraftFollowUps(index: number) {
|
|
72
|
+
// Default (null/true) is enabled, so the first toggle disables it (false); toggle back to null.
|
|
73
|
+
draftFollowUps.value[index] = draftFollowUps.value[index] === false ? null : false
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Toggle the test quality-control companion on the draft (Tester) step at `index`. The
|
|
78
|
+
* companion is enabled by default (a `null` entry), so the first toggle disables it
|
|
79
|
+
* (`{ enabled: false }`, dropping any gating) and the next restores the default.
|
|
80
|
+
*/
|
|
81
|
+
function toggleDraftTesterQuality(index: number) {
|
|
82
|
+
draftTesterQuality.value[index] =
|
|
83
|
+
draftTesterQuality.value[index]?.enabled === false ? null : { enabled: false }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Toggle estimate gating on/off for the QC companion on the draft (Tester) step at `index`.
|
|
88
|
+
* A no-op while the companion is disabled (nothing to gate). Enabling gating pins the config
|
|
89
|
+
* to `{ enabled: true, gating }` so the thresholds are editable; disabling drops back to the
|
|
90
|
+
* default `null` (enabled, ungated).
|
|
91
|
+
*/
|
|
92
|
+
function toggleDraftTesterQualityGating(index: number) {
|
|
93
|
+
const cur = draftTesterQuality.value[index]
|
|
94
|
+
if (cur?.enabled === false) return
|
|
95
|
+
draftTesterQuality.value[index] = cur?.gating?.enabled
|
|
96
|
+
? null
|
|
97
|
+
: {
|
|
98
|
+
enabled: true,
|
|
99
|
+
gating: { enabled: true, minRisk: 0.5, minImpact: 0.5, onMissingEstimate: 'run' },
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Enable/disable the draft step at `index` without removing it. */
|
|
104
|
+
function toggleDraftEnabled(index: number) {
|
|
105
|
+
draftEnabled.value[index] = draftEnabled.value[index] === false
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Whether auto-recommendation is on for the draft (requirements-review) step at `index`. */
|
|
109
|
+
function draftAutoRecommendEnabled(index: number): boolean {
|
|
110
|
+
return draftStepOptions.value[index]?.autoRecommend !== false
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Toggle the requirements-review auto-recommendation on the draft step at `index`. It is on by
|
|
115
|
+
* default, so we store ONLY the opt-out (`{ autoRecommend: false }`); toggling back drops the
|
|
116
|
+
* flag. Merges with any other future StepOptions fields rather than clobbering the whole bag.
|
|
117
|
+
*/
|
|
118
|
+
function toggleDraftAutoRecommend(index: number) {
|
|
119
|
+
const next: StepOptions = { ...draftStepOptions.value[index] }
|
|
120
|
+
if (draftAutoRecommendEnabled(index)) next.autoRecommend = false
|
|
121
|
+
else delete next.autoRecommend
|
|
122
|
+
draftStepOptions.value[index] = Object.keys(next).length ? next : null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The skill picked for the draft `skill` step at `index` (its `stepOptions.skillId`). */
|
|
126
|
+
function draftSkillId(index: number): string | undefined {
|
|
127
|
+
return draftStepOptions.value[index]?.skillId
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Set (or clear) the picked skill on the draft `skill` step at `index`. Merges into the
|
|
132
|
+
* step's `StepOptions` bag rather than clobbering it; clearing drops the field and, if the
|
|
133
|
+
* bag empties, the whole entry (so it normalizes away like the other options).
|
|
134
|
+
*/
|
|
135
|
+
function setDraftSkillId(index: number, skillId: string | undefined) {
|
|
136
|
+
const next: StepOptions = { ...draftStepOptions.value[index] }
|
|
137
|
+
if (skillId) next.skillId = skillId
|
|
138
|
+
else delete next.skillId
|
|
139
|
+
draftStepOptions.value[index] = Object.keys(next).length ? next : null
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
toggleDraftGating,
|
|
144
|
+
toggleDraftConsensus,
|
|
145
|
+
setDraftConsensus,
|
|
146
|
+
toggleDraftConsensusGroup,
|
|
147
|
+
toggleDraftGate,
|
|
148
|
+
toggleDraftFollowUps,
|
|
149
|
+
toggleDraftTesterQuality,
|
|
150
|
+
toggleDraftTesterQualityGating,
|
|
151
|
+
toggleDraftEnabled,
|
|
152
|
+
draftAutoRecommendEnabled,
|
|
153
|
+
toggleDraftAutoRecommend,
|
|
154
|
+
draftSkillId,
|
|
155
|
+
setDraftSkillId,
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -12,6 +12,7 @@ import { useFragmentsStore } from '~/stores/fragments'
|
|
|
12
12
|
import { useGitHubStore } from '~/stores/github'
|
|
13
13
|
import { useInitiativesStore } from '~/stores/initiative'
|
|
14
14
|
import { useModelPresetsStore } from '~/stores/modelPresets'
|
|
15
|
+
import { useConsensusGroupsStore } from '~/stores/consensusGroups'
|
|
15
16
|
import { useNotificationsStore } from '~/stores/notifications'
|
|
16
17
|
import { usePipelinesStore } from '~/stores/pipelines'
|
|
17
18
|
import { useProviderConnectionsStore } from '~/stores/providerConnections'
|
|
@@ -72,6 +73,7 @@ export function applySnapshotToStores(snapshot: WorkspaceSnapshot, boardSince?:
|
|
|
72
73
|
useWorkspaceSettingsStore().hydrate(snapshot.settings)
|
|
73
74
|
useAgentConfigStore().hydrate(snapshot.agentConfigCatalog ?? [])
|
|
74
75
|
useModelPresetsStore().hydrate(snapshot.modelPresets ?? [], snapshot.modelPresetCatalogVersions)
|
|
76
|
+
useConsensusGroupsStore().hydrate(snapshot.consensusGroups ?? [])
|
|
75
77
|
useServiceFragmentDefaultsStore().hydrate(snapshot.serviceFragmentDefaults?.fragmentIds)
|
|
76
78
|
useRecurringPipelinesStore().hydrate(snapshot.recurringPipelines ?? [])
|
|
77
79
|
useInitiativesStore().hydrate(snapshot.initiatives)
|
package/app/types/consensus.ts
CHANGED
package/app/types/domain.ts
CHANGED
|
@@ -60,6 +60,7 @@ export type {
|
|
|
60
60
|
TestScreenshot,
|
|
61
61
|
AgentKind,
|
|
62
62
|
AgentCategory,
|
|
63
|
+
AgentTier,
|
|
63
64
|
CustomAgentKind,
|
|
64
65
|
CustomTaskType,
|
|
65
66
|
TaskTypePresentation,
|
|
@@ -96,7 +97,7 @@ export type {
|
|
|
96
97
|
PreviewStatus,
|
|
97
98
|
} from '@cat-factory/contracts'
|
|
98
99
|
|
|
99
|
-
import type { AgentCategory, AgentKind } from '@cat-factory/contracts'
|
|
100
|
+
import type { AgentCategory, AgentKind, AgentTier } from '@cat-factory/contracts'
|
|
100
101
|
|
|
101
102
|
// The document-kind list + the per-kind field descriptors are runtime values (used to render
|
|
102
103
|
// the picker and the conditional per-kind inputs), so they are re-exported as values — the
|
|
@@ -114,6 +115,12 @@ export interface AgentArchetype {
|
|
|
114
115
|
description: string
|
|
115
116
|
/** Palette category this archetype is grouped under. Absent ⇒ ungrouped/system kind. */
|
|
116
117
|
category?: AgentCategory
|
|
118
|
+
/**
|
|
119
|
+
* How specialist this kind is — the tier the palette / model-preset override list filter on
|
|
120
|
+
* (`basic` shows only basic kinds, `intermediate` adds those, `advanced` shows everything).
|
|
121
|
+
* Absent ⇒ `DEFAULT_AGENT_TIER`, which is how an unclassified deployment kind behaves.
|
|
122
|
+
*/
|
|
123
|
+
tier?: AgentTier
|
|
117
124
|
/**
|
|
118
125
|
* Optional id of a DEDICATED result window this agent's step opens instead of the
|
|
119
126
|
* generic prose step-detail panel. Resolved through the modular `resultViews` slot
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import type { AgentArchetype } from '~/types/domain'
|
|
3
|
+
import { filterByAgentTier, filterByAgentTierKeeping, isAgentTier } from '~/utils/agentTier'
|
|
4
|
+
|
|
5
|
+
const archetype = (kind: string, tier?: AgentArchetype['tier']): AgentArchetype => ({
|
|
6
|
+
kind: kind as AgentArchetype['kind'],
|
|
7
|
+
label: kind,
|
|
8
|
+
icon: 'i-lucide-bot',
|
|
9
|
+
color: '#fff',
|
|
10
|
+
description: kind,
|
|
11
|
+
...(tier ? { tier } : {}),
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const CATALOG: AgentArchetype[] = [
|
|
15
|
+
archetype('coder', 'basic'),
|
|
16
|
+
archetype('researcher', 'intermediate'),
|
|
17
|
+
archetype('mocker', 'advanced'),
|
|
18
|
+
// A deployment-registered kind that declared no tier.
|
|
19
|
+
archetype('acme-auditor'),
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
describe('filterByAgentTier', () => {
|
|
23
|
+
it('shows only the basic kinds at the default level', () => {
|
|
24
|
+
expect(filterByAgentTier(CATALOG, 'basic').map((a) => a.kind)).toEqual(['coder'])
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('accumulates the levels, with advanced showing the whole catalog', () => {
|
|
28
|
+
expect(filterByAgentTier(CATALOG, 'intermediate').map((a) => a.kind)).toEqual([
|
|
29
|
+
'coder',
|
|
30
|
+
'researcher',
|
|
31
|
+
// An undeclared tier defaults to intermediate, so it appears here.
|
|
32
|
+
'acme-auditor',
|
|
33
|
+
])
|
|
34
|
+
expect(filterByAgentTier(CATALOG, 'advanced')).toHaveLength(CATALOG.length)
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('filterByAgentTierKeeping', () => {
|
|
39
|
+
it('keeps a pinned kind the tier would otherwise hide, in catalog order', () => {
|
|
40
|
+
const kept = filterByAgentTierKeeping(CATALOG, 'basic', (a) => a.kind === 'mocker')
|
|
41
|
+
expect(kept.map((a) => a.kind)).toEqual(['coder', 'mocker'])
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('does not duplicate a pinned kind the tier already shows', () => {
|
|
45
|
+
const kept = filterByAgentTierKeeping(CATALOG, 'advanced', () => true)
|
|
46
|
+
expect(kept.map((a) => a.kind)).toEqual(CATALOG.map((a) => a.kind))
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('isAgentTier', () => {
|
|
51
|
+
it('accepts the known tiers and rejects anything else', () => {
|
|
52
|
+
expect(isAgentTier('basic')).toBe(true)
|
|
53
|
+
expect(isAgentTier('advanced')).toBe(true)
|
|
54
|
+
// A persisted blob from an older build, or a hand-edited one.
|
|
55
|
+
expect(isAgentTier('expert')).toBe(false)
|
|
56
|
+
expect(isAgentTier(undefined)).toBe(false)
|
|
57
|
+
expect(isAgentTier(2)).toBe(false)
|
|
58
|
+
})
|
|
59
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { AGENT_TIERS, agentTierVisibleAt, type AgentTier } from '@cat-factory/contracts'
|
|
2
|
+
import type { AgentArchetype } from '~/types/domain'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Agent-tier filtering for the two catalog surfaces — the pipeline builder's palette and the
|
|
6
|
+
* model preset's per-agent override list.
|
|
7
|
+
*
|
|
8
|
+
* The tier vocabulary, the default and the cumulative predicate live in `@cat-factory/contracts`
|
|
9
|
+
* (beside `purposeAllowsAgentCategory`) so a deployment-registered kind's declared tier and the
|
|
10
|
+
* SPA's own built-ins are read by ONE rule. This module holds only the frontend-shaped helpers
|
|
11
|
+
* built on it.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately NOT the interface mode (`utils/uiMode.ts`). That tier says which SURFACES the
|
|
14
|
+
* whole SPA offers; this one says how deep into the agent catalog a given surface reaches. They
|
|
15
|
+
* are independent axes — an advanced-mode user still starts on the basic agent tier — and the
|
|
16
|
+
* control for this one is visible in both interface modes, since it is the only way to reach
|
|
17
|
+
* the kinds it hides.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** The default agent tier a surface opens on: the everyday delivery loop, nothing else. */
|
|
21
|
+
export const DEFAULT_AGENT_TIER_LEVEL: AgentTier = 'basic'
|
|
22
|
+
|
|
23
|
+
/** Whether an untrusted (persisted / hand-edited) value is one of the known tiers. */
|
|
24
|
+
export function isAgentTier(value: unknown): value is AgentTier {
|
|
25
|
+
return typeof value === 'string' && (AGENT_TIERS as readonly string[]).includes(value)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Keep only the archetypes visible at `level` (cumulative — see `agentTierVisibleAt`). */
|
|
29
|
+
export function filterByAgentTier<T extends Pick<AgentArchetype, 'tier'>>(
|
|
30
|
+
archetypes: readonly T[],
|
|
31
|
+
level: AgentTier,
|
|
32
|
+
): T[] {
|
|
33
|
+
return archetypes.filter((a) => agentTierVisibleAt(a.tier, level))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Keep the archetypes visible at `level`, PLUS any the caller marks as pinned — the model
|
|
38
|
+
* preset editor's kinds that already carry an override. An entity can hold a setting written
|
|
39
|
+
* by a teammate, by the API, or by this user at a wider tier; hiding the row would leave them
|
|
40
|
+
* unable to see or clear it, which is the same failure the interface mode's `showOverrideField`
|
|
41
|
+
* exists to prevent. Order follows the input, so a pinned kind stays where the catalog puts it.
|
|
42
|
+
*/
|
|
43
|
+
export function filterByAgentTierKeeping<T extends Pick<AgentArchetype, 'tier' | 'kind'>>(
|
|
44
|
+
archetypes: readonly T[],
|
|
45
|
+
level: AgentTier,
|
|
46
|
+
isPinned: (archetype: T) => boolean,
|
|
47
|
+
): T[] {
|
|
48
|
+
return archetypes.filter((a) => agentTierVisibleAt(a.tier, level) || isPinned(a))
|
|
49
|
+
}
|
|
@@ -67,6 +67,18 @@ describe('catalog', () => {
|
|
|
67
67
|
}
|
|
68
68
|
})
|
|
69
69
|
|
|
70
|
+
it('classifies every built-in kind into a tier', () => {
|
|
71
|
+
// The palette / model-preset surfaces open on `basic`, so a built-in that forgot its tier
|
|
72
|
+
// would silently fall to the DEFAULT (intermediate) and vanish from the default view for
|
|
73
|
+
// no stated reason. Only a deployment-registered kind may leave it to the default.
|
|
74
|
+
for (const a of [...AGENT_ARCHETYPES, ...Object.values(SYSTEM_AGENT_META)]) {
|
|
75
|
+
expect(a.tier, `${a.kind} declares no tier`).toBeDefined()
|
|
76
|
+
}
|
|
77
|
+
// And the everyday delivery loop has to be assemblable without touching the control.
|
|
78
|
+
const basic = AGENT_ARCHETYPES.filter((a) => a.tier === 'basic').map((a) => a.kind)
|
|
79
|
+
expect(basic).toEqual(expect.arrayContaining(['architect', 'coder', 'tester-api']))
|
|
80
|
+
})
|
|
81
|
+
|
|
70
82
|
it('resolves usable metadata for every kind via agentKindMeta', () => {
|
|
71
83
|
// Palette archetypes resolve to their own entry.
|
|
72
84
|
for (const a of AGENT_ARCHETYPES) {
|