@rynx-ai/server 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/channel-manager.d.ts +60 -0
- package/dist/channel-manager.js +106 -0
- package/dist/control-api.d.ts +188 -0
- package/dist/control-api.js +834 -0
- package/dist/control-web-dist.d.ts +6 -0
- package/dist/control-web-dist.js +63 -0
- package/dist/emulator-touch-ws.d.ts +8 -0
- package/dist/emulator-touch-ws.js +87 -0
- package/dist/server.d.ts +66 -0
- package/dist/server.js +224 -0
- package/dist/terminal-ws.d.ts +25 -0
- package/dist/terminal-ws.js +123 -0
- package/package.json +38 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime manager for mounted channel instances in the single-process control
|
|
3
|
+
* plane. Holds live channel handles keyed by instance id and can start / stop /
|
|
4
|
+
* reload a single instance without restarting the daemon — the seam the web
|
|
5
|
+
* control page drives.
|
|
6
|
+
*
|
|
7
|
+
* Shared pieces (host/conversationRuntime/codex session store) are injected once
|
|
8
|
+
* and reused across instances; only the per-instance {@link ChannelContext}
|
|
9
|
+
* fields (instanceId / options / defaultAgent) differ, so two Lark bots get
|
|
10
|
+
* distinct credentials + state while sharing one agent runtime.
|
|
11
|
+
*/
|
|
12
|
+
import type { AgentCapabilities, AgentSessionStore, AppConfig, ChannelInstanceDescriptor, ConversationRuntime, SessionBus, SessionLogStore, SessionRegistry } from "@rynx-ai/core";
|
|
13
|
+
export interface ChannelInstanceStatus {
|
|
14
|
+
instanceId: string;
|
|
15
|
+
type: string;
|
|
16
|
+
running: boolean;
|
|
17
|
+
agent?: string;
|
|
18
|
+
/** Connection state from the channel, or null when it exposes no status. */
|
|
19
|
+
connected: boolean | null;
|
|
20
|
+
lastError?: string | null;
|
|
21
|
+
lastEventAt?: string | null;
|
|
22
|
+
}
|
|
23
|
+
export interface ChannelManagerOptions {
|
|
24
|
+
config: AppConfig;
|
|
25
|
+
conversationRuntime: ConversationRuntime;
|
|
26
|
+
capabilities: AgentCapabilities;
|
|
27
|
+
sessionStore: AgentSessionStore;
|
|
28
|
+
/** Durable canonical session log, shared across instances (absent ⇒ no
|
|
29
|
+
* persistence). */
|
|
30
|
+
sessionLog?: SessionLogStore;
|
|
31
|
+
/** Shared canonical session event bus (one instance ⇒ cross-channel fan-out). */
|
|
32
|
+
sessionBus?: SessionBus;
|
|
33
|
+
/** Unified machine-session registry, shared across instances; channels register
|
|
34
|
+
* their sessions' identity rows here so the control plane lists them. */
|
|
35
|
+
sessionRegistry?: SessionRegistry;
|
|
36
|
+
/** Source of truth for current instance descriptors (daemon reads config). */
|
|
37
|
+
loadInstances: () => Promise<ChannelInstanceDescriptor[]>;
|
|
38
|
+
log?: (line: string) => void;
|
|
39
|
+
}
|
|
40
|
+
export declare class ChannelManager {
|
|
41
|
+
private readonly opts;
|
|
42
|
+
private readonly mounted;
|
|
43
|
+
constructor(opts: ChannelManagerOptions);
|
|
44
|
+
/** Mount + start every enabled instance from `loadInstances`. */
|
|
45
|
+
startAll(): Promise<void>;
|
|
46
|
+
/** Stop every mounted instance (on server close). */
|
|
47
|
+
stopAll(): Promise<void>;
|
|
48
|
+
/** Live status of currently-mounted instances. */
|
|
49
|
+
list(): ChannelInstanceStatus[];
|
|
50
|
+
isRunning(instanceId: string): boolean;
|
|
51
|
+
/** Start one instance by id (re-reads descriptors). No-op if already running. */
|
|
52
|
+
startInstance(instanceId: string): Promise<void>;
|
|
53
|
+
/** Stop + unmount one instance. No-op if not running. */
|
|
54
|
+
stopInstance(instanceId: string): Promise<void>;
|
|
55
|
+
/** Stop then start, picking up edited options/agent. */
|
|
56
|
+
reloadInstance(instanceId: string): Promise<void>;
|
|
57
|
+
private mountAndStart;
|
|
58
|
+
private buildContext;
|
|
59
|
+
private log;
|
|
60
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export class ChannelManager {
|
|
2
|
+
opts;
|
|
3
|
+
mounted = new Map();
|
|
4
|
+
constructor(opts) {
|
|
5
|
+
this.opts = opts;
|
|
6
|
+
}
|
|
7
|
+
/** Mount + start every enabled instance from `loadInstances`. */
|
|
8
|
+
async startAll() {
|
|
9
|
+
const instances = await this.opts.loadInstances();
|
|
10
|
+
for (const descriptor of instances) {
|
|
11
|
+
await this.mountAndStart(descriptor);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** Stop every mounted instance (on server close). */
|
|
15
|
+
async stopAll() {
|
|
16
|
+
const ids = [...this.mounted.keys()];
|
|
17
|
+
await Promise.all(ids.map((id) => this.stopInstance(id)));
|
|
18
|
+
}
|
|
19
|
+
/** Live status of currently-mounted instances. */
|
|
20
|
+
list() {
|
|
21
|
+
return [...this.mounted.values()].map(({ descriptor, channel }) => {
|
|
22
|
+
const status = channel.getStatus?.();
|
|
23
|
+
return {
|
|
24
|
+
instanceId: descriptor.instanceId,
|
|
25
|
+
type: descriptor.type,
|
|
26
|
+
running: true,
|
|
27
|
+
agent: descriptor.agent,
|
|
28
|
+
connected: status ? status.connected : null,
|
|
29
|
+
lastError: status?.lastError ?? null,
|
|
30
|
+
lastEventAt: status?.lastEventAt ?? null,
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
isRunning(instanceId) {
|
|
35
|
+
return this.mounted.has(instanceId);
|
|
36
|
+
}
|
|
37
|
+
/** Start one instance by id (re-reads descriptors). No-op if already running. */
|
|
38
|
+
async startInstance(instanceId) {
|
|
39
|
+
if (this.mounted.has(instanceId))
|
|
40
|
+
return;
|
|
41
|
+
const descriptor = (await this.opts.loadInstances()).find((instance) => instance.instanceId === instanceId);
|
|
42
|
+
if (!descriptor) {
|
|
43
|
+
throw new Error(`channel instance "${instanceId}" is not enabled/configured`);
|
|
44
|
+
}
|
|
45
|
+
await this.mountAndStart(descriptor);
|
|
46
|
+
}
|
|
47
|
+
/** Stop + unmount one instance. No-op if not running. */
|
|
48
|
+
async stopInstance(instanceId) {
|
|
49
|
+
const entry = this.mounted.get(instanceId);
|
|
50
|
+
if (!entry)
|
|
51
|
+
return;
|
|
52
|
+
this.mounted.delete(instanceId);
|
|
53
|
+
try {
|
|
54
|
+
await entry.channel.stop();
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
this.log(`instance "${instanceId}" stop failed: ${messageOf(error)}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Stop then start, picking up edited options/agent. */
|
|
61
|
+
async reloadInstance(instanceId) {
|
|
62
|
+
await this.stopInstance(instanceId);
|
|
63
|
+
// Only restart if it's still enabled/configured (disable ⇒ stays stopped).
|
|
64
|
+
const descriptor = (await this.opts.loadInstances()).find((instance) => instance.instanceId === instanceId);
|
|
65
|
+
if (descriptor) {
|
|
66
|
+
await this.mountAndStart(descriptor);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async mountAndStart(descriptor) {
|
|
70
|
+
let channel;
|
|
71
|
+
try {
|
|
72
|
+
channel = descriptor.factory(this.buildContext(descriptor));
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
this.log(`instance "${descriptor.instanceId}" construct failed: ${messageOf(error)}`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
this.mounted.set(descriptor.instanceId, { descriptor, channel });
|
|
79
|
+
try {
|
|
80
|
+
await channel.start();
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
this.log(`instance "${descriptor.instanceId}" start failed: ${messageOf(error)}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
buildContext(descriptor) {
|
|
87
|
+
return {
|
|
88
|
+
config: this.opts.config,
|
|
89
|
+
conversationRuntime: this.opts.conversationRuntime,
|
|
90
|
+
capabilities: this.opts.capabilities,
|
|
91
|
+
sessionStore: this.opts.sessionStore,
|
|
92
|
+
sessionLog: this.opts.sessionLog,
|
|
93
|
+
sessionBus: this.opts.sessionBus,
|
|
94
|
+
sessionRegistry: this.opts.sessionRegistry,
|
|
95
|
+
instanceId: descriptor.instanceId,
|
|
96
|
+
options: descriptor.options,
|
|
97
|
+
defaultAgent: descriptor.agent,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
log(line) {
|
|
101
|
+
(this.opts.log ?? ((message) => console.error(message)))(line);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function messageOf(error) {
|
|
105
|
+
return error instanceof Error ? error.message : String(error);
|
|
106
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import Router from "@koa/router";
|
|
2
|
+
import { type AgentSpec, type AuthMaterial, type ConversationRuntime, type MachineSessionRecord, type AgentRuntimeId, type SessionBus, type SessionLogStore, type SkillInstallRecipe } from "@rynx-ai/core";
|
|
3
|
+
import type { ControlChannel, ControlChannelType, ControlInstanceConfig, ControlAgentSummary, SkillSummary, SkillDetail } from "@rynx-ai/protocol/control";
|
|
4
|
+
import type { CodexSessionStore, InjectOutcome } from "@rynx-ai/runtime";
|
|
5
|
+
import type { ChannelManager } from "./channel-manager.js";
|
|
6
|
+
export interface ControlEmulatorDeps {
|
|
7
|
+
doctor(): Promise<unknown>;
|
|
8
|
+
devices(): Promise<unknown[]>;
|
|
9
|
+
active(): Promise<unknown | null>;
|
|
10
|
+
/** One device's attached session, for session-scoped `active` resolution. */
|
|
11
|
+
deviceSession?(input: {
|
|
12
|
+
device: string;
|
|
13
|
+
}): Promise<unknown | null>;
|
|
14
|
+
attach(device?: string): Promise<unknown>;
|
|
15
|
+
tap(input: {
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
device?: string;
|
|
19
|
+
}): Promise<unknown>;
|
|
20
|
+
type(input: {
|
|
21
|
+
text: string;
|
|
22
|
+
device?: string;
|
|
23
|
+
mode?: string;
|
|
24
|
+
}): Promise<unknown>;
|
|
25
|
+
button(input: {
|
|
26
|
+
name?: string;
|
|
27
|
+
device?: string;
|
|
28
|
+
}): Promise<unknown>;
|
|
29
|
+
rotate(input: {
|
|
30
|
+
orientation: string;
|
|
31
|
+
device?: string;
|
|
32
|
+
}): Promise<unknown>;
|
|
33
|
+
launch(input: {
|
|
34
|
+
appId: string;
|
|
35
|
+
device?: string;
|
|
36
|
+
}): Promise<unknown>;
|
|
37
|
+
gesture(input: {
|
|
38
|
+
points: unknown[];
|
|
39
|
+
device?: string;
|
|
40
|
+
}): Promise<unknown>;
|
|
41
|
+
exec(input: {
|
|
42
|
+
command: string;
|
|
43
|
+
device?: string;
|
|
44
|
+
}): Promise<unknown>;
|
|
45
|
+
screenshot(input: {
|
|
46
|
+
device?: string;
|
|
47
|
+
out?: string;
|
|
48
|
+
}): Promise<unknown>;
|
|
49
|
+
frame?(input: {
|
|
50
|
+
device?: string;
|
|
51
|
+
}): Promise<{
|
|
52
|
+
ok: true;
|
|
53
|
+
backend: "ios" | "android";
|
|
54
|
+
device: string;
|
|
55
|
+
contentType: "image/png";
|
|
56
|
+
data: Buffer;
|
|
57
|
+
}>;
|
|
58
|
+
video?(input: {
|
|
59
|
+
device?: string;
|
|
60
|
+
}): Promise<{
|
|
61
|
+
ok: true;
|
|
62
|
+
backend: "ios" | "android";
|
|
63
|
+
device: string;
|
|
64
|
+
contentType: "video/h264";
|
|
65
|
+
stream: NodeJS.ReadableStream;
|
|
66
|
+
stop(): void;
|
|
67
|
+
}>;
|
|
68
|
+
liveTouch?(input: {
|
|
69
|
+
device?: string;
|
|
70
|
+
}): Promise<{
|
|
71
|
+
ok: true;
|
|
72
|
+
backend: "ios" | "android";
|
|
73
|
+
device: string;
|
|
74
|
+
send(point: {
|
|
75
|
+
type: "begin" | "move" | "end";
|
|
76
|
+
x: number;
|
|
77
|
+
y: number;
|
|
78
|
+
}): void;
|
|
79
|
+
close(): void;
|
|
80
|
+
}>;
|
|
81
|
+
readScreenshotFile?(input: {
|
|
82
|
+
file: string;
|
|
83
|
+
}): Promise<{
|
|
84
|
+
file: string;
|
|
85
|
+
contentType: "image/png";
|
|
86
|
+
data: Buffer;
|
|
87
|
+
} | null>;
|
|
88
|
+
kill(input: {
|
|
89
|
+
device?: string;
|
|
90
|
+
}): Promise<unknown>;
|
|
91
|
+
shutdown(input: {
|
|
92
|
+
device?: string;
|
|
93
|
+
}): Promise<unknown>;
|
|
94
|
+
}
|
|
95
|
+
export interface ControlPlaneDeps {
|
|
96
|
+
listChannelTypes(): ControlChannelType[];
|
|
97
|
+
listChannels(): ControlChannel[];
|
|
98
|
+
createChannel(input: {
|
|
99
|
+
name: string;
|
|
100
|
+
type: string;
|
|
101
|
+
options?: Record<string, unknown>;
|
|
102
|
+
}): {
|
|
103
|
+
id: string;
|
|
104
|
+
};
|
|
105
|
+
setChannel(id: string, patch: {
|
|
106
|
+
name?: string;
|
|
107
|
+
options?: Record<string, unknown>;
|
|
108
|
+
}): void;
|
|
109
|
+
removeChannel(id: string): void;
|
|
110
|
+
/** Run a channel type's authorize flow; resolves with the options to persist. */
|
|
111
|
+
authorizeChannel(type: string, emit: (material: AuthMaterial) => void, signal: AbortSignal): Promise<Record<string, unknown>>;
|
|
112
|
+
listInstancesConfig(): ControlInstanceConfig[];
|
|
113
|
+
createInstance(input: {
|
|
114
|
+
channelId: string;
|
|
115
|
+
agent?: string;
|
|
116
|
+
}): {
|
|
117
|
+
id: string;
|
|
118
|
+
};
|
|
119
|
+
setInstance(id: string, patch: {
|
|
120
|
+
agent?: string;
|
|
121
|
+
enabled?: boolean;
|
|
122
|
+
}): void;
|
|
123
|
+
removeInstance(id: string): void;
|
|
124
|
+
listAgents(): Promise<ControlAgentSummary[]>;
|
|
125
|
+
getAgent(name: string): Promise<unknown | null>;
|
|
126
|
+
/** Validate + persist an agent spec; throws on a schema violation (→ 400). */
|
|
127
|
+
writeAgent(name: string, spec: Record<string, unknown>): void;
|
|
128
|
+
removeAgent(name: string): boolean;
|
|
129
|
+
/** List skills installed in the daemon's global catalog (with install provenance). */
|
|
130
|
+
listSkills(): Promise<SkillSummary[]>;
|
|
131
|
+
/** One skill's metadata + its file tree, or null if not in the catalog. */
|
|
132
|
+
getSkill(name: string): Promise<SkillDetail | null>;
|
|
133
|
+
/** Read one text file inside a catalog skill (path-confined, size-capped). */
|
|
134
|
+
readSkillFile(name: string, path: string): Promise<{
|
|
135
|
+
path: string;
|
|
136
|
+
content: string;
|
|
137
|
+
}>;
|
|
138
|
+
/** Install per a full `SkillInstallRecipe` (the same object recorded in the
|
|
139
|
+
* skill's receipt); returns the new catalog. */
|
|
140
|
+
installSkill(recipe: SkillInstallRecipe): Promise<{
|
|
141
|
+
name: string;
|
|
142
|
+
description: string;
|
|
143
|
+
}[]>;
|
|
144
|
+
/** Remove a catalog skill by name. */
|
|
145
|
+
removeSkill(name: string): Promise<boolean>;
|
|
146
|
+
tailLogs(instanceId: string | undefined, lines: number): Promise<string>;
|
|
147
|
+
emulator?: ControlEmulatorDeps;
|
|
148
|
+
listSessionMetas(): StoredSessionMeta[];
|
|
149
|
+
getSessionMeta(id: string): StoredSessionMeta | undefined;
|
|
150
|
+
createSessionMeta(meta: StoredSessionMeta): void;
|
|
151
|
+
setSessionTitle(id: string, title: string): void;
|
|
152
|
+
removeSessionMeta(id: string): void;
|
|
153
|
+
}
|
|
154
|
+
/** Persisted meta for an agent session — the unified machine-session record.
|
|
155
|
+
* Runtime `status` is held separately by the server (idle after a restart);
|
|
156
|
+
* transcripts persist via {@link SessionLogStore}. */
|
|
157
|
+
export type StoredSessionMeta = MachineSessionRecord;
|
|
158
|
+
export declare function createControlRouter(opts: {
|
|
159
|
+
manager: ChannelManager;
|
|
160
|
+
deps: ControlPlaneDeps;
|
|
161
|
+
/** Present ⇒ the control plane can run agent sessions (see `/api/sessions`). */
|
|
162
|
+
runtime?: ConversationRuntime;
|
|
163
|
+
sessionBus?: SessionBus;
|
|
164
|
+
sessionLog?: SessionLogStore;
|
|
165
|
+
/** Used to stop a session's runner on close, resolve interactive approvals,
|
|
166
|
+
* and (codex-native live mode) bring up + inject into a session's forwarder. */
|
|
167
|
+
runnerManager?: {
|
|
168
|
+
stopRunner(localThreadId: string): void;
|
|
169
|
+
resolveApproval?(localThreadId: string, approvalId: string, decision: "acceptForSession" | "accept" | "decline" | "cancel"): void;
|
|
170
|
+
ensureLiveSession?(localThreadId: string, opts?: {
|
|
171
|
+
cwd?: string;
|
|
172
|
+
cols?: number;
|
|
173
|
+
rows?: number;
|
|
174
|
+
runtime?: AgentRuntimeId;
|
|
175
|
+
agentName?: string;
|
|
176
|
+
agentSpec?: AgentSpec;
|
|
177
|
+
}): Promise<boolean>;
|
|
178
|
+
injectMessage?(localThreadId: string, text: string): Promise<InjectOutcome>;
|
|
179
|
+
interruptLiveSession?(localThreadId: string): Promise<boolean>;
|
|
180
|
+
};
|
|
181
|
+
/** Global codex/claude session store. Lets the control plane resume a
|
|
182
|
+
* non-console session on its original runtime/model/cwd binding. */
|
|
183
|
+
sessionStore?: CodexSessionStore;
|
|
184
|
+
}): Router;
|
|
185
|
+
/** Derive a one-line session title from the first user message: collapse
|
|
186
|
+
* whitespace, trim, and truncate to `limit` chars + an ellipsis (omnigent's
|
|
187
|
+
* rule — no LLM call). */
|
|
188
|
+
export declare function synthesizeSessionTitle(message: string, limit?: number): string;
|