Skip to content

Commit 6eabbf5

Browse files
committed
Enhance trajectory integration for tmux and pty wrappers
- Add TrajectoryIntegration class with lifecycle methods - Auto-start trajectory when agents spawn with a task - Record incoming/outgoing messages in trajectory - Auto-detect PDERO phase transitions from output - Complete trajectory on graceful stop, abandon on kill - Inject compact trail instructions to agents - Set trail environment variables for CLI access
1 parent b926083 commit 6eabbf5

File tree

3 files changed

+258
-7
lines changed

3 files changed

+258
-7
lines changed

src/trajectory/integration.ts

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,15 @@
66
*
77
* This module provides a bridge between agent-relay and the
88
* external `trail` CLI / agent-trajectories library.
9+
*
10+
* Key integration points:
11+
* - Auto-starts trajectory when agent is instantiated with a task
12+
* - Records all inter-agent messages
13+
* - Auto-detects PDERO phase transitions from output
14+
* - Provides hooks for key agent lifecycle events
915
*/
1016

11-
import { spawn } from 'node:child_process';
17+
import { spawn, execSync } from 'node:child_process';
1218
import { getProjectPaths } from '../utils/project-namespace.js';
1319

1420
/**
@@ -280,12 +286,21 @@ export function detectPhaseFromContent(content: string): PDEROPhase | undefined
280286

281287
/**
282288
* TrajectoryIntegration class for managing trajectory state
289+
*
290+
* This class enforces trajectory tracking during agent lifecycle:
291+
* - Auto-starts trajectory when agent is instantiated with a task
292+
* - Records all inter-agent messages
293+
* - Auto-detects PDERO phase transitions
294+
* - Provides lifecycle hooks for tmux/pty wrappers
283295
*/
284296
export class TrajectoryIntegration {
285297
private projectId: string;
286298
private agentName: string;
287299
private trailAvailable: boolean | null = null;
288300
private currentPhase: PDEROPhase | null = null;
301+
private trajectoryId: string | null = null;
302+
private initialized = false;
303+
private task: string | null = null;
289304

290305
constructor(projectId: string, agentName: string) {
291306
this.projectId = projectId;
@@ -302,6 +317,43 @@ export class TrajectoryIntegration {
302317
return this.trailAvailable;
303318
}
304319

320+
/**
321+
* Check if trail CLI is installed synchronously
322+
*/
323+
isTrailInstalledSync(): boolean {
324+
try {
325+
execSync('which trail', { stdio: 'pipe' });
326+
return true;
327+
} catch {
328+
return false;
329+
}
330+
}
331+
332+
/**
333+
* Initialize trajectory tracking for agent lifecycle
334+
* Called automatically when agent starts with a task
335+
*/
336+
async initialize(task?: string, taskId?: string, source?: string): Promise<boolean> {
337+
if (this.initialized) return true;
338+
339+
if (!(await this.isAvailable())) {
340+
return false;
341+
}
342+
343+
// If task provided, auto-start trajectory
344+
if (task) {
345+
const success = await this.start(task, taskId, source);
346+
if (success) {
347+
this.initialized = true;
348+
this.task = task;
349+
}
350+
return success;
351+
}
352+
353+
this.initialized = true;
354+
return true;
355+
}
356+
305357
/**
306358
* Start tracking a trajectory
307359
*/
@@ -318,11 +370,34 @@ export class TrajectoryIntegration {
318370

319371
if (result.success) {
320372
this.currentPhase = 'plan';
373+
this.trajectoryId = result.trajectoryId || null;
374+
this.task = task;
321375
}
322376

323377
return result.success;
324378
}
325379

380+
/**
381+
* Check if there's an active trajectory
382+
*/
383+
hasActiveTrajectory(): boolean {
384+
return this.currentPhase !== null;
385+
}
386+
387+
/**
388+
* Get the current task
389+
*/
390+
getTask(): string | null {
391+
return this.task;
392+
}
393+
394+
/**
395+
* Get trajectory ID
396+
*/
397+
getTrajectoryId(): string | null {
398+
return this.trajectoryId;
399+
}
400+
326401
/**
327402
* Record a message
328403
*/
@@ -433,3 +508,63 @@ export function getTrajectoryIntegration(projectId: string, agentName: string):
433508
}
434509
return instance;
435510
}
511+
512+
/**
513+
* Generate trail usage instructions for agents
514+
*/
515+
export function getTrailInstructions(): string[] {
516+
return [
517+
'📍 TRAJECTORY TRACKING (PDERO Paradigm)',
518+
'',
519+
'You MUST use trail commands to track your work:',
520+
'',
521+
'PHASES: plan → design → execute → review → observe',
522+
'',
523+
'COMMANDS:',
524+
' trail start "task" Start trajectory for a task',
525+
' trail phase <phase> Transition to new phase',
526+
' trail decision "choice" Record key decisions',
527+
' trail event "what happened" Log significant events',
528+
' trail complete Complete with summary',
529+
'',
530+
'WHEN TO USE:',
531+
' - Start: At beginning of any task',
532+
' - Phase: When shifting focus (planning→implementing, etc.)',
533+
' - Decision: For architecture/approach choices',
534+
' - Event: For tool calls, errors, milestones',
535+
' - Complete: When task is done',
536+
'',
537+
'Example workflow:',
538+
' trail start "Implement auth feature"',
539+
' trail phase design',
540+
' trail decision "Use JWT" --reasoning "Stateless, scalable"',
541+
' trail phase execute',
542+
' trail event "Created auth middleware"',
543+
' trail phase review',
544+
' trail event "All tests passing"',
545+
' trail complete --summary "Auth implemented" --confidence 0.9',
546+
];
547+
}
548+
549+
/**
550+
* Get a compact trail instruction string for injection
551+
*/
552+
export function getCompactTrailInstructions(): string {
553+
return [
554+
'[TRAIL] Track work with PDERO: plan→design→execute→review→observe.',
555+
'Commands: trail start "task" | trail phase <phase> | trail decision "choice" | trail event "log" | trail complete',
556+
'Use trail often to document your thought process.',
557+
].join(' ');
558+
}
559+
560+
/**
561+
* Get environment variables for trail CLI
562+
*/
563+
export function getTrailEnvVars(projectId: string, agentName: string, dataDir: string): Record<string, string> {
564+
return {
565+
TRAJECTORIES_PROJECT: projectId,
566+
TRAJECTORIES_DATA_DIR: dataDir,
567+
TRAJECTORIES_AGENT: agentName,
568+
TRAIL_AUTO_PHASE: '1', // Enable auto phase detection
569+
};
570+
}

src/wrapper/pty-wrapper.ts

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@ import { EventEmitter } from 'node:events';
1313
import { RelayClient } from './client.js';
1414
import type { ParsedCommand } from './parser.js';
1515
import type { SendPayload, SpeakOnTrigger } from '../protocol/types.js';
16+
import { getProjectPaths } from '../utils/project-namespace.js';
17+
import {
18+
TrajectoryIntegration,
19+
getTrajectoryIntegration,
20+
getTrailEnvVars,
21+
getCompactTrailInstructions,
22+
detectPhaseFromContent,
23+
type PDEROPhase,
24+
} from '../trajectory/integration.js';
1625

1726
/** Maximum lines to keep in output buffer */
1827
const MAX_BUFFER_LINES = 10000;
@@ -42,6 +51,8 @@ export interface PtyWrapperConfig {
4251
shadowSpeakOn?: SpeakOnTrigger[];
4352
/** Stream output to daemon for dashboard log viewing (default: true) */
4453
streamLogs?: boolean;
54+
/** Task/role description for trajectory tracking */
55+
task?: string;
4556
}
4657

4758
export interface PtyWrapperEvents {
@@ -67,6 +78,8 @@ export class PtyWrapper extends EventEmitter {
6778
private logFilePath?: string;
6879
private logStream?: fs.WriteStream;
6980
private hasAcceptedPrompt = false;
81+
private trajectory?: TrajectoryIntegration; // Trajectory tracking via trail
82+
private lastDetectedPhase?: PDEROPhase;
7083

7184
constructor(config: PtyWrapperConfig) {
7285
super();
@@ -81,6 +94,10 @@ export class PtyWrapper extends EventEmitter {
8194
quiet: true,
8295
});
8396

97+
// Initialize trajectory tracking
98+
const projectPaths = getProjectPaths();
99+
this.trajectory = getTrajectoryIntegration(projectPaths.projectId, config.name);
100+
84101
// Handle incoming messages
85102
this.client.onMessage = (from: string, payload: SendPayload, messageId: string) => {
86103
this.handleIncomingMessage(from, payload, messageId);
@@ -131,6 +148,10 @@ export class PtyWrapper extends EventEmitter {
131148
console.log(`[pty:${this.config.name}] Spawning: ${this.config.command} ${args.join(' ')}`);
132149
console.log(`[pty:${this.config.name}] CWD: ${cwd}`);
133150

151+
// Get trail environment variables
152+
const projectPaths = getProjectPaths();
153+
const trailEnvVars = getTrailEnvVars(projectPaths.projectId, this.config.name, projectPaths.dataDir);
154+
134155
// Spawn the process with error handling
135156
try {
136157
this.ptyProcess = pty.spawn(this.config.command, args, {
@@ -141,6 +162,7 @@ export class PtyWrapper extends EventEmitter {
141162
env: {
142163
...process.env,
143164
...this.config.env,
165+
...trailEnvVars,
144166
AGENT_RELAY_NAME: this.config.name,
145167
TERM: 'xterm-256color',
146168
},
@@ -152,6 +174,15 @@ export class PtyWrapper extends EventEmitter {
152174
throw spawnError;
153175
}
154176

177+
// Initialize trajectory (auto-start if task provided)
178+
if (this.config.task) {
179+
this.trajectory?.initialize(this.config.task).then(success => {
180+
if (success) {
181+
console.log(`[pty:${this.config.name}] Trajectory started for task: ${this.config.task}`);
182+
}
183+
});
184+
}
185+
155186
this.running = true;
156187

157188
// Capture output
@@ -215,6 +246,24 @@ export class PtyWrapper extends EventEmitter {
215246

216247
// Parse for relay commands
217248
this.parseRelayCommands();
249+
250+
// Auto-detect phase transitions from output
251+
this.detectAndTransitionPhase(data);
252+
}
253+
254+
/**
255+
* Detect PDERO phase from output and transition if needed
256+
*/
257+
private detectAndTransitionPhase(data: string): void {
258+
if (!this.trajectory) return;
259+
260+
const cleanData = this.stripAnsi(data);
261+
const detectedPhase = detectPhaseFromContent(cleanData);
262+
263+
if (detectedPhase && detectedPhase !== this.lastDetectedPhase) {
264+
this.lastDetectedPhase = detectedPhase;
265+
this.trajectory.transition(detectedPhase, 'Auto-detected from output');
266+
}
218267
}
219268

220269
/**
@@ -386,6 +435,9 @@ export class PtyWrapper extends EventEmitter {
386435
const success = this.client.sendMessage(cmd.to, cmd.body, cmd.kind, cmd.data, cmd.thread);
387436
if (success) {
388437
this.sentMessageHashes.add(msgHash);
438+
439+
// Record outgoing message in trajectory
440+
this.trajectory?.message('sent', this.config.name, cmd.to, cmd.body);
389441
}
390442
}
391443

@@ -514,6 +566,9 @@ export class PtyWrapper extends EventEmitter {
514566
private handleIncomingMessage(from: string, payload: SendPayload, messageId: string): void {
515567
this.messageQueue.push({ from, body: payload.body, messageId });
516568
this.processMessageQueue();
569+
570+
// Record incoming message in trajectory
571+
this.trajectory?.message('received', from, this.config.name, payload.body);
517572
}
518573

519574
/**
@@ -562,7 +617,14 @@ export class PtyWrapper extends EventEmitter {
562617
if (!this.running || !this.ptyProcess) return;
563618

564619
const escapedPrefix = '\\' + this.relayPrefix;
565-
const instructions = `[Agent Relay] You are "${this.config.name}" - connected for real-time messaging. SEND: ${escapedPrefix}AgentName message`;
620+
const relayInstruction = `[Agent Relay] You are "${this.config.name}" - connected for real-time messaging. SEND: ${escapedPrefix}AgentName message`;
621+
622+
// Include trail instructions if available
623+
const trailInstruction = this.trajectory?.isTrailInstalledSync()
624+
? ` ${getCompactTrailInstructions()}`
625+
: '';
626+
627+
const instructions = relayInstruction + trailInstruction;
566628

567629
try {
568630
this.ptyProcess.write(instructions + '\r');
@@ -604,6 +666,11 @@ export class PtyWrapper extends EventEmitter {
604666
if (!this.running) return;
605667
this.running = false;
606668

669+
// Complete trajectory with summary
670+
this.trajectory?.complete({
671+
summary: `Agent ${this.config.name} stopped gracefully`,
672+
});
673+
607674
if (this.ptyProcess) {
608675
// Try graceful termination first
609676
this.ptyProcess.write('\x03'); // Ctrl+C
@@ -623,6 +690,10 @@ export class PtyWrapper extends EventEmitter {
623690
*/
624691
kill(): void {
625692
this.running = false;
693+
694+
// Abandon trajectory (forced termination)
695+
this.trajectory?.abandon(`Agent ${this.config.name} killed`);
696+
626697
if (this.ptyProcess) {
627698
this.ptyProcess.kill();
628699
}

0 commit comments

Comments
 (0)