refactor(types): add GroupId and AgentId branded types to ids.ts
Extend the branded type system with two new domain types for btmsg/bttask agent and group identifiers. Apply to groups.ts interfaces including agentToProject() domain crossing cast.
This commit is contained in:
parent
52fd3ee562
commit
5454587acb
3 changed files with 70 additions and 8 deletions
|
|
@ -1,8 +1,9 @@
|
||||||
import type { ProviderId } from '../providers/types';
|
import type { ProviderId } from '../providers/types';
|
||||||
import type { AnchorBudgetScale } from './anchors';
|
import type { AnchorBudgetScale } from './anchors';
|
||||||
|
import type { ProjectId, GroupId, AgentId } from './ids';
|
||||||
|
|
||||||
export interface ProjectConfig {
|
export interface ProjectConfig {
|
||||||
id: string;
|
id: ProjectId;
|
||||||
name: string;
|
name: string;
|
||||||
identifier: string;
|
identifier: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
|
@ -35,8 +36,9 @@ export const AGENT_ROLE_ICONS: Record<string, string> = {
|
||||||
|
|
||||||
/** Convert a GroupAgentConfig to a ProjectConfig for unified rendering */
|
/** Convert a GroupAgentConfig to a ProjectConfig for unified rendering */
|
||||||
export function agentToProject(agent: GroupAgentConfig, groupCwd: string): ProjectConfig {
|
export function agentToProject(agent: GroupAgentConfig, groupCwd: string): ProjectConfig {
|
||||||
|
// Agent IDs serve as project IDs in the workspace (agents render as project boxes)
|
||||||
return {
|
return {
|
||||||
id: agent.id,
|
id: agent.id as unknown as ProjectId,
|
||||||
name: agent.name,
|
name: agent.name,
|
||||||
identifier: agent.role,
|
identifier: agent.role,
|
||||||
description: `${agent.role.charAt(0).toUpperCase() + agent.role.slice(1)} agent`,
|
description: `${agent.role.charAt(0).toUpperCase() + agent.role.slice(1)} agent`,
|
||||||
|
|
@ -58,7 +60,7 @@ export type GroupAgentStatus = 'active' | 'sleeping' | 'stopped';
|
||||||
|
|
||||||
/** Group-level agent configuration */
|
/** Group-level agent configuration */
|
||||||
export interface GroupAgentConfig {
|
export interface GroupAgentConfig {
|
||||||
id: string;
|
id: AgentId;
|
||||||
name: string;
|
name: string;
|
||||||
role: GroupAgentRole;
|
role: GroupAgentRole;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
|
@ -70,7 +72,7 @@ export interface GroupAgentConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GroupConfig {
|
export interface GroupConfig {
|
||||||
id: string;
|
id: GroupId;
|
||||||
name: string;
|
name: string;
|
||||||
projects: ProjectConfig[];
|
projects: ProjectConfig[];
|
||||||
/** Group-level orchestration agents (Tier 1) */
|
/** Group-level orchestration agents (Tier 1) */
|
||||||
|
|
@ -80,7 +82,7 @@ export interface GroupConfig {
|
||||||
export interface GroupsFile {
|
export interface GroupsFile {
|
||||||
version: number;
|
version: number;
|
||||||
groups: GroupConfig[];
|
groups: GroupConfig[];
|
||||||
activeGroupId: string;
|
activeGroupId: GroupId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Derive a project identifier from a name: lowercase, spaces to dashes */
|
/** Derive a project identifier from a name: lowercase, spaces to dashes */
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { SessionId, ProjectId, type SessionId as SessionIdType, type ProjectId as ProjectIdType } from './ids';
|
import {
|
||||||
|
SessionId, ProjectId, GroupId, AgentId,
|
||||||
|
type SessionId as SessionIdType,
|
||||||
|
type ProjectId as ProjectIdType,
|
||||||
|
type GroupId as GroupIdType,
|
||||||
|
type AgentId as AgentIdType,
|
||||||
|
} from './ids';
|
||||||
|
|
||||||
describe('branded types', () => {
|
describe('branded types', () => {
|
||||||
describe('SessionId', () => {
|
describe('SessionId', () => {
|
||||||
|
|
@ -41,12 +47,50 @@ describe('branded types', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('GroupId', () => {
|
||||||
|
it('creates a GroupId from a string', () => {
|
||||||
|
const id = GroupId('grp-abc');
|
||||||
|
expect(id).toBe('grp-abc');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is usable as a Map key', () => {
|
||||||
|
const map = new Map<GroupIdType, string>();
|
||||||
|
const id = GroupId('grp-1');
|
||||||
|
map.set(id, 'test-group');
|
||||||
|
expect(map.get(id)).toBe('test-group');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('AgentId', () => {
|
||||||
|
it('creates an AgentId from a string', () => {
|
||||||
|
const id = AgentId('agent-manager');
|
||||||
|
expect(id).toBe('agent-manager');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is usable as a Map key', () => {
|
||||||
|
const map = new Map<AgentIdType, number>();
|
||||||
|
const id = AgentId('a1');
|
||||||
|
map.set(id, 99);
|
||||||
|
expect(map.get(id)).toBe(99);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('equality works between two AgentIds with same value', () => {
|
||||||
|
const a = AgentId('a1');
|
||||||
|
const b = AgentId('a1');
|
||||||
|
expect(a === b).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('type safety (compile-time)', () => {
|
describe('type safety (compile-time)', () => {
|
||||||
it('both types are strings at runtime', () => {
|
it('all four types are strings at runtime', () => {
|
||||||
const sid = SessionId('s1');
|
const sid = SessionId('s1');
|
||||||
const pid = ProjectId('p1');
|
const pid = ProjectId('p1');
|
||||||
|
const gid = GroupId('g1');
|
||||||
|
const aid = AgentId('a1');
|
||||||
expect(typeof sid).toBe('string');
|
expect(typeof sid).toBe('string');
|
||||||
expect(typeof pid).toBe('string');
|
expect(typeof pid).toBe('string');
|
||||||
|
expect(typeof gid).toBe('string');
|
||||||
|
expect(typeof aid).toBe('string');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// Branded types for domain identifiers — prevents accidental swapping of sessionId/projectId
|
// Branded types for domain identifiers — prevents accidental swapping of IDs across domains.
|
||||||
// These are compile-time only; at runtime they are plain strings.
|
// These are compile-time only; at runtime they are plain strings.
|
||||||
|
|
||||||
/** Unique identifier for an agent session */
|
/** Unique identifier for an agent session */
|
||||||
|
|
@ -7,6 +7,12 @@ export type SessionId = string & { readonly __brand: 'SessionId' };
|
||||||
/** Unique identifier for a project */
|
/** Unique identifier for a project */
|
||||||
export type ProjectId = string & { readonly __brand: 'ProjectId' };
|
export type ProjectId = string & { readonly __brand: 'ProjectId' };
|
||||||
|
|
||||||
|
/** Unique identifier for a project group */
|
||||||
|
export type GroupId = string & { readonly __brand: 'GroupId' };
|
||||||
|
|
||||||
|
/** Unique identifier for an agent in the btmsg/bttask system */
|
||||||
|
export type AgentId = string & { readonly __brand: 'AgentId' };
|
||||||
|
|
||||||
/** Create a SessionId from a raw string */
|
/** Create a SessionId from a raw string */
|
||||||
export function SessionId(value: string): SessionId {
|
export function SessionId(value: string): SessionId {
|
||||||
return value as SessionId;
|
return value as SessionId;
|
||||||
|
|
@ -16,3 +22,13 @@ export function SessionId(value: string): SessionId {
|
||||||
export function ProjectId(value: string): ProjectId {
|
export function ProjectId(value: string): ProjectId {
|
||||||
return value as ProjectId;
|
return value as ProjectId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Create a GroupId from a raw string */
|
||||||
|
export function GroupId(value: string): GroupId {
|
||||||
|
return value as GroupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Create an AgentId from a raw string */
|
||||||
|
export function AgentId(value: string): AgentId {
|
||||||
|
return value as AgentId;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue