export type WorkflowNodeKind = | 'environment' | 'space' | 'agent' | 'behavior' | 'output'; export type WorkflowPortType = | 'ENV_CONTEXT' | 'SPACE_CONTEXT' | 'AGENT_CONTEXT' | 'OBSERVABLE_INFO' | 'BEHAVIOR_OUTPUT' | 'REPORT'; export type WorkflowNodeStatus = 'ready' | 'warning' | 'error' | 'success'; export interface WorkflowPort { id: string; label: string; type: WorkflowPortType; } export interface WorkflowNode { id: string; title: string; subtitle: string; kind: WorkflowNodeKind; x: number; y: number; status: WorkflowNodeStatus; inputs: WorkflowPort[]; outputs: WorkflowPort[]; config: Array<{ label: string; value: string }>; } export interface WorkflowEdge { id: string; source: string; target: string; type: WorkflowPortType; label: string; } export type AttributeScope = 'endogenous' | 'exogenous' | 'derived'; export interface WorkflowParameter { id: string; nodeId: string; name: string; type: 'Float' | 'Integer' | 'Boolean' | 'String' | 'Coordinate'; scope: AttributeScope; source: string; value: string; status: 'ok' | 'warning' | 'error'; description: string; } export type BehaviorSectionKind = 'observable_pool' | 'input' | 'calculation' | 'output'; export interface BehaviorSection { id: string; nodeId: string; kind: BehaviorSectionKind; title: string; description: string; items: Array<{ label: string; source: string; value: string }>; } export interface WorkflowModel { nodes: WorkflowNode[]; edges: WorkflowEdge[]; parameters: WorkflowParameter[]; behaviorSections: BehaviorSection[]; } export type InitialSpaceType = 'grid' | 'network' | 'continuous' | 'gis'; function createSpaceNode(spaceType: InitialSpaceType): WorkflowNode { const spaceConfig: Record }> = { grid: { title: 'Grid Space', subtitle: '40 x 40 grid for spatial interaction', config: [ { label: 'width', value: '40' }, { label: 'height', value: '40' }, { label: 'horizontal wrap', value: 'true' }, { label: 'vertical wrap', value: 'true' }, { label: 'agent capacity/cell', value: '1' }, ], }, network: { title: 'Network Space', subtitle: 'Nodes and edges for relationship-based interaction', config: [ { label: 'network type', value: 'undirected' }, { label: 'default degree', value: '4' }, { label: 'rewiring', value: 'false' }, ], }, continuous: { title: 'Continuous Space', subtitle: 'Coordinate plane for movement and distance-based sensing', config: [ { label: 'width', value: '100' }, { label: 'height', value: '100' }, { label: 'boundary', value: 'bounded' }, ], }, gis: { title: 'GIS Space', subtitle: 'Map-coordinate space for real geography', config: [ { label: 'projection', value: 'WGS84' }, { label: 'basemap', value: 'not selected' }, { label: 'boundary layer', value: 'required' }, ], }, }; const selected = spaceConfig[spaceType]; return { id: `${spaceType}-space`, title: selected.title, subtitle: selected.subtitle, kind: 'space', x: 100, y: 80, status: spaceType === 'grid' ? 'warning' : 'ready', inputs: [{ id: 'env', label: 'environment', type: 'ENV_CONTEXT' }], outputs: [{ id: 'space', label: 'space state', type: 'SPACE_CONTEXT' }], config: selected.config, }; } export function createDefaultWorkflow(spaceType?: InitialSpaceType): WorkflowModel { const nodes: WorkflowNode[] = [ { id: 'environment', title: 'Environment', subtitle: 'Global model container and shared context', kind: 'environment', x: -220, y: -110, status: 'ready', inputs: [], outputs: [{ id: 'env', label: 'environment', type: 'ENV_CONTEXT' }], config: [ { label: 'model type', value: 'ABM + Game Model' }, { label: 'default agent', value: 'Example Agent' }, ], }, { id: 'example-agent', title: 'Example Agent', subtitle: 'Default agent with attributes and behaviors', kind: 'agent', x: 120, y: -95, status: 'ready', inputs: [ { id: 'space', label: 'located in grid', type: 'SPACE_CONTEXT' }, { id: 'env', label: 'environment context', type: 'ENV_CONTEXT' }, ], outputs: [{ id: 'agent', label: 'agent state', type: 'AGENT_CONTEXT' }], config: [ { label: 'attributes', value: 'endogenous + exogenous' }, { label: 'behavior', value: 'on_sensing' }, ], }, ]; if (spaceType) { nodes.splice(1, 0, createSpaceNode(spaceType)); } const edges: WorkflowEdge[] = [ { id: 'e1', source: 'environment', target: 'example-agent', type: 'ENV_CONTEXT', label: 'default context' }, ]; if (spaceType) { const spaceId = `${spaceType}-space`; edges.splice( 0, 1, { id: 'e1', source: 'environment', target: spaceId, type: 'ENV_CONTEXT', label: 'contains space' }, { id: 'e2', source: spaceId, target: 'example-agent', type: 'SPACE_CONTEXT', label: 'locates agent' }, ); } return { nodes, edges, parameters: [ { id: 'a1', nodeId: 'example-agent', name: 'race', type: 'String', scope: 'endogenous', source: 'initialization', value: 'A | B', status: 'ok', description: 'Agent group identity used by segregation behavior', }, { id: 'a2', nodeId: 'example-agent', name: 'position', type: 'Coordinate', scope: 'derived', source: 'Grid Space connection', value: '(x, y)', status: 'ok', description: 'Automatically added when agent is connected to grid space', }, { id: 'a3', nodeId: 'example-agent', name: 'happiness', type: 'Boolean', scope: 'endogenous', source: 'on_sensing output', value: '--', status: 'warning', description: 'Assigned by behavior after observing neighbors', }, { id: 's1', nodeId: 'grid-space', name: 'cell_capacity', type: 'Integer', scope: 'exogenous', source: 'space config', value: '1', status: 'ok', description: 'How many agents can occupy one grid cell', }, { id: 's2', nodeId: 'grid-space', name: 'neighbor_radius', type: 'Integer', scope: 'exogenous', source: 'space config', value: '--', status: 'error', description: 'Required for on_sensing neighborhood lookup', }, { id: 'n1', nodeId: 'neighbor-agent', name: 'race', type: 'String', scope: 'endogenous', source: 'peer attribute', value: 'visible', status: 'ok', description: 'Selectable observable attribute for Example Agent', }, ], behaviorSections: [ { id: 'b1', nodeId: 'example-agent', kind: 'observable_pool', title: 'Observable Information Pool', description: 'All information the behavior may use, grouped by self and observed subjects.', items: [ { label: 'self.race', source: 'Example Agent', value: 'String' }, { label: 'self.position', source: 'Grid Space', value: 'Coordinate' }, { label: 'neighbor.race', source: 'Neighbor Agent', value: 'String' }, { label: 'space.neighbor_count', source: 'Grid Space', value: 'Integer' }, ], }, { id: 'b2', nodeId: 'example-agent', kind: 'input', title: 'Behavior Inputs', description: 'Selected inputs passed into this behavior.', items: [ { label: 'self.position', source: 'Observable pool', value: 'selected' }, { label: 'neighbor.race', source: 'Observable pool', value: 'selected' }, ], }, { id: 'b3', nodeId: 'example-agent', kind: 'calculation', title: 'Calculation or LLM Call', description: 'How inputs are transformed into a decision.', items: [ { label: 'mode', source: 'user choice', value: 'formula' }, { label: 'rule', source: 'behavior editor', value: 'same_race_neighbors / total_neighbors >= threshold' }, ], }, { id: 'b4', nodeId: 'example-agent', kind: 'output', title: 'Output Assignment', description: 'Where the return value is saved.', items: [ { label: 'return', source: 'calculation', value: 'Boolean' }, { label: 'assign to', source: 'Example Agent', value: 'self.happiness' }, ], }, ], }; } export function getParametersForNode(workflow: WorkflowModel, nodeId: string) { return workflow.parameters.filter((parameter) => parameter.nodeId === nodeId); } export function getBehaviorSectionsForNode(workflow: WorkflowModel, nodeId: string) { return workflow.behaviorSections.filter((section) => section.nodeId === nodeId); } export const NODE_CARD_SIZE = { width: 176, height: 128, }; export function getNodeConnectionAnchor({ x, y, side, }: { x: number; y: number; side: 'left' | 'right'; }) { return { x: side === 'left' ? x : x + NODE_CARD_SIZE.width, y: y + NODE_CARD_SIZE.height / 2, }; }