+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| protocol.ts | +
+
+ |
+ 91.89% | +68/74 | +83.33% | +45/54 | +100% | +13/13 | +91.89% | +68/74 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 | + +1x +1x + + + + +1x + + + + + + + +27x +27x + + + + + + + + + + + + + + +2048x + +2048x + + + + + + + + + + + + +2048x + + +2048x + +2048x + + + + + + +14x +14x + + +14x +1x + + +14x +1x + + +14x +1x + + +14x + + +14x +14x +1x + + + +14x +1x + + +13x +13x + + + + +14x +1x + + +14x +1x + + + +14x +2x +1x + + + +14x +2x + + + +14x +14x +1x + + +14x + + + + + + + + + + +9x + + + + + + +4x + +4x +1x + + +3x + + +3x +3x +7x +7x + + + +3x +7x +7x +7x + +7x + + + +3x +7x +7x +7x +7x + + + +3x + + + + + + + + + + + + + +5x + + + + + + +2x + + + + + +21x + + + +7x +3x + +4x + + + +7x + +3x + +4x + + + + + + + + + + + + | import { v4 as uuidv4 } from 'uuid';
+import { HandshakeMarker, HandoffState, ValidationResult } from './types';
+import { HandoffStorage } from '../storage/HandoffStorage';
+import { ATOMIntegration } from '../integrations/ATOMIntegration';
+
+/**
+ * Main HandshakeProtocol class for managing multi-agent handoffs
+ */
+export class HandshakeProtocol {
+ private storage: HandoffStorage;
+ private atomIntegration: ATOMIntegration;
+
+ constructor(
+ storageDir: string = '.wave/handoffs',
+ atomDir: string = '.wave/atom-trail'
+ ) {
+ this.storage = new HandoffStorage(storageDir);
+ this.atomIntegration = new ATOMIntegration(atomDir);
+ }
+
+ /**
+ * Create a new handoff marker
+ */
+ async createHandoff(
+ fromAgent: string,
+ toAgent: string,
+ state: HandoffState,
+ context: Record<string, any>,
+ sessionId: string,
+ coherenceScore?: number
+ ): Promise<HandshakeMarker> {
+ // Generate UUID using a more Jest-friendly approach
+ const uuid = require('crypto').randomUUID();
+
+ const marker: HandshakeMarker = {
+ id: uuid,
+ timestamp: new Date().toISOString(),
+ fromAgent,
+ toAgent,
+ state,
+ context,
+ atomTrailId: `ATOM-${require('crypto').randomUUID()}`,
+ coherenceScore,
+ sessionId
+ };
+
+ // Save the marker
+ await this.storage.saveMarker(marker);
+
+ // Log to ATOM trail
+ await this.atomIntegration.logHandoff(marker);
+
+ return marker;
+ }
+
+ /**
+ * Validate a handoff marker
+ */
+ async validateHandoff(marker: HandshakeMarker): Promise<ValidationResult> {
+ const errors: string[] = [];
+ const warnings: string[] = [];
+
+ // Basic validation
+ if (!marker.id) {
+ errors.push('Marker ID is required');
+ }
+
+ if (!marker.fromAgent || marker.fromAgent.trim() === '') {
+ errors.push('fromAgent is required');
+ }
+
+ if (!marker.toAgent || marker.toAgent.trim() === '') {
+ errors.push('toAgent is required');
+ }
+
+ Iif (!marker.state) {
+ errors.push('state is required');
+ } else {
+ const validStates: HandoffState[] = ['WAVE', 'PASS', 'BLOCK', 'HOLD', 'PUSH'];
+ if (!validStates.includes(marker.state)) {
+ errors.push(`Invalid state: ${marker.state}`);
+ }
+ }
+
+ if (!marker.timestamp) {
+ errors.push('timestamp is required');
+ } else {
+ // Validate ISO 8601 format
+ const date = new Date(marker.timestamp);
+ Iif (isNaN(date.getTime())) {
+ errors.push('Invalid timestamp format');
+ }
+ }
+
+ if (!marker.atomTrailId) {
+ errors.push('atomTrailId is required');
+ }
+
+ if (!marker.sessionId) {
+ errors.push('sessionId is required');
+ }
+
+ // Warnings
+ if (marker.coherenceScore !== undefined) {
+ if (marker.coherenceScore < 0 || marker.coherenceScore > 100) {
+ warnings.push('coherenceScore should be between 0 and 100');
+ }
+ }
+
+ if (marker.state === 'WAVE' && marker.coherenceScore === undefined) {
+ warnings.push('WAVE state typically includes a coherenceScore');
+ }
+
+ // Verify marker exists in storage
+ const storedMarker = await this.storage.findMarkerById(marker.id);
+ if (!storedMarker) {
+ warnings.push('Marker not found in storage');
+ }
+
+ return {
+ valid: errors.length === 0,
+ errors: errors.length > 0 ? errors : undefined,
+ warnings: warnings.length > 0 ? warnings : undefined
+ };
+ }
+
+ /**
+ * Get the handoff chain for a session
+ */
+ async getHandoffChain(sessionId: string): Promise<HandshakeMarker[]> {
+ return await this.storage.loadMarkers(sessionId);
+ }
+
+ /**
+ * Generate a Mermaid diagram for workflow visualization
+ */
+ async visualizeWorkflow(sessionId: string): Promise<string> {
+ const markers = await this.getHandoffChain(sessionId);
+
+ if (markers.length === 0) {
+ return 'graph LR\n Empty["No handoffs found"]';
+ }
+
+ let mermaid = 'graph LR\n';
+
+ // Track unique agents
+ const agents = new Set<string>();
+ markers.forEach(m => {
+ agents.add(m.fromAgent);
+ agents.add(m.toAgent);
+ });
+
+ // Generate connections
+ markers.forEach((marker, index) => {
+ const from = this.sanitizeNodeName(marker.fromAgent);
+ const to = this.sanitizeNodeName(marker.toAgent);
+ const label = this.getStateLabel(marker.state, marker.coherenceScore);
+
+ mermaid += ` ${from} -->|${label}| ${to}\n`;
+ });
+
+ // Add styling based on state
+ markers.forEach((marker) => {
+ const to = this.sanitizeNodeName(marker.toAgent);
+ const style = this.getNodeStyle(marker.state);
+ Eif (style) {
+ mermaid += ` style ${to} ${style}\n`;
+ }
+ });
+
+ return mermaid;
+ }
+
+ /**
+ * Query handoffs by criteria
+ */
+ async queryHandoffs(criteria: {
+ sessionId?: string;
+ fromAgent?: string;
+ toAgent?: string;
+ state?: HandoffState;
+ startTime?: Date;
+ endTime?: Date;
+ }): Promise<HandshakeMarker[]> {
+ return await this.storage.queryMarkers(criteria);
+ }
+
+ /**
+ * Get all session IDs
+ */
+ async getAllSessions(): Promise<string[]> {
+ return await this.storage.getAllSessions();
+ }
+
+ // Helper methods
+
+ private sanitizeNodeName(name: string): string {
+ return name.replace(/[^a-zA-Z0-9]/g, '_');
+ }
+
+ private getStateLabel(state: HandoffState, coherenceScore?: number): string {
+ if (state === 'WAVE' && coherenceScore !== undefined) {
+ return `${state}(${coherenceScore}%)`;
+ }
+ return state;
+ }
+
+ private getNodeStyle(state: HandoffState): string | null {
+ switch (state) {
+ case 'WAVE':
+ return 'fill:#90EE90,stroke:#006400,stroke-width:2px';
+ case 'PASS':
+ return 'fill:#87CEEB,stroke:#0000CD,stroke-width:2px';
+ case 'BLOCK':
+ return 'fill:#FFB6C1,stroke:#DC143C,stroke-width:2px';
+ case 'HOLD':
+ return 'fill:#FFD700,stroke:#FF8C00,stroke-width:2px';
+ case 'PUSH':
+ return 'fill:#DDA0DD,stroke:#8B008B,stroke-width:2px';
+ default:
+ return null;
+ }
+ }
+}
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| handshake | +
+
+ |
+ 91.89% | +68/74 | +83.33% | +45/54 | +100% | +13/13 | +91.89% | +68/74 | +
| integrations | +
+
+ |
+ 100% | +21/21 | +80% | +4/5 | +100% | +5/5 | +100% | +19/19 | +
| storage | +
+
+ |
+ 90.62% | +58/64 | +74.19% | +23/31 | +100% | +14/14 | +96.29% | +52/54 | +
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| 1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 | + + + + + + + + + + + + + + + + +2x + + + +32x + + + + + + +2052x + + + + + + + + + + +2052x +2052x + +2052x + +2052x +2052x +2052x + + + + + + +3x +3x + +3x + +3x +3x +2x +2x + +2x +1x + +1x + + + + | import { HandshakeMarker } from '../handshake/types';
+
+/**
+ * ATOM Trail integration interface
+ */
+export interface ATOMEntry {
+ actor: string;
+ decision: string;
+ rationale: string;
+ outcome: string;
+ coherenceScore?: number;
+ timestamp?: string;
+}
+
+/**
+ * ATOM Trail logger for handoff events
+ */
+export class ATOMIntegration {
+ private atomDir: string;
+
+ constructor(atomDir: string = '.wave/atom-trail') {
+ this.atomDir = atomDir;
+ }
+
+ /**
+ * Log a handoff to the ATOM trail
+ */
+ async logHandoff(marker: HandshakeMarker): Promise<void> {
+ const entry: ATOMEntry = {
+ actor: marker.fromAgent,
+ decision: `H&&S: ${marker.state} to ${marker.toAgent}`,
+ rationale: `Coherence: ${marker.coherenceScore ?? 'N/A'}%, Context: ${JSON.stringify(marker.context)}`,
+ outcome: 'success',
+ coherenceScore: marker.coherenceScore,
+ timestamp: marker.timestamp
+ };
+
+ // For now, we'll create a simple log file
+ // In a real implementation, this would integrate with an existing ATOM trail system
+ const fs = require('fs');
+ const path = require('path');
+
+ await fs.promises.mkdir(this.atomDir, { recursive: true });
+
+ const atomFile = path.join(this.atomDir, `${marker.sessionId}.atom.jsonl`);
+ const line = JSON.stringify(entry) + '\n';
+ await fs.promises.appendFile(atomFile, line, 'utf-8');
+ }
+
+ /**
+ * Get ATOM entries for a session
+ */
+ async getEntries(sessionId: string): Promise<ATOMEntry[]> {
+ const fs = require('fs');
+ const path = require('path');
+
+ const atomFile = path.join(this.atomDir, `${sessionId}.atom.jsonl`);
+
+ try {
+ const content = await fs.promises.readFile(atomFile, 'utf-8');
+ const lines = content.trim().split('\n').filter((line: string) => line.length > 0);
+ return lines.map((line: string) => JSON.parse(line) as ATOMEntry);
+ } catch (error: any) {
+ if (error.code === 'ENOENT') {
+ return [];
+ }
+ throw error;
+ }
+ }
+}
+ |
+ Press n or j to go to the next uncovered block, b, p or k for the previous block. +
+ +| File | ++ | Statements | ++ | Branches | ++ | Functions | ++ | Lines | ++ |
|---|---|---|---|---|---|---|---|---|---|
| ATOMIntegration.ts | +
+
+ |
+ 100% | +21/21 | +80% | +4/5 | +100% | +5/5 | +100% | +19/19 | +