feat: @mosaic/coord — migrate from v0, gateway integration (P2-005) (#77)
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #77.
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
"@mariozechner/pi-coding-agent": "~0.57.1",
|
||||
"@mosaic/auth": "workspace:^",
|
||||
"@mosaic/brain": "workspace:^",
|
||||
"@mosaic/coord": "workspace:^",
|
||||
"@mosaic/db": "workspace:^",
|
||||
"@mosaic/types": "workspace:^",
|
||||
"@nestjs/common": "^11.0.0",
|
||||
|
||||
@@ -3,9 +3,11 @@ import { AgentService } from './agent.service.js';
|
||||
import { ProviderService } from './provider.service.js';
|
||||
import { RoutingService } from './routing.service.js';
|
||||
import { ProvidersController } from './providers.controller.js';
|
||||
import { CoordModule } from '../coord/coord.module.js';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [CoordModule],
|
||||
providers: [ProviderService, RoutingService, AgentService],
|
||||
controllers: [ProvidersController],
|
||||
exports: [AgentService, ProviderService, RoutingService],
|
||||
|
||||
@@ -8,8 +8,10 @@ import {
|
||||
} from '@mariozechner/pi-coding-agent';
|
||||
import type { Brain } from '@mosaic/brain';
|
||||
import { BRAIN } from '../brain/brain.tokens.js';
|
||||
import { CoordService } from '../coord/coord.service.js';
|
||||
import { ProviderService } from './provider.service.js';
|
||||
import { createBrainTools } from './tools/brain-tools.js';
|
||||
import { createCoordTools } from './tools/coord-tools.js';
|
||||
|
||||
export interface AgentSessionOptions {
|
||||
provider?: string;
|
||||
@@ -36,8 +38,9 @@ export class AgentService implements OnModuleDestroy {
|
||||
constructor(
|
||||
private readonly providerService: ProviderService,
|
||||
@Inject(BRAIN) private readonly brain: Brain,
|
||||
private readonly coordService: CoordService,
|
||||
) {
|
||||
this.customTools = createBrainTools(brain);
|
||||
this.customTools = [...createBrainTools(brain), ...createCoordTools(coordService)];
|
||||
this.logger.log(`Registered ${this.customTools.length} custom tools`);
|
||||
}
|
||||
|
||||
|
||||
81
apps/gateway/src/agent/tools/coord-tools.ts
Normal file
81
apps/gateway/src/agent/tools/coord-tools.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { Type } from '@sinclair/typebox';
|
||||
import type { ToolDefinition } from '@mariozechner/pi-coding-agent';
|
||||
import type { CoordService } from '../../coord/coord.service.js';
|
||||
|
||||
export function createCoordTools(coordService: CoordService): ToolDefinition[] {
|
||||
const getMissionStatus: ToolDefinition = {
|
||||
name: 'coord_mission_status',
|
||||
label: 'Mission Status',
|
||||
description:
|
||||
'Get the current orchestration mission status including milestones, tasks, and active session.',
|
||||
parameters: Type.Object({
|
||||
projectPath: Type.Optional(
|
||||
Type.String({ description: 'Project path. Defaults to gateway working directory.' }),
|
||||
),
|
||||
}),
|
||||
async execute(_toolCallId, params) {
|
||||
const { projectPath } = params as { projectPath?: string };
|
||||
const resolvedPath = projectPath ?? process.cwd();
|
||||
const status = await coordService.getMissionStatus(resolvedPath);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: status ? JSON.stringify(status, null, 2) : 'No active coord mission found.',
|
||||
},
|
||||
],
|
||||
details: undefined,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const listCoordTasks: ToolDefinition = {
|
||||
name: 'coord_list_tasks',
|
||||
label: 'List Coord Tasks',
|
||||
description: 'List all tasks from the orchestration TASKS.md file.',
|
||||
parameters: Type.Object({
|
||||
projectPath: Type.Optional(
|
||||
Type.String({ description: 'Project path. Defaults to gateway working directory.' }),
|
||||
),
|
||||
}),
|
||||
async execute(_toolCallId, params) {
|
||||
const { projectPath } = params as { projectPath?: string };
|
||||
const resolvedPath = projectPath ?? process.cwd();
|
||||
const tasks = await coordService.listTasks(resolvedPath);
|
||||
return {
|
||||
content: [{ type: 'text' as const, text: JSON.stringify(tasks, null, 2) }],
|
||||
details: undefined,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const getCoordTaskDetail: ToolDefinition = {
|
||||
name: 'coord_task_detail',
|
||||
label: 'Coord Task Detail',
|
||||
description: 'Get detailed status for a specific orchestration task.',
|
||||
parameters: Type.Object({
|
||||
taskId: Type.String({ description: 'Task ID (e.g. P2-005)' }),
|
||||
projectPath: Type.Optional(
|
||||
Type.String({ description: 'Project path. Defaults to gateway working directory.' }),
|
||||
),
|
||||
}),
|
||||
async execute(_toolCallId, params) {
|
||||
const { taskId, projectPath } = params as { taskId: string; projectPath?: string };
|
||||
const resolvedPath = projectPath ?? process.cwd();
|
||||
const detail = await coordService.getTaskStatus(resolvedPath, taskId);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text' as const,
|
||||
text: detail
|
||||
? JSON.stringify(detail, null, 2)
|
||||
: `Task ${taskId} not found in coord mission.`,
|
||||
},
|
||||
],
|
||||
details: undefined,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
return [getMissionStatus, listCoordTasks, getCoordTaskDetail];
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export { createBrainTools } from './brain-tools.js';
|
||||
export { createCoordTools } from './coord-tools.js';
|
||||
|
||||
@@ -9,6 +9,7 @@ import { ConversationsModule } from './conversations/conversations.module.js';
|
||||
import { ProjectsModule } from './projects/projects.module.js';
|
||||
import { MissionsModule } from './missions/missions.module.js';
|
||||
import { TasksModule } from './tasks/tasks.module.js';
|
||||
import { CoordModule } from './coord/coord.module.js';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -21,6 +22,7 @@ import { TasksModule } from './tasks/tasks.module.js';
|
||||
ProjectsModule,
|
||||
MissionsModule,
|
||||
TasksModule,
|
||||
CoordModule,
|
||||
],
|
||||
controllers: [HealthController],
|
||||
})
|
||||
|
||||
31
apps/gateway/src/coord/coord.controller.ts
Normal file
31
apps/gateway/src/coord/coord.controller.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Controller, Get, NotFoundException, Param, Query, UseGuards } from '@nestjs/common';
|
||||
import { AuthGuard } from '../auth/auth.guard.js';
|
||||
import { CoordService } from './coord.service.js';
|
||||
|
||||
@Controller('api/coord')
|
||||
@UseGuards(AuthGuard)
|
||||
export class CoordController {
|
||||
constructor(private readonly coordService: CoordService) {}
|
||||
|
||||
@Get('status')
|
||||
async missionStatus(@Query('projectPath') projectPath?: string) {
|
||||
const resolvedPath = projectPath ?? process.cwd();
|
||||
const status = await this.coordService.getMissionStatus(resolvedPath);
|
||||
if (!status) throw new NotFoundException('No active coord mission found');
|
||||
return status;
|
||||
}
|
||||
|
||||
@Get('tasks')
|
||||
async listTasks(@Query('projectPath') projectPath?: string) {
|
||||
const resolvedPath = projectPath ?? process.cwd();
|
||||
return this.coordService.listTasks(resolvedPath);
|
||||
}
|
||||
|
||||
@Get('tasks/:taskId')
|
||||
async taskStatus(@Param('taskId') taskId: string, @Query('projectPath') projectPath?: string) {
|
||||
const resolvedPath = projectPath ?? process.cwd();
|
||||
const detail = await this.coordService.getTaskStatus(resolvedPath, taskId);
|
||||
if (!detail) throw new NotFoundException(`Task ${taskId} not found in coord mission`);
|
||||
return detail;
|
||||
}
|
||||
}
|
||||
49
apps/gateway/src/coord/coord.dto.ts
Normal file
49
apps/gateway/src/coord/coord.dto.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
export interface CoordMissionStatusDto {
|
||||
mission: {
|
||||
id: string;
|
||||
name: string;
|
||||
status: string;
|
||||
projectPath: string;
|
||||
};
|
||||
milestones: {
|
||||
total: number;
|
||||
completed: number;
|
||||
current?: {
|
||||
id: string;
|
||||
name: string;
|
||||
status: string;
|
||||
};
|
||||
};
|
||||
tasks: {
|
||||
total: number;
|
||||
done: number;
|
||||
inProgress: number;
|
||||
pending: number;
|
||||
blocked: number;
|
||||
cancelled: number;
|
||||
};
|
||||
nextTaskId?: string;
|
||||
activeSession?: {
|
||||
sessionId: string;
|
||||
runtime: string;
|
||||
startedAt: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CoordTaskDetailDto {
|
||||
missionId: string;
|
||||
task: {
|
||||
id: string;
|
||||
title: string;
|
||||
status: string;
|
||||
milestone?: string;
|
||||
pr?: string;
|
||||
notes?: string;
|
||||
};
|
||||
isNextTask: boolean;
|
||||
activeSession?: {
|
||||
sessionId: string;
|
||||
runtime: string;
|
||||
startedAt: string;
|
||||
};
|
||||
}
|
||||
10
apps/gateway/src/coord/coord.module.ts
Normal file
10
apps/gateway/src/coord/coord.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { CoordService } from './coord.service.js';
|
||||
import { CoordController } from './coord.controller.js';
|
||||
|
||||
@Module({
|
||||
providers: [CoordService],
|
||||
controllers: [CoordController],
|
||||
exports: [CoordService],
|
||||
})
|
||||
export class CoordModule {}
|
||||
73
apps/gateway/src/coord/coord.service.ts
Normal file
73
apps/gateway/src/coord/coord.service.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import {
|
||||
loadMission,
|
||||
getMissionStatus,
|
||||
getTaskStatus,
|
||||
parseTasksFile,
|
||||
type Mission,
|
||||
type MissionStatusSummary,
|
||||
type MissionTask,
|
||||
type TaskDetail,
|
||||
} from '@mosaic/coord';
|
||||
import { promises as fs } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
@Injectable()
|
||||
export class CoordService {
|
||||
private readonly logger = new Logger(CoordService.name);
|
||||
|
||||
async loadMission(projectPath: string): Promise<Mission | null> {
|
||||
try {
|
||||
return await loadMission(projectPath);
|
||||
} catch (err) {
|
||||
this.logger.debug(
|
||||
`No coord mission at ${projectPath}: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async getMissionStatus(projectPath: string): Promise<MissionStatusSummary | null> {
|
||||
const mission = await this.loadMission(projectPath);
|
||||
if (!mission) return null;
|
||||
|
||||
try {
|
||||
return await getMissionStatus(mission);
|
||||
} catch (err) {
|
||||
this.logger.error(
|
||||
`Failed to get mission status: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async getTaskStatus(projectPath: string, taskId: string): Promise<TaskDetail | null> {
|
||||
const mission = await this.loadMission(projectPath);
|
||||
if (!mission) return null;
|
||||
|
||||
try {
|
||||
return await getTaskStatus(mission, taskId);
|
||||
} catch (err) {
|
||||
this.logger.error(
|
||||
`Failed to get task status for ${taskId}: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async listTasks(projectPath: string): Promise<MissionTask[]> {
|
||||
const mission = await this.loadMission(projectPath);
|
||||
if (!mission) return [];
|
||||
|
||||
const tasksFile = path.isAbsolute(mission.tasksFile)
|
||||
? mission.tasksFile
|
||||
: path.join(mission.projectPath, mission.tasksFile);
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(tasksFile, 'utf8');
|
||||
return parseTasksFile(content);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user