@cat-factory/app 0.187.0 → 0.189.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/palettes/AgentPalette.vue +12 -1
- package/app/components/palettes/AgentTierSelect.vue +67 -0
- package/app/components/pipeline/PipelineBuilder.vue +166 -92
- package/app/components/pipeline/PipelineHealthModal.vue +68 -12
- 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/composables/usePipelineErrorToast.ts +17 -0
- package/app/composables/usePipelineHealth.spec.ts +79 -7
- package/app/composables/usePipelineHealth.ts +70 -3
- 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/pipelines.ts +21 -3
- package/app/stores/workspace/hydrate.ts +7 -1
- 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 +74 -5
- package/i18n/locales/en.json +80 -5
- package/i18n/locales/es.json +74 -5
- package/i18n/locales/fr.json +74 -5
- package/i18n/locales/he.json +74 -5
- package/i18n/locales/it.json +74 -5
- package/i18n/locales/ja.json +74 -5
- package/i18n/locales/pl.json +74 -5
- package/i18n/locales/tr.json +74 -5
- package/i18n/locales/uk.json +74 -5
- package/package.json +2 -2
|
@@ -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
|
+
}
|
package/app/stores/pipelines.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defineStore } from 'pinia'
|
|
2
2
|
import { ref } from 'vue'
|
|
3
3
|
import type { Pipeline } from '~/types/domain'
|
|
4
|
-
import type { PipelinePurpose } from '@cat-factory/contracts'
|
|
4
|
+
import type { PipelinePurpose, RetiredPipelineWire } from '@cat-factory/contracts'
|
|
5
5
|
import { useUpsertList } from '~/composables/useUpsertList'
|
|
6
6
|
import { createDraftStepState, type PipelinesContext } from '~/stores/pipelines/context'
|
|
7
7
|
import { createPipelineDraftActions } from '~/stores/pipelines/draftActions'
|
|
@@ -32,6 +32,13 @@ export const usePipelinesStore = defineStore('pipelines', () => {
|
|
|
32
32
|
* a newer definition available (see `usePipelineHealth`).
|
|
33
33
|
*/
|
|
34
34
|
const catalogVersions = ref<Record<string, number>>({})
|
|
35
|
+
/**
|
|
36
|
+
* Built-in pipelines WITHDRAWN from the catalog (`retiredPipelines()`), from the workspace
|
|
37
|
+
* snapshot. A stored pipeline whose id appears here is no longer relevant and can be REMOVED —
|
|
38
|
+
* the opposite of a reseed, and the only case where deleting a built-in is allowed (see
|
|
39
|
+
* `usePipelineHealth`). Disjoint from {@link catalogVersions} by construction.
|
|
40
|
+
*/
|
|
41
|
+
const retiredPipelines = ref<RetiredPipelineWire[]>([])
|
|
35
42
|
|
|
36
43
|
// The per-step, index-aligned draft arrays (kept in lockstep — see `createDraftStepState`).
|
|
37
44
|
const {
|
|
@@ -60,10 +67,20 @@ export const usePipelinesStore = defineStore('pipelines', () => {
|
|
|
60
67
|
/** The id of the pipeline being edited, or null when assembling a brand-new one. */
|
|
61
68
|
const editingId = ref<string | null>(null)
|
|
62
69
|
|
|
63
|
-
/**
|
|
64
|
-
|
|
70
|
+
/**
|
|
71
|
+
* Replace the cached pipelines (and the current built-in catalog versions + retirements) from a
|
|
72
|
+
* snapshot. `retired` is applied even when EMPTY, unlike `versions`: an absent list means the
|
|
73
|
+
* facade shipped no retirements, and carrying the previous board's forward would offer a delete
|
|
74
|
+
* for a pipeline this deployment still ships.
|
|
75
|
+
*/
|
|
76
|
+
function hydrate(
|
|
77
|
+
next: Pipeline[],
|
|
78
|
+
versions?: Record<string, number>,
|
|
79
|
+
retired?: RetiredPipelineWire[],
|
|
80
|
+
) {
|
|
65
81
|
pipelines.value = next
|
|
66
82
|
if (versions) catalogVersions.value = versions
|
|
83
|
+
retiredPipelines.value = retired ?? []
|
|
67
84
|
}
|
|
68
85
|
|
|
69
86
|
function getPipeline(id: string) {
|
|
@@ -99,6 +116,7 @@ export const usePipelinesStore = defineStore('pipelines', () => {
|
|
|
99
116
|
return {
|
|
100
117
|
pipelines,
|
|
101
118
|
catalogVersions,
|
|
119
|
+
retiredPipelines,
|
|
102
120
|
draft,
|
|
103
121
|
draftGates,
|
|
104
122
|
draftEnabled,
|
|
@@ -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'
|
|
@@ -61,7 +62,11 @@ export function applySnapshotToStores(snapshot: WorkspaceSnapshot, boardSince?:
|
|
|
61
62
|
useUserSettingsStore().hydrate(snapshot.userSettings ?? null)
|
|
62
63
|
useBoardStore().hydrate(snapshot.blocks, boardSince)
|
|
63
64
|
useBoardStore().hydrateArchived(snapshot.archivedServices ?? [])
|
|
64
|
-
usePipelinesStore().hydrate(
|
|
65
|
+
usePipelinesStore().hydrate(
|
|
66
|
+
snapshot.pipelines,
|
|
67
|
+
snapshot.pipelineCatalogVersions,
|
|
68
|
+
snapshot.retiredPipelines,
|
|
69
|
+
)
|
|
65
70
|
useExecutionStore().hydrate(snapshot.executions, snapshot.workspace.id)
|
|
66
71
|
useAgentRunsStore().hydrate(snapshot.bootstrapJobs ?? [], snapshot.workspace.id)
|
|
67
72
|
useAgentRunsStore().hydrateEnvConfigRepair(snapshot.envConfigRepairJobs ?? [])
|
|
@@ -72,6 +77,7 @@ export function applySnapshotToStores(snapshot: WorkspaceSnapshot, boardSince?:
|
|
|
72
77
|
useWorkspaceSettingsStore().hydrate(snapshot.settings)
|
|
73
78
|
useAgentConfigStore().hydrate(snapshot.agentConfigCatalog ?? [])
|
|
74
79
|
useModelPresetsStore().hydrate(snapshot.modelPresets ?? [], snapshot.modelPresetCatalogVersions)
|
|
80
|
+
useConsensusGroupsStore().hydrate(snapshot.consensusGroups ?? [])
|
|
75
81
|
useServiceFragmentDefaultsStore().hydrate(snapshot.serviceFragmentDefaults?.fragmentIds)
|
|
76
82
|
useRecurringPipelinesStore().hydrate(snapshot.recurringPipelines ?? [])
|
|
77
83
|
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) {
|