@love-moon/conductor-sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/backend/client.d.ts +62 -0
- package/dist/backend/client.js +207 -0
- package/dist/backend/index.d.ts +1 -0
- package/dist/backend/index.js +1 -0
- package/dist/bin/mcp-server.d.ts +2 -0
- package/dist/bin/mcp-server.js +175 -0
- package/dist/config/index.d.ts +33 -0
- package/dist/config/index.js +152 -0
- package/dist/context/index.d.ts +1 -0
- package/dist/context/index.js +1 -0
- package/dist/context/project_context.d.ts +14 -0
- package/dist/context/project_context.js +92 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.js +2 -0
- package/dist/mcp/notifications.d.ts +20 -0
- package/dist/mcp/notifications.js +44 -0
- package/dist/mcp/server.d.ts +37 -0
- package/dist/mcp/server.js +211 -0
- package/dist/message/index.d.ts +1 -0
- package/dist/message/index.js +1 -0
- package/dist/message/router.d.ts +19 -0
- package/dist/message/router.js +122 -0
- package/dist/orchestrator.d.ts +21 -0
- package/dist/orchestrator.js +20 -0
- package/dist/reporter/event_stream.d.ts +7 -0
- package/dist/reporter/event_stream.js +20 -0
- package/dist/reporter/index.d.ts +1 -0
- package/dist/reporter/index.js +1 -0
- package/dist/session/index.d.ts +2 -0
- package/dist/session/index.js +2 -0
- package/dist/session/manager.d.ts +39 -0
- package/dist/session/manager.js +162 -0
- package/dist/session/store.d.ts +36 -0
- package/dist/session/store.js +147 -0
- package/dist/ws/client.d.ts +49 -0
- package/dist/ws/client.js +296 -0
- package/dist/ws/index.d.ts +1 -0
- package/dist/ws/index.js +1 -0
- package/package.json +31 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
export class ProjectContext {
|
|
5
|
+
root;
|
|
6
|
+
constructor(targetPath) {
|
|
7
|
+
const resolvedPath = path.resolve(targetPath ?? process.cwd());
|
|
8
|
+
this.root = fs.realpathSync(resolvedPath);
|
|
9
|
+
}
|
|
10
|
+
guess() {
|
|
11
|
+
const repoRoot = this.gitRoot(this.root);
|
|
12
|
+
return {
|
|
13
|
+
projectRoot: this.root,
|
|
14
|
+
repoRoot: repoRoot ?? undefined,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
listFiles(relativeToRepo = true) {
|
|
18
|
+
const guess = this.guess();
|
|
19
|
+
if (guess.repoRoot) {
|
|
20
|
+
const files = this.gitListFiles(guess.repoRoot);
|
|
21
|
+
if (relativeToRepo) {
|
|
22
|
+
return files;
|
|
23
|
+
}
|
|
24
|
+
return files.map((file) => path.join(guess.repoRoot, file));
|
|
25
|
+
}
|
|
26
|
+
const result = [];
|
|
27
|
+
for (const filePath of walkFiles(guess.projectRoot)) {
|
|
28
|
+
result.push(relativeToRepo ? path.relative(guess.projectRoot, filePath) : filePath);
|
|
29
|
+
}
|
|
30
|
+
return result.sort();
|
|
31
|
+
}
|
|
32
|
+
readFile(relativePath) {
|
|
33
|
+
const guess = this.guess();
|
|
34
|
+
const base = guess.repoRoot ?? guess.projectRoot;
|
|
35
|
+
const target = path.resolve(base, relativePath);
|
|
36
|
+
return fs.readFileSync(target, 'utf-8');
|
|
37
|
+
}
|
|
38
|
+
getDiff(staged = false) {
|
|
39
|
+
const guess = this.guess();
|
|
40
|
+
if (!guess.repoRoot) {
|
|
41
|
+
return '';
|
|
42
|
+
}
|
|
43
|
+
const args = ['diff'];
|
|
44
|
+
if (staged) {
|
|
45
|
+
args.push('--staged');
|
|
46
|
+
}
|
|
47
|
+
return runGit(args, guess.repoRoot);
|
|
48
|
+
}
|
|
49
|
+
gitRoot(start) {
|
|
50
|
+
try {
|
|
51
|
+
const repoPath = runGit(['rev-parse', '--show-toplevel'], start).trim();
|
|
52
|
+
return fs.realpathSync(repoPath);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
gitListFiles(repoRoot) {
|
|
59
|
+
try {
|
|
60
|
+
const output = runGit(['ls-files'], repoRoot);
|
|
61
|
+
return output
|
|
62
|
+
.split(/\r?\n/)
|
|
63
|
+
.map((line) => line.trim())
|
|
64
|
+
.filter(Boolean);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function runGit(args, cwd) {
|
|
72
|
+
const result = spawnSync('git', args, {
|
|
73
|
+
cwd,
|
|
74
|
+
encoding: 'utf-8',
|
|
75
|
+
});
|
|
76
|
+
if (result.status !== 0) {
|
|
77
|
+
throw new Error(result.stderr || 'git command failed');
|
|
78
|
+
}
|
|
79
|
+
return result.stdout;
|
|
80
|
+
}
|
|
81
|
+
function* walkFiles(root) {
|
|
82
|
+
const entries = fs.readdirSync(root, { withFileTypes: true });
|
|
83
|
+
for (const entry of entries) {
|
|
84
|
+
const fullPath = path.join(root, entry.name);
|
|
85
|
+
if (entry.isDirectory()) {
|
|
86
|
+
yield* walkFiles(fullPath);
|
|
87
|
+
}
|
|
88
|
+
else if (entry.isFile()) {
|
|
89
|
+
yield fullPath;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './config/index.js';
|
|
2
|
+
export * from './backend/index.js';
|
|
3
|
+
export * from './ws/index.js';
|
|
4
|
+
export * from './session/index.js';
|
|
5
|
+
export * from './message/index.js';
|
|
6
|
+
export * from './context/index.js';
|
|
7
|
+
export * from './mcp/index.js';
|
|
8
|
+
export * from './reporter/index.js';
|
|
9
|
+
export * from './orchestrator.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './config/index.js';
|
|
2
|
+
export * from './backend/index.js';
|
|
3
|
+
export * from './ws/index.js';
|
|
4
|
+
export * from './session/index.js';
|
|
5
|
+
export * from './message/index.js';
|
|
6
|
+
export * from './context/index.js';
|
|
7
|
+
export * from './mcp/index.js';
|
|
8
|
+
export * from './reporter/index.js';
|
|
9
|
+
export * from './orchestrator.js';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface LogSession {
|
|
2
|
+
sendLogMessage(args: {
|
|
3
|
+
level: string;
|
|
4
|
+
data: string;
|
|
5
|
+
logger?: string;
|
|
6
|
+
}): Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
export interface MCPContext {
|
|
9
|
+
requestContext?: {
|
|
10
|
+
session?: LogSession;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export declare class MCPNotifier {
|
|
14
|
+
private session?;
|
|
15
|
+
private readonly lock;
|
|
16
|
+
bindContext(ctx?: MCPContext | null): Promise<void>;
|
|
17
|
+
setSession(session: LogSession): Promise<void>;
|
|
18
|
+
notifyNewMessage(taskId: string): Promise<void>;
|
|
19
|
+
private getSession;
|
|
20
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
class AsyncLock {
|
|
2
|
+
tail = Promise.resolve();
|
|
3
|
+
async runExclusive(fn) {
|
|
4
|
+
const run = this.tail.then(fn, fn);
|
|
5
|
+
this.tail = run
|
|
6
|
+
.then(() => undefined)
|
|
7
|
+
.catch(() => undefined);
|
|
8
|
+
return run;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class MCPNotifier {
|
|
12
|
+
session;
|
|
13
|
+
lock = new AsyncLock();
|
|
14
|
+
async bindContext(ctx) {
|
|
15
|
+
if (!ctx?.requestContext?.session) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
await this.setSession(ctx.requestContext.session);
|
|
19
|
+
}
|
|
20
|
+
async setSession(session) {
|
|
21
|
+
await this.lock.runExclusive(async () => {
|
|
22
|
+
this.session = session;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
async notifyNewMessage(taskId) {
|
|
26
|
+
const session = await this.getSession();
|
|
27
|
+
if (!session) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
await session.sendLogMessage({
|
|
32
|
+
level: 'info',
|
|
33
|
+
data: `任务 ${taskId} 收到了新的消息,请调用 receive_messages 工具查看。`,
|
|
34
|
+
logger: 'conductor.notifications',
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// Best-effort notification; ignore errors.
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async getSession() {
|
|
42
|
+
return this.lock.runExclusive(async () => this.session);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { BackendPayload, MessageRouter } from '../message/router.js';
|
|
2
|
+
import { SessionManager } from '../session/manager.js';
|
|
3
|
+
import { SessionDiskStore } from '../session/store.js';
|
|
4
|
+
import { ConductorConfig } from '../config/index.js';
|
|
5
|
+
import { BackendApiClient } from '../backend/index.js';
|
|
6
|
+
type BackendSender = (payload: BackendPayload) => Promise<void>;
|
|
7
|
+
export interface MCPServerOptions {
|
|
8
|
+
sessionManager: SessionManager;
|
|
9
|
+
messageRouter: MessageRouter;
|
|
10
|
+
backendSender: BackendSender;
|
|
11
|
+
backendApi: Pick<BackendApiClient, 'listProjects' | 'listTasks' | 'createProject' | 'createTask'>;
|
|
12
|
+
sessionStore?: SessionDiskStore;
|
|
13
|
+
env?: Record<string, string | undefined>;
|
|
14
|
+
}
|
|
15
|
+
type ToolRequest = Record<string, any>;
|
|
16
|
+
type ToolResponse = Record<string, any>;
|
|
17
|
+
export declare class MCPServer {
|
|
18
|
+
private readonly config;
|
|
19
|
+
private readonly options;
|
|
20
|
+
private readonly tools;
|
|
21
|
+
private readonly sessionStore;
|
|
22
|
+
private readonly env;
|
|
23
|
+
constructor(config: ConductorConfig, options: MCPServerOptions);
|
|
24
|
+
handleRequest(toolName: string, payload: ToolRequest): Promise<ToolResponse>;
|
|
25
|
+
private toolCreateTaskSession;
|
|
26
|
+
private toolSendMessage;
|
|
27
|
+
private toolReceiveMessages;
|
|
28
|
+
private toolAckMessages;
|
|
29
|
+
private toolListProjects;
|
|
30
|
+
private toolCreateProject;
|
|
31
|
+
private toolListTasks;
|
|
32
|
+
private toolGetLocalProjectId;
|
|
33
|
+
private resolveHostname;
|
|
34
|
+
private waitForTaskCreation;
|
|
35
|
+
private readIntEnv;
|
|
36
|
+
}
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { SessionDiskStore, currentHostname, currentSessionId } from '../session/store.js';
|
|
3
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
4
|
+
export class MCPServer {
|
|
5
|
+
config;
|
|
6
|
+
options;
|
|
7
|
+
tools;
|
|
8
|
+
sessionStore;
|
|
9
|
+
env;
|
|
10
|
+
constructor(config, options) {
|
|
11
|
+
this.config = config;
|
|
12
|
+
this.options = options;
|
|
13
|
+
this.sessionStore = options.sessionStore ?? new SessionDiskStore();
|
|
14
|
+
this.env = options.env ?? process.env;
|
|
15
|
+
this.tools = {
|
|
16
|
+
create_task_session: this.toolCreateTaskSession,
|
|
17
|
+
send_message: this.toolSendMessage,
|
|
18
|
+
receive_messages: this.toolReceiveMessages,
|
|
19
|
+
ack_messages: this.toolAckMessages,
|
|
20
|
+
list_projects: this.toolListProjects,
|
|
21
|
+
create_project: this.toolCreateProject,
|
|
22
|
+
list_tasks: this.toolListTasks,
|
|
23
|
+
get_local_project_id: this.toolGetLocalProjectId,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
async handleRequest(toolName, payload) {
|
|
27
|
+
const handler = this.tools[toolName];
|
|
28
|
+
if (!handler) {
|
|
29
|
+
throw new Error(`Unknown tool: ${toolName}`);
|
|
30
|
+
}
|
|
31
|
+
return handler.call(this, payload);
|
|
32
|
+
}
|
|
33
|
+
async toolCreateTaskSession(payload) {
|
|
34
|
+
const projectId = String(payload.project_id || '');
|
|
35
|
+
if (!projectId) {
|
|
36
|
+
throw new Error('project_id is required');
|
|
37
|
+
}
|
|
38
|
+
const title = String(payload.task_title || 'Untitled');
|
|
39
|
+
const taskId = String(payload.task_id || crypto.randomUUID());
|
|
40
|
+
const sessionId = String(payload.session_id || taskId);
|
|
41
|
+
console.error(`[mcp] create_task_session task=${taskId} project=${projectId} title=${title} session=${sessionId}`);
|
|
42
|
+
await this.options.sessionManager.addSession(taskId, sessionId, projectId);
|
|
43
|
+
// Create task in database via HTTP API
|
|
44
|
+
await this.options.backendApi.createTask({
|
|
45
|
+
id: taskId,
|
|
46
|
+
projectId,
|
|
47
|
+
title,
|
|
48
|
+
initialContent: payload.prefill,
|
|
49
|
+
});
|
|
50
|
+
await this.waitForTaskCreation(projectId, taskId);
|
|
51
|
+
const projectPath = typeof payload.project_path === 'string' && payload.project_path
|
|
52
|
+
? payload.project_path
|
|
53
|
+
: process.cwd();
|
|
54
|
+
this.sessionStore.upsert({
|
|
55
|
+
projectId,
|
|
56
|
+
taskId,
|
|
57
|
+
projectPath,
|
|
58
|
+
sessionId: currentSessionId(this.env),
|
|
59
|
+
hostname: this.resolveHostname(),
|
|
60
|
+
});
|
|
61
|
+
return {
|
|
62
|
+
task_id: taskId,
|
|
63
|
+
session_id: sessionId,
|
|
64
|
+
app_url: payload.app_url,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
async toolSendMessage(payload) {
|
|
68
|
+
const taskId = String(payload.task_id || '');
|
|
69
|
+
if (!taskId) {
|
|
70
|
+
throw new Error('task_id required');
|
|
71
|
+
}
|
|
72
|
+
await this.options.backendSender({
|
|
73
|
+
type: 'sdk_message',
|
|
74
|
+
payload: {
|
|
75
|
+
task_id: taskId,
|
|
76
|
+
content: payload.content,
|
|
77
|
+
metadata: payload.metadata,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
return { delivered: true };
|
|
81
|
+
}
|
|
82
|
+
async toolReceiveMessages(payload) {
|
|
83
|
+
const taskId = String(payload.task_id || '');
|
|
84
|
+
if (!taskId) {
|
|
85
|
+
throw new Error('task_id required');
|
|
86
|
+
}
|
|
87
|
+
const limit = typeof payload.limit === 'number' ? payload.limit : 20;
|
|
88
|
+
const messages = await this.options.sessionManager.popMessages(taskId, limit);
|
|
89
|
+
return formatMessagesResponse(messages);
|
|
90
|
+
}
|
|
91
|
+
async toolAckMessages(payload) {
|
|
92
|
+
const taskId = String(payload.task_id || '');
|
|
93
|
+
const ackToken = String(payload.ack_token || '');
|
|
94
|
+
if (!taskId || !ackToken) {
|
|
95
|
+
throw new Error('task_id and ack_token required');
|
|
96
|
+
}
|
|
97
|
+
const success = await this.options.sessionManager.ack(taskId, ackToken);
|
|
98
|
+
return { status: success ? 'ok' : 'ignored' };
|
|
99
|
+
}
|
|
100
|
+
async toolListProjects(_payload) {
|
|
101
|
+
const projects = await this.options.backendApi.listProjects();
|
|
102
|
+
return {
|
|
103
|
+
projects: projects.map((project) => typeof project.asObject === 'function'
|
|
104
|
+
? project.asObject()
|
|
105
|
+
: {
|
|
106
|
+
id: project.id,
|
|
107
|
+
name: project.name ?? null,
|
|
108
|
+
description: project.description ?? null,
|
|
109
|
+
}),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
async toolCreateProject(payload) {
|
|
113
|
+
const name = String(payload.name || '').trim();
|
|
114
|
+
if (!name) {
|
|
115
|
+
throw new Error('name is required');
|
|
116
|
+
}
|
|
117
|
+
const description = payload.description ? String(payload.description) : undefined;
|
|
118
|
+
const metadata = payload.metadata && typeof payload.metadata === 'object' ? payload.metadata : undefined;
|
|
119
|
+
const project = await this.options.backendApi.createProject({
|
|
120
|
+
name,
|
|
121
|
+
description,
|
|
122
|
+
metadata,
|
|
123
|
+
});
|
|
124
|
+
return typeof project.asObject === 'function' ? project.asObject() : project;
|
|
125
|
+
}
|
|
126
|
+
async toolListTasks(payload) {
|
|
127
|
+
const tasks = await this.options.backendApi.listTasks({
|
|
128
|
+
projectId: payload.project_id ? String(payload.project_id) : undefined,
|
|
129
|
+
status: payload.status ? String(payload.status) : undefined,
|
|
130
|
+
});
|
|
131
|
+
return {
|
|
132
|
+
tasks: tasks.map((task) => ({
|
|
133
|
+
id: task.id,
|
|
134
|
+
project_id: task.project_id ?? task.projectId ?? null,
|
|
135
|
+
title: task.title,
|
|
136
|
+
status: task.status,
|
|
137
|
+
created_at: task.created_at ?? task.createdAt ?? null,
|
|
138
|
+
updated_at: task.updated_at ?? task.updatedAt ?? null,
|
|
139
|
+
})),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
async toolGetLocalProjectId(payload) {
|
|
143
|
+
const projectPath = typeof payload.project_path === 'string' && payload.project_path
|
|
144
|
+
? payload.project_path
|
|
145
|
+
: process.cwd();
|
|
146
|
+
const record = this.sessionStore.findByPath(projectPath);
|
|
147
|
+
if (!record) {
|
|
148
|
+
throw new Error(`No session record found for project path ${projectPath}`);
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
project_id: record.projectId,
|
|
152
|
+
task_id: Array.from(record.taskIds),
|
|
153
|
+
session_id: record.sessionId,
|
|
154
|
+
hostname: record.hostname,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
resolveHostname() {
|
|
158
|
+
const records = this.sessionStore.load();
|
|
159
|
+
for (const record of records) {
|
|
160
|
+
if (record.hostname) {
|
|
161
|
+
return record.hostname;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return currentHostname();
|
|
165
|
+
}
|
|
166
|
+
async waitForTaskCreation(projectId, taskId) {
|
|
167
|
+
const retries = this.readIntEnv('CONDUCTOR_TASK_CREATE_RETRIES', 10);
|
|
168
|
+
if (retries <= 0) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const delayMs = this.readIntEnv('CONDUCTOR_TASK_CREATE_DELAY_MS', 250);
|
|
172
|
+
for (let attempt = 0; attempt < retries; attempt += 1) {
|
|
173
|
+
try {
|
|
174
|
+
const tasks = await this.options.backendApi.listTasks({ projectId });
|
|
175
|
+
if (tasks.some((task) => String(task?.id || '') === taskId)) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
181
|
+
console.warn(`[mcp] create_task_session unable to confirm task ${taskId}: ${message}`);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (attempt < retries - 1) {
|
|
185
|
+
await sleep(delayMs);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
console.warn(`[mcp] create_task_session timed out waiting for task ${taskId}`);
|
|
189
|
+
}
|
|
190
|
+
readIntEnv(key, fallback) {
|
|
191
|
+
const raw = this.env[key];
|
|
192
|
+
if (!raw) {
|
|
193
|
+
return fallback;
|
|
194
|
+
}
|
|
195
|
+
const value = parseInt(raw, 10);
|
|
196
|
+
return Number.isFinite(value) ? value : fallback;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function formatMessagesResponse(messages) {
|
|
200
|
+
return {
|
|
201
|
+
messages: messages.map((msg) => ({
|
|
202
|
+
message_id: msg.messageId,
|
|
203
|
+
role: msg.role,
|
|
204
|
+
content: msg.content,
|
|
205
|
+
ack_token: msg.ackToken,
|
|
206
|
+
created_at: msg.createdAt.toISOString(),
|
|
207
|
+
})),
|
|
208
|
+
next_ack_token: messages.length ? messages[messages.length - 1].ackToken ?? null : null,
|
|
209
|
+
has_more: false,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './router.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './router.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { SessionManager } from '../session/manager.js';
|
|
2
|
+
import { MCPNotifier } from '../mcp/notifications.js';
|
|
3
|
+
export type BackendPayload = Record<string, any>;
|
|
4
|
+
export type OutboundHandler = (payload: BackendPayload) => Promise<void> | void;
|
|
5
|
+
export declare class MessageRouter {
|
|
6
|
+
private readonly sessions;
|
|
7
|
+
private readonly notifier?;
|
|
8
|
+
private readonly outboundHandlers;
|
|
9
|
+
constructor(sessions: SessionManager, notifier?: MCPNotifier | undefined);
|
|
10
|
+
registerOutboundHandler(handler: OutboundHandler): void;
|
|
11
|
+
handleBackendEvent(payload: BackendPayload): Promise<void>;
|
|
12
|
+
sendToBackend(payload: BackendPayload): Promise<void>;
|
|
13
|
+
private notify;
|
|
14
|
+
private resolveMessageId;
|
|
15
|
+
private coerceRole;
|
|
16
|
+
private coerceContent;
|
|
17
|
+
private formatActionContent;
|
|
18
|
+
private ensureSession;
|
|
19
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
const RANDOM_UUID = () => (typeof crypto.randomUUID === 'function' ? crypto.randomUUID() : crypto.randomBytes(16).toString('hex'));
|
|
3
|
+
export class MessageRouter {
|
|
4
|
+
sessions;
|
|
5
|
+
notifier;
|
|
6
|
+
outboundHandlers = [];
|
|
7
|
+
constructor(sessions, notifier) {
|
|
8
|
+
this.sessions = sessions;
|
|
9
|
+
this.notifier = notifier;
|
|
10
|
+
}
|
|
11
|
+
registerOutboundHandler(handler) {
|
|
12
|
+
this.outboundHandlers.push(handler);
|
|
13
|
+
}
|
|
14
|
+
async handleBackendEvent(payload) {
|
|
15
|
+
const eventType = payload?.type;
|
|
16
|
+
const data = payload?.payload && typeof payload.payload === 'object' ? payload.payload : {};
|
|
17
|
+
const taskId = typeof data.task_id === 'string' ? data.task_id : undefined;
|
|
18
|
+
if (!taskId) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const projectId = typeof data.project_id === 'string' ? data.project_id : undefined;
|
|
22
|
+
if (eventType === 'task_user_message') {
|
|
23
|
+
await this.ensureSession(taskId, projectId);
|
|
24
|
+
await this.sessions.addMessage(taskId, {
|
|
25
|
+
messageId: this.resolveMessageId(data),
|
|
26
|
+
role: this.coerceRole(data.role, 'user'),
|
|
27
|
+
content: this.coerceContent(data.content),
|
|
28
|
+
ackToken: data.ack_token ? String(data.ack_token) : null,
|
|
29
|
+
});
|
|
30
|
+
await this.notify(taskId);
|
|
31
|
+
}
|
|
32
|
+
else if (eventType === 'task_action') {
|
|
33
|
+
await this.ensureSession(taskId, projectId);
|
|
34
|
+
await this.sessions.addMessage(taskId, {
|
|
35
|
+
messageId: this.resolveMessageId(data),
|
|
36
|
+
role: this.coerceRole(data.role, 'action'),
|
|
37
|
+
content: this.formatActionContent(data),
|
|
38
|
+
ackToken: data.ack_token ? String(data.ack_token) : null,
|
|
39
|
+
});
|
|
40
|
+
await this.notify(taskId);
|
|
41
|
+
}
|
|
42
|
+
else if (eventType === 'task_status_update') {
|
|
43
|
+
const session = await this.sessions.getSession(taskId);
|
|
44
|
+
if (session && typeof data.status === 'string') {
|
|
45
|
+
session.status = data.status;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async sendToBackend(payload) {
|
|
50
|
+
for (const handler of this.outboundHandlers) {
|
|
51
|
+
const result = handler(payload);
|
|
52
|
+
if (result && typeof result.then === 'function') {
|
|
53
|
+
await result;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async notify(taskId) {
|
|
58
|
+
if (!this.notifier) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
await this.notifier.notifyNewMessage(taskId);
|
|
62
|
+
}
|
|
63
|
+
resolveMessageId(data) {
|
|
64
|
+
const candidates = [data.message_id, data.action_id, data.id, data.request_id];
|
|
65
|
+
for (const candidate of candidates) {
|
|
66
|
+
if (typeof candidate === 'string' && candidate.trim()) {
|
|
67
|
+
return candidate;
|
|
68
|
+
}
|
|
69
|
+
if (typeof candidate === 'number') {
|
|
70
|
+
return String(candidate);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return RANDOM_UUID();
|
|
74
|
+
}
|
|
75
|
+
coerceRole(role, fallback) {
|
|
76
|
+
if (typeof role === 'string' && role.trim()) {
|
|
77
|
+
return role;
|
|
78
|
+
}
|
|
79
|
+
return fallback;
|
|
80
|
+
}
|
|
81
|
+
coerceContent(content) {
|
|
82
|
+
if (typeof content === 'string') {
|
|
83
|
+
return content;
|
|
84
|
+
}
|
|
85
|
+
if (content === undefined || content === null) {
|
|
86
|
+
return '';
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
return JSON.stringify(content);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return String(content);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
formatActionContent(data) {
|
|
96
|
+
if (typeof data.content === 'string' && data.content.trim()) {
|
|
97
|
+
return data.content;
|
|
98
|
+
}
|
|
99
|
+
const action = typeof data.action === 'string' && data.action.trim() ? data.action : 'action';
|
|
100
|
+
const args = data.args;
|
|
101
|
+
if (args && typeof args === 'object') {
|
|
102
|
+
const command = args.command;
|
|
103
|
+
if (typeof command === 'string' && command.trim()) {
|
|
104
|
+
return `${action}: ${command.trim()}`;
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
return `${action}: ${JSON.stringify(args)}`;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return `${action}: ${String(args)}`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return action;
|
|
114
|
+
}
|
|
115
|
+
async ensureSession(taskId, projectId) {
|
|
116
|
+
const session = await this.sessions.getSession(taskId);
|
|
117
|
+
if (session || !projectId) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
await this.sessions.addSession(taskId, taskId, projectId);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { MessageRouter } from './message/index.js';
|
|
2
|
+
import { EventReporter } from './reporter/index.js';
|
|
3
|
+
import { SessionManager } from './session/index.js';
|
|
4
|
+
import { ConductorWebSocketClient } from './ws/client.js';
|
|
5
|
+
import { MCPServer } from './mcp/server.js';
|
|
6
|
+
export interface OrchestratorDeps {
|
|
7
|
+
wsClient: ConductorWebSocketClient;
|
|
8
|
+
messageRouter: MessageRouter;
|
|
9
|
+
sessionManager: SessionManager;
|
|
10
|
+
mcpServer: MCPServer;
|
|
11
|
+
reporter: EventReporter;
|
|
12
|
+
}
|
|
13
|
+
export declare class SDKOrchestrator {
|
|
14
|
+
private readonly deps;
|
|
15
|
+
private readonly wsClient;
|
|
16
|
+
private readonly router;
|
|
17
|
+
constructor(deps: OrchestratorDeps);
|
|
18
|
+
start(): Promise<void>;
|
|
19
|
+
stop(): Promise<void>;
|
|
20
|
+
private handleBackendEvent;
|
|
21
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export class SDKOrchestrator {
|
|
2
|
+
deps;
|
|
3
|
+
wsClient;
|
|
4
|
+
router;
|
|
5
|
+
constructor(deps) {
|
|
6
|
+
this.deps = deps;
|
|
7
|
+
this.wsClient = deps.wsClient;
|
|
8
|
+
this.router = deps.messageRouter;
|
|
9
|
+
this.wsClient.registerHandler((payload) => this.handleBackendEvent(payload));
|
|
10
|
+
}
|
|
11
|
+
async start() {
|
|
12
|
+
await this.wsClient.connect();
|
|
13
|
+
}
|
|
14
|
+
async stop() {
|
|
15
|
+
await this.wsClient.disconnect();
|
|
16
|
+
}
|
|
17
|
+
async handleBackendEvent(payload) {
|
|
18
|
+
await this.router.handleBackendEvent(payload);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type BackendSender = (payload: Record<string, any>) => Promise<void>;
|
|
2
|
+
export declare class EventReporter {
|
|
3
|
+
private readonly backendSender;
|
|
4
|
+
constructor(backendSender: BackendSender);
|
|
5
|
+
emit(eventType: string, payload: Record<string, any>): Promise<void>;
|
|
6
|
+
taskStatus(taskId: string, status: string, summary?: string | null): Promise<void>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export class EventReporter {
|
|
2
|
+
backendSender;
|
|
3
|
+
constructor(backendSender) {
|
|
4
|
+
this.backendSender = backendSender;
|
|
5
|
+
}
|
|
6
|
+
async emit(eventType, payload) {
|
|
7
|
+
await this.backendSender({
|
|
8
|
+
type: eventType,
|
|
9
|
+
timestamp: new Date().toISOString(),
|
|
10
|
+
payload,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
async taskStatus(taskId, status, summary) {
|
|
14
|
+
await this.emit('task_status_update', {
|
|
15
|
+
task_id: taskId,
|
|
16
|
+
status,
|
|
17
|
+
summary: summary ?? undefined,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './event_stream.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './event_stream.js';
|