diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3ee56b2..ebeeecc 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -106,7 +106,18 @@ export { type ReflexionKind, type ReflexionMetadata, } from './reflexion.js'; -export type { SearchResult, GetObservationsOptions, Observation, Session } from './types.js'; +export { + MAX_OPEN_PROPOSALS_PER_SCOUT, + SCOUT_PROPOSAL_ERROR_CODES, + type AgentRole, + type GetObservationsOptions, + type Observation, + type SearchResult, + type Session, + type ScoutProposalErrorCode, + type TaskProposalStatus, + type TaskThreadProposalFields, +} from './types.js'; export { createSessionId } from './ids.js'; export { inferIdeFromSessionId } from './infer-ide.js'; export { diff --git a/packages/core/src/response-thresholds.ts b/packages/core/src/response-thresholds.ts index ea9ddab..2b73aea 100644 --- a/packages/core/src/response-thresholds.ts +++ b/packages/core/src/response-thresholds.ts @@ -1,4 +1,5 @@ import type { Storage } from '@colony/storage'; +import type { AgentRole } from './types.js'; /** * Named capability dimensions. Intentionally small — five is the upper @@ -17,10 +18,17 @@ export interface AgentCapabilities { export interface AgentProfile { agent: string; + role: AgentRole; + openProposalCount: number; capabilities: AgentCapabilities; updated_at: number; } +interface AgentProfileStorageExtras { + role?: AgentRole | null; + open_proposal_count?: number | null; +} + /** * Default profile for agents that haven't registered capabilities yet. * All dimensions at 0.5 means "no preference either way" — the agent @@ -121,6 +129,8 @@ export function loadProfile(storage: Storage, agent: string): AgentProfile { if (!row) { return { agent, + role: 'executor', + openProposalCount: 0, capabilities: { ...DEFAULT_CAPABILITIES }, updated_at: 0, }; @@ -132,7 +142,14 @@ export function loadProfile(storage: Storage, agent: string): AgentProfile { } catch { caps = { ...DEFAULT_CAPABILITIES }; } - return { agent, capabilities: caps, updated_at: row.updated_at }; + const extras = row as typeof row & AgentProfileStorageExtras; + return { + agent, + role: extras.role ?? 'executor', + openProposalCount: extras.open_proposal_count ?? 0, + capabilities: caps, + updated_at: row.updated_at, + }; } /** Write a full or partial capability profile for an agent. */ @@ -148,5 +165,11 @@ export function saveProfile( capabilities: JSON.stringify(merged), updated_at: Date.now(), }); - return { agent, capabilities: merged, updated_at: Date.now() }; + return { + agent, + role: current.role, + openProposalCount: current.openProposalCount, + capabilities: merged, + updated_at: Date.now(), + }; } diff --git a/packages/core/src/task-thread.ts b/packages/core/src/task-thread.ts index e6d2978..0e87056 100644 --- a/packages/core/src/task-thread.ts +++ b/packages/core/src/task-thread.ts @@ -16,6 +16,7 @@ import { loadProfile, rankCandidates, } from './response-thresholds.js'; +import { SCOUT_PROPOSAL_ERROR_CODES } from './types.js'; import { type WorkingHandoffNoteInput, type WorkingHandoffNoteResult, @@ -101,6 +102,7 @@ export const TASK_THREAD_ERROR_CODES = { CLAIM_BATON_CONFLICT: 'CLAIM_BATON_CONFLICT', INVALID_CLAIM_PATH: 'INVALID_CLAIM_PATH', PROTECTED_BRANCH_CLAIM_REJECTED: 'PROTECTED_BRANCH_CLAIM_REJECTED', + ...SCOUT_PROPOSAL_ERROR_CODES, INTERNAL_ERROR: 'INTERNAL_ERROR', } as const; diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index fc084c4..5de55c7 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1,3 +1,25 @@ +export type AgentRole = 'scout' | 'executor' | 'queen'; + +export const MAX_OPEN_PROPOSALS_PER_SCOUT = 3; + +export type TaskProposalStatus = 'proposed' | 'approved' | 'archived'; + +export interface TaskThreadProposalFields { + proposalStatus?: TaskProposalStatus | null; + approvedBy?: string | null; + observationEvidenceIds?: number[]; +} + +export const SCOUT_PROPOSAL_ERROR_CODES = { + PROPOSAL_MISSING_EVIDENCE: 'PROPOSAL_MISSING_EVIDENCE', + PROPOSAL_CAP_EXCEEDED: 'PROPOSAL_CAP_EXCEEDED', + EXECUTOR_CANNOT_PROPOSE: 'EXECUTOR_CANNOT_PROPOSE', + SCOUT_NO_CLAIM: 'SCOUT_NO_CLAIM', +} as const; + +export type ScoutProposalErrorCode = + (typeof SCOUT_PROPOSAL_ERROR_CODES)[keyof typeof SCOUT_PROPOSAL_ERROR_CODES]; + export interface Observation { id: number; session_id: string; diff --git a/packages/core/test/response-thresholds.test.ts b/packages/core/test/response-thresholds.test.ts index 5d7e0a1..e239049 100644 --- a/packages/core/test/response-thresholds.test.ts +++ b/packages/core/test/response-thresholds.test.ts @@ -112,6 +112,8 @@ describe('rankCandidates', () => { describe('loadProfile / saveProfile', () => { it('loadProfile returns the default capabilities for unknown agents', () => { const profile = loadProfile(store.storage, 'unknown'); + expect(profile.role).toBe('executor'); + expect(profile.openProposalCount).toBe(0); expect(profile.capabilities).toEqual(DEFAULT_CAPABILITIES); expect(profile.updated_at).toBe(0); }); @@ -120,6 +122,8 @@ describe('loadProfile / saveProfile', () => { saveProfile(store.storage, 'claude', { ui_work: 0.9, api_work: 0.4 }); saveProfile(store.storage, 'claude', { ui_work: 0.95 }); const profile = loadProfile(store.storage, 'claude'); + expect(profile.role).toBe('executor'); + expect(profile.openProposalCount).toBe(0); expect(profile.capabilities.ui_work).toBe(0.95); // Other values preserved from previous save. expect(profile.capabilities.api_work).toBe(0.4);