| name | agentop-conventions |
| description | Project-specific conventions for AgentOp development covering data model, attention system, decision flows, and architecture patterns. Use when implementing features that touch the core domain model, working with task hierarchies, handling agent sessions, or implementing the attention system. Triggers on agentop, task, outcome, session, attention, decision point, supervised, delegated, sensitive. |
AgentOp Project Conventions
Core domain model, attention system, and architectural patterns specific to AgentOp.
When to Use This Skill
- Implementing features touching the core domain
- Working with task hierarchies
- Handling agent sessions and steps
- Implementing the attention system
- Building decision point flows
- Adding new entity types
Core Data Model
Entity Relationships
Task (tree hierarchy via parentId)
└── Outcome (many per task, represents expected results)
└── AgentSession (many per outcome, AI execution attempts)
└── AgentStep (stream of: thinking | action | decision_point)
└── DecisionPoint (user approval for sensitive actions)
Type Definitions
export type TaskStatus =
| 'pending'
| 'in_progress'
| 'completed'
| 'blocked';
export type AttentionType =
| 'supervised'
| 'delegated'
| 'sensitive';
export type SessionStatus =
| 'pending'
| 'running'
| 'completed'
| 'error'
| 'cancelled';
export type StepType =
| 'thinking'
| 'action'
| 'decision_point';
export type DecisionStatus =
| 'pending'
| 'approved'
| 'denied'
| 'timeout';
export interface Task {
id: string;
title: string;
description?: string;
parentId: string | null;
status: TaskStatus;
attentionType: AttentionType;
orderIndex: number;
createdAt: Date;
updatedAt: Date;
}
export interface Outcome {
id: string;
taskId: string;
title: string;
description?: string;
status: TaskStatus;
createdAt: Date;
updatedAt: Date;
}
export interface AgentSession {
id: string;
outcomeId: string;
status: SessionStatus;
attentionType: AttentionType;
startedAt: Date;
completedAt?: Date;
error?: string;
tokenUsage?: {
prompt: number;
completion: number;
total: number;
};
}
export interface AgentStep {
id: string;
sessionId: string;
type: StepType;
status: 'in_progress' | 'completed' | 'error' | DecisionStatus;
content?: string;
toolCall?: ToolCall;
result?: ToolResult;
createdAt: Date;
}
export interface ToolCall {
id: string;
name: string;
arguments: Record<string, unknown>;
}
export interface ToolResult {
success: boolean;
result?: string;
error?: string;
}
export interface EventLog {
id: string;
timestamp: Date;
eventType: string;
entityType: 'task' | 'outcome' | 'session' | 'step';
entityId: string;
payload: Record<string, unknown>;
userId?: string;
}
Attention System
Behavior by Type
export const attentionBehavior = {
supervised: {
requiresApproval: () => true,
showStream: true,
timeout: null,
},
delegated: {
requiresApproval: () => false,
showStream: false,
timeout: null,
},
sensitive: {
requiresApproval: (toolName: string) => {
const sensitiveTools = [
'run_command',
'delete_file',
'edit_file',
'request_approval',
];
return sensitiveTools.includes(toolName);
},
showStream: true,
timeout: 5 * 60 * 1000,
},
} as const;
export function needsApproval(
attentionType: AttentionType,
toolName: string
): boolean {
return attentionBehavior[attentionType].requiresApproval(toolName);
}
UI Indicators
import { cn } from '@/lib/utils';
import { Eye, Bot, AlertTriangle } from 'lucide-react';
import type { AttentionType } from '@agentop/core';
const config = {
supervised: {
icon: Eye,
label: 'Supervised',
color: 'text-blue-500 bg-blue-500/10',
},
delegated: {
icon: Bot,
label: 'Delegated',
color: 'text-green-500 bg-green-500/10',
},
sensitive: {
icon: AlertTriangle,
label: 'Sensitive',
color: 'text-yellow-500 bg-yellow-500/10',
},
};
export function AttentionBadge({ type }: { type: AttentionType }) {
const { icon: Icon, label, color } = config[type];
return (
<span className={cn('inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs', color)}>
<Icon className="w-3 h-3" />
{label}
</span>
);
}
Task Hierarchy Patterns
Creating Subtasks
export async function createSubtask(
parentId: string,
title: string,
db: Database
): Promise<Task> {
const parent = await db.query.tasks.findFirst({
where: eq(tasks.id, parentId),
});
if (!parent) throw new Error('Parent task not found');
const siblings = await db.query.tasks.findMany({
where: eq(tasks.parentId, parentId),
orderBy: desc(tasks.orderIndex),
limit: 1,
});
const nextOrderIndex = siblings.length > 0
? siblings[0].orderIndex + 1
: 0;
const subtask: NewTask = {
id: nanoid(),
title,
parentId,
status: 'pending',
attentionType: parent.attentionType,
orderIndex: nextOrderIndex,
createdAt: new Date(),
updatedAt: new Date(),
};
await db.insert(tasks).values(subtask);
await updateTaskClosure(db, subtask.id, parentId);
return subtask;
}
Reordering with Fractional Indexing
export function calculateOrderIndex(
prevIndex: number | null,
nextIndex: number | null
): number {
if (prevIndex === null && nextIndex === null) {
return 0;
}
if (prevIndex === null) {
return nextIndex! - 1;
}
if (nextIndex === null) {
return prevIndex + 1;
}
return (prevIndex + nextIndex) / 2;
}
async function handleDragEnd(
taskId: string,
overTaskId: string,
position: 'before' | 'after'
) {
const siblings = await getSiblingTasks(taskId);
const overIndex = siblings.findIndex(t => t.id === overTaskId);
const prevTask = position === 'before'
? siblings[overIndex - 1]
: siblings[overIndex];
const nextTask = position === 'before'
? siblings[overIndex]
: siblings[overIndex + 1];
const newOrderIndex = calculateOrderIndex(
prevTask?.orderIndex ?? null,
nextTask?.orderIndex ?? null
);
await updateTask(taskId, { orderIndex: newOrderIndex });
}
Session Flow Conventions
Starting a Session
export async function startSession(
outcomeId: string,
systemPrompt: string,
userMessage: string
): Promise<AgentSession> {
const outcome = await db.query.outcomes.findFirst({
where: eq(outcomes.id, outcomeId),
with: { task: true },
});
if (!outcome) throw new Error('Outcome not found');
const session: NewAgentSession = {
id: nanoid(),
outcomeId,
status: 'pending',
attentionType: outcome.task.attentionType,
startedAt: new Date(),
};
await db.insert(agentSessions).values(session);
await logEvent({
eventType: 'session_started',
entityType: 'session',
entityId: session.id,
payload: { outcomeId, attentionType: session.attentionType },
});
return session;
}
Handling Decision Points
export async function handleDecisionPoint(
stepId: string,
approved: boolean
): Promise<void> {
const step = await db.query.agentSteps.findFirst({
where: eq(agentSteps.id, stepId),
});
if (!step || step.type !== 'decision_point') {
throw new Error('Invalid decision point');
}
if (step.status !== 'pending') {
throw new Error('Decision already made');
}
await db.update(agentSteps)
.set({
status: approved ? 'approved' : 'denied',
})
.where(eq(agentSteps.id, stepId));
await logEvent({
eventType: approved ? 'decision_approved' : 'decision_denied',
entityType: 'step',
entityId: stepId,
payload: { toolCall: step.toolCall },
});
}
File Organization
packages/
core/ # Shared types and utilities
src/
types.ts # All type definitions
attention.ts # Attention system logic
validation.ts # Zod schemas
index.ts # Public exports
main/ # Electron main process
src/
database/ # SQLite + Drizzle
copilot/ # Copilot SDK integration
ipc/ # IPC handlers
renderer/ # React frontend
src/
components/
ui/ # shadcn/ui components
tree/ # Task tree view
canvas/ # Mindmap canvas
session/ # Agent session UI
state/ # Jotai atoms
hooks/ # React hooks
Naming Conventions
| Category | Convention | Example |
|---|
| Files | kebab-case | task-tree-node.tsx |
| Components | PascalCase | TaskTreeNode |
| Hooks | camelCase with use prefix | useAgentSession |
| Atoms | camelCase with Atom suffix | tasksMapAtom |
| AtomFamily | camelCase with AtomFamily suffix | taskAtomFamily |
| IPC Channels | colon-separated namespace | db:tasks:create |
| Event Types | snake_case | session_started |
| Database Tables | snake_case plural | agent_sessions |
Error Handling
export class AgentOpError extends Error {
constructor(
message: string,
public code: string,
public context?: Record<string, unknown>
) {
super(message);
this.name = 'AgentOpError';
}
}
export class TaskNotFoundError extends AgentOpError {
constructor(taskId: string) {
super(`Task not found: ${taskId}`, 'TASK_NOT_FOUND', { taskId });
}
}
export class SessionInProgressError extends AgentOpError {
constructor(sessionId: string) {
super(
`Session already in progress: ${sessionId}`,
'SESSION_IN_PROGRESS',
{ sessionId }
);
}
}
try {
await startSession(outcomeId, prompt, message);
} catch (error) {
if (error instanceof TaskNotFoundError) {
} else if (error instanceof SessionInProgressError) {
} else {
throw error;
}
}
Testing Conventions
describe('agentop/attention', () => {
describe('needsApproval', () => {
it('returns true for all tools in supervised mode', () => {
expect(needsApproval('supervised', 'read_file')).toBe(true);
expect(needsApproval('supervised', 'run_command')).toBe(true);
});
it('returns false for all tools in delegated mode', () => {
expect(needsApproval('delegated', 'read_file')).toBe(false);
expect(needsApproval('delegated', 'run_command')).toBe(false);
});
it('returns true only for sensitive tools in sensitive mode', () => {
expect(needsApproval('sensitive', 'read_file')).toBe(false);
expect(needsApproval('sensitive', 'run_command')).toBe(true);
});
});
});
Git Commit Conventions
feat(task): add subtask creation with inherited attention type
fix(session): prevent concurrent sessions for same outcome
refactor(state): migrate from useState to Jotai atoms
docs(readme): add development setup instructions
test(attention): add unit tests for needsApproval
chore(deps): update @github/copilot-sdk to 1.2.0