orbitx 0.0.1 → 0.0.3

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.
@@ -0,0 +1,297 @@
1
+ import type { AIProvider, Message, StreamCallback } from "./ai-provider";
2
+ import type MCPClient from "./mcp-client";
3
+ import type MCPTool from "./mcp";
4
+ import type Skill from "./skill";
5
+
6
+ export interface ParsedToolCall {
7
+ id: string;
8
+ tool: string;
9
+ inputs: Record<string, any>;
10
+ }
11
+
12
+ export type ExtractedSegment =
13
+ | { type: "text"; context: string }
14
+ | { type: "tool"; context: ParsedToolCall };
15
+
16
+ export class BaseAgent {
17
+ #maxMemorizeToken = 16000;
18
+ #instruction: string = "";
19
+ #allowedTools: string[] = [];
20
+
21
+ #skills: Skill[] = [];
22
+
23
+ #memory: string = "";
24
+
25
+ #messagesFull: Message[] = [];
26
+
27
+ #fullInputMissTokens = 0;
28
+ #fullInputHitTokens = 0;
29
+ #fullOutputTokens = 0;
30
+
31
+ #messagesCompact: Message[] = [];
32
+ #currentInputMissTokens = 0;
33
+ #currentInputHitTokens = 0;
34
+ #currentLastOutputTokens = 0;
35
+ #currentOutputTokens = 0;
36
+
37
+ #mcpClient: MCPClient;
38
+ #aiProvider: AIProvider;
39
+
40
+ constructor({ instruction, allowedTools, aiProvider, mcpClient, initData = {
41
+ memory: "",
42
+ messagesFull: [],
43
+ fullInputMissTokens: 0,
44
+ fullInputHitTokens: 0,
45
+ fullOutputTokens: 0,
46
+ messagesCompact: [],
47
+ currentInputMissTokens: 0,
48
+ currentInputHitTokens: 0,
49
+ currentOutputTokens: 0,
50
+ }, skills = [], maxMemorizeToken = 16000 }: {
51
+ instruction: string;
52
+ allowedTools: MCPTool[];
53
+ aiProvider: AIProvider;
54
+ mcpClient: MCPClient;
55
+ skills?: Skill[];
56
+ maxMemorizeToken?: number;
57
+ initData?: {
58
+ memory: string;
59
+ messagesFull: Message[];
60
+ fullInputMissTokens: number;
61
+ fullInputHitTokens: number;
62
+ fullOutputTokens: number;
63
+ messagesCompact: Message[];
64
+ currentInputMissTokens: number;
65
+ currentInputHitTokens: number;
66
+ currentOutputTokens: number;
67
+ }
68
+ }) {
69
+ this.#instruction = instruction;
70
+ this.#allowedTools = allowedTools.map(t => t.getOptions().name);
71
+ this.#aiProvider = aiProvider;
72
+ this.#mcpClient = mcpClient;
73
+ this.#memory = initData.memory;
74
+ this.#messagesFull = initData.messagesFull;
75
+ this.#fullInputMissTokens = initData.fullInputMissTokens
76
+ this.#fullInputHitTokens = initData.fullInputHitTokens
77
+ this.#fullOutputTokens = initData.fullOutputTokens
78
+ this.#messagesCompact = initData.messagesCompact;
79
+ this.#currentInputMissTokens = initData.currentInputMissTokens
80
+ this.#currentInputHitTokens = initData.currentInputHitTokens
81
+ this.#currentOutputTokens = initData.currentOutputTokens
82
+ this.#maxMemorizeToken = maxMemorizeToken;
83
+ this.#skills = skills;
84
+ }
85
+
86
+ #extractJson(str: string): ExtractedSegment[] {
87
+ const segments: ExtractedSegment[] = [];
88
+ let cursor = 0;
89
+ let textStart = 0;
90
+
91
+ for (let i = 0; i < str.length; i++) {
92
+ if (str[i] !== "{") continue;
93
+
94
+ let brace = 0;
95
+ for (let j = i; j < str.length; j++) {
96
+ if (str[j] === "{") brace++;
97
+ if (str[j] === "}") brace--;
98
+
99
+ if (brace === 0) {
100
+ const candidate = str.slice(i, j + 1);
101
+ try {
102
+ const result = JSON.parse(candidate);
103
+ if (
104
+ result &&
105
+ typeof result === "object" &&
106
+ "tool" in result &&
107
+ "inputs" in result &&
108
+ typeof result.tool === "string"
109
+ ) {
110
+ const text = str.slice(textStart, i);
111
+ if (text.trim().length > 0) {
112
+ segments.push({ type: "text", context: text });
113
+ }
114
+ segments.push({ type: "tool", context: result as ParsedToolCall });
115
+
116
+ i = j;
117
+ textStart = j + 1;
118
+ cursor = j + 1;
119
+ }
120
+ } catch {
121
+ // not valid JSON, skip
122
+ }
123
+ break;
124
+ }
125
+ }
126
+ }
127
+
128
+ const remaining = str.slice(textStart);
129
+ if (remaining.trim().length > 0) {
130
+ segments.push({ type: "text", context: remaining });
131
+ }
132
+
133
+ return segments;
134
+ }
135
+
136
+ async #buildSystemPrompt(): Promise<string> {
137
+ const tools = (await this.#mcpClient.getTools())
138
+ .filter(t => this.#allowedTools.includes(t.name));
139
+
140
+ const toolBlock = tools.map(t => {
141
+ const params = t.inputs.map(ti =>
142
+ `${ti.name}${ti.required ? "" : "?"}:${ti.type}${ti.default !== undefined ? `=${JSON.stringify(ti.default)}` : ""} // ${ti.description}`
143
+ ).join(", ");
144
+
145
+ const example: Record<string, unknown> = {};
146
+ t.inputs.forEach(ti => {
147
+ example[ti.name] = ti.default !== undefined ? ti.default
148
+ : ti.type === "string" ? "..."
149
+ : ti.type === "number" ? 0
150
+ : ti.type === "boolean" ? true
151
+ : ti.type === "array" ? []
152
+ : {};
153
+ });
154
+
155
+ return `${t.name}(${params || ""})\n${t.description}\nex: {"tool":"${t.name}","inputs":${JSON.stringify(example)}}`;
156
+ }).join("\n\n");
157
+
158
+ return `You are an assistant with tools. CRITICAL RULES:
159
+ 1. Think step by step.
160
+ ${tools.length > 0 && `2. EVERY tool call MUST include a UNIQUE 10-digit random ID in the format: {"id":"1234567890", "tool":"<name>","inputs":{...}}
161
+ 3. The ID MUST be exactly 10 digits (numbers only) and MUST be different for EACH tool call.
162
+ 4. Example of VALID tool call: {"id":"7845123698", "tool":"search", "inputs":{"query":"weather"}}
163
+ 5. After each tool result, continue reasoning or call another tool.
164
+ 6. Never invent tool results.`}
165
+ ${tools.length > 0 ? "7" : "2"}. If any rules conflict with upper ones, align with upper rules.
166
+
167
+ ${tools.length > 0 && `REMEMBER: Every tool call MUST include a random 10-digit ID!
168
+
169
+ TOOLS:
170
+ ${toolBlock}
171
+ `}
172
+
173
+ ${this.#skills &&`
174
+ SKILLS:
175
+ ${this.#skills.map(skill => (`${skill.getSkill().name} - ${skill.getSkill().description}
176
+ ${skill.getSkill().instructions}
177
+ \n`))}
178
+ `}
179
+
180
+ MEMORY:
181
+ ${this.#memory || "(empty)"}
182
+
183
+ INSTRUCTIONS:
184
+ ${this.#instruction}
185
+ `;
186
+ }
187
+
188
+ #buildMemoryPrompt(): string {
189
+ return `Summarize all important facts and pending tasks from this conversation.
190
+ Output only this JSON line, nothing else:
191
+ {"tool":"set_memory","inputs":{"new_memory":"<compact summary>"}}`;
192
+ }
193
+
194
+ async #extractToolCalls(content: string): Promise<ParsedToolCall[]> {
195
+ return await this.#extractJson(content)
196
+ .filter((r: { type: string }) => r.type === "tool")
197
+ .map((t) => t.context as ParsedToolCall);
198
+ }
199
+
200
+ async #dispatchTool(toolCall: ParsedToolCall): Promise<string> {
201
+ if (toolCall.tool === "set_memory") {
202
+ if (typeof toolCall.inputs?.new_memory === "string") {
203
+ this.#memory = toolCall.inputs.new_memory;
204
+ return "Memory updated.";
205
+ }
206
+ return `Error: set_memory requires new_memory:string, got ${JSON.stringify(toolCall.inputs)}`;
207
+ }
208
+
209
+ if (this.#allowedTools.includes(toolCall.tool)) {
210
+ return JSON.stringify({ id: toolCall.id, output: await this.#mcpClient.callTool(toolCall.tool, toolCall.inputs) });
211
+ }
212
+
213
+ return `Error: tool "${toolCall.tool}" not found or not allowed.`;
214
+ }
215
+
216
+ async run(prompt: string, streamCallback?: StreamCallback): Promise<void> {
217
+ let toolsCalls: ParsedToolCall[] = [];
218
+
219
+ do {
220
+ const systemPrompt = await this.#buildSystemPrompt();
221
+
222
+ const chat = await this.#aiProvider.chat([
223
+ { role: "system", content: systemPrompt },
224
+ ...this.#messagesCompact,
225
+ { role: "user", content: prompt }
226
+ ], streamCallback);
227
+
228
+ this.#messagesFull.push({ role: "user", content: prompt });
229
+ this.#messagesCompact.push({ role: "user", content: prompt });
230
+ this.#messagesFull.push({ role: "assistant", content: chat.content });
231
+ this.#messagesCompact.push({ role: "assistant", content: chat.content });
232
+
233
+ this.#currentInputHitTokens += this.#currentInputMissTokens;
234
+ this.#currentInputMissTokens += chat.inputTokens - this.#currentInputMissTokens;
235
+ this.#currentOutputTokens += chat.outputTokens;
236
+ this.#currentLastOutputTokens = chat.outputTokens;
237
+
238
+ toolsCalls = await this.#extractToolCalls(chat.content);
239
+
240
+ for (const toolCall of toolsCalls) {
241
+ const toolResponse = await this.#dispatchTool(toolCall);
242
+ this.#messagesFull.push({ role: "system", content: toolResponse });
243
+ this.#messagesCompact.push({ role: "system", content: toolResponse });
244
+ streamCallback?.({ role: "tool", content: toolResponse, done: true });
245
+ }
246
+
247
+ if (this.#currentInputMissTokens + this.#currentLastOutputTokens > this.#maxMemorizeToken) {
248
+ const memChat = await this.#aiProvider.chat([
249
+ { role: "system", content: systemPrompt },
250
+ ...this.#messagesCompact,
251
+ { role: "user", content: this.#buildMemoryPrompt() }
252
+ ]);
253
+
254
+ this.#currentInputHitTokens += this.#currentInputMissTokens;
255
+ this.#currentInputMissTokens += memChat.inputTokens - this.#currentInputMissTokens;
256
+ this.#currentOutputTokens += memChat.outputTokens;
257
+
258
+ for (const mc of await this.#extractToolCalls(memChat.content)) {
259
+ const toolResponse = await this.#dispatchTool(mc);
260
+ streamCallback?.({ role: "tool", content: toolResponse, done: true });
261
+ }
262
+
263
+ this.#messagesCompact = [];
264
+ this.#fullInputHitTokens += this.#currentInputHitTokens;
265
+ this.#fullInputMissTokens += this.#currentInputMissTokens;
266
+ this.#fullOutputTokens += this.#currentOutputTokens;
267
+ this.#currentInputHitTokens = 0;
268
+ this.#currentInputMissTokens = 0;
269
+ this.#currentOutputTokens = 0;
270
+ this.#currentLastOutputTokens = 0;
271
+ }
272
+
273
+ prompt = "";
274
+
275
+ } while (toolsCalls.length > 0);
276
+ }
277
+
278
+ getCurrentTotalTokens() {
279
+ return {
280
+ total: this.#currentInputHitTokens + this.#currentInputMissTokens + this.#currentOutputTokens,
281
+ inputHit: this.#currentInputHitTokens,
282
+ inputMiss: this.#currentInputMissTokens,
283
+ output: this.#currentOutputTokens
284
+ };
285
+ }
286
+
287
+ getFullTotalTokens() {
288
+ return {
289
+ total: this.#fullInputHitTokens + this.#fullInputMissTokens + this.#fullOutputTokens,
290
+ inputHit: this.#fullInputHitTokens,
291
+ inputMiss: this.#fullInputMissTokens,
292
+ output: this.#fullOutputTokens
293
+ };
294
+ }
295
+ }
296
+
297
+ export default BaseAgent;
@@ -0,0 +1,71 @@
1
+ import OpenAI from "openai";
2
+ import AIProvider, { type Message, type ChatResponse, type StreamCallback } from "./ai-provider";
3
+
4
+ export class DeepSeekProvider extends AIProvider {
5
+ private client: OpenAI;
6
+ private model: string;
7
+
8
+ constructor(apiKey: string, model: string = "deepseek-v4-flash") {
9
+ super();
10
+ this.client = new OpenAI({
11
+ apiKey: apiKey,
12
+ baseURL: "https://api.deepseek.com"
13
+ });
14
+ this.model = model;
15
+ }
16
+
17
+ async chat(
18
+ messages: Message[],
19
+ streamCallback?: StreamCallback
20
+ ): Promise<ChatResponse> {
21
+ const formattedMessages = messages.map(msg => ({
22
+ role: msg.role,
23
+ content: msg.content || ""
24
+ }));
25
+
26
+ if (streamCallback) {
27
+ const stream = await this.client.chat.completions.create({
28
+ model: this.model,
29
+ messages: formattedMessages as any,
30
+ stream: true
31
+ });
32
+
33
+ let fullContent = "";
34
+ let inputTokens = 0;
35
+ let outputTokens = 0;
36
+
37
+ for await (const chunk of stream) {
38
+ const content = chunk.choices[0]?.delta?.content || "";
39
+ if (content) {
40
+ fullContent += content;
41
+ streamCallback({ role: "assistant", content, done: false });
42
+ }
43
+
44
+ if (chunk.usage) {
45
+ inputTokens = chunk.usage.prompt_tokens || 0;
46
+ outputTokens = chunk.usage.completion_tokens || 0;
47
+ }
48
+ }
49
+
50
+ streamCallback({ role: "assistant", content: "", done: true });
51
+ return {
52
+ content: fullContent,
53
+ inputTokens,
54
+ outputTokens,
55
+ };
56
+ } else {
57
+ const response = await this.client.chat.completions.create({
58
+ model: this.model,
59
+ messages: formattedMessages as any
60
+ });
61
+
62
+ return {
63
+ content: response.choices[0]?.message?.content || "",
64
+ inputTokens: response.usage?.prompt_tokens || 0,
65
+ outputTokens: response.usage?.completion_tokens || 0,
66
+ };
67
+ }
68
+ }
69
+ }
70
+
71
+ export default DeepSeekProvider;
@@ -0,0 +1,113 @@
1
+ import type MCPTool from "./mcp";
2
+ import type MCPConnection from "./mcp-connection";
3
+ import { randomUUID } from "crypto";
4
+
5
+ export class MCPClient {
6
+ #connections;
7
+ #tools: MCPTool[] = [];
8
+ #envID;
9
+ constructor(envID: string, connection: MCPConnection | MCPConnection[]) {
10
+ this.#connections = Array.isArray(connection) ? connection : [connection];
11
+ this.#envID = envID;
12
+ }
13
+
14
+ async getTools() {
15
+ const connectionPromises = this.#connections.map(conn => {
16
+ return new Promise<{
17
+ name: string;
18
+ description: string;
19
+ inputs: {
20
+ name: string;
21
+ type: "number" | "string" | "boolean" | "object" | "array";
22
+ description: string;
23
+ required?: boolean;
24
+ default?: any;
25
+ }[];
26
+ }[]>((resolve) => {
27
+ const pid = randomUUID();
28
+ let resolved = false;
29
+
30
+ const onRead = (data: any) => {
31
+ if (data.topic === "getToolsCallback" && data.pid === pid) {
32
+ resolved = true;
33
+ conn.off("read", onRead);
34
+ resolve(data.tools || []);
35
+ }
36
+ };
37
+
38
+ conn.on("read", onRead);
39
+ conn.emit("write", {
40
+ pid,
41
+ topic: "getTools"
42
+ });
43
+
44
+ setTimeout(() => {
45
+ if (!resolved) {
46
+ conn.off("read", onRead);
47
+ resolve([]);
48
+ }
49
+ }, 2000);
50
+ });
51
+ });
52
+
53
+ const results = await Promise.all(connectionPromises);
54
+
55
+ const connectionsTools = results.flat();
56
+ return [...this.#tools.map(t => ({
57
+ name: t.getOptions().name,
58
+ description: t.getOptions().description,
59
+ inputs: t.getOptions().inputs
60
+ })), ...connectionsTools];
61
+ }
62
+
63
+ async callTool(toolName: string, inputs: Record<string, any>) {
64
+ const clientTool = this.#tools.find(t => t.getOptions().name == toolName);
65
+ if (clientTool) {
66
+ return JSON.stringify(await clientTool.getOptions().execute(this.#envID, inputs));
67
+ } else {
68
+ return JSON.stringify(await new Promise<any>((resolve) => {
69
+ let resolvedResult = false;
70
+ this.#connections.map(conn => {
71
+ const pid = randomUUID();
72
+ let resolved = false;
73
+
74
+ const onRead = (data: any) => {
75
+ if (data.topic === "toolCallCallback" && data.pid === pid) {
76
+ resolved = true;
77
+ conn.off("read", onRead);
78
+ if (!resolvedResult) {
79
+ resolve(data.output || {});
80
+ resolvedResult = true;
81
+ }
82
+ }
83
+ };
84
+
85
+ conn.on("read", onRead);
86
+ conn.emit("write", {
87
+ pid,
88
+ topic: "toolCall",
89
+ tool: toolName,
90
+ envID: this.#envID,
91
+ inputs
92
+ });
93
+
94
+ setTimeout(() => {
95
+ if (!resolved) {
96
+ conn.off("read", onRead);
97
+ }
98
+ if (!resolvedResult) {
99
+ resolve({ error: "The tool call exceeded the 300second timeout limit." });
100
+ resolvedResult = true;
101
+ }
102
+ }, 300000);
103
+ });
104
+ }));
105
+ }
106
+ }
107
+
108
+ registerTool(tool: MCPTool) {
109
+ this.#tools.push(tool);
110
+ }
111
+ }
112
+
113
+ export default MCPClient;
@@ -0,0 +1,5 @@
1
+ export default class MCPComputer {
2
+ constructor() {
3
+
4
+ }
5
+ }
@@ -0,0 +1,12 @@
1
+ import EventEmitter from "events";
2
+
3
+ export class MCPConnection extends EventEmitter {
4
+ constructor(overrided: boolean = false) {
5
+ super();
6
+
7
+ if (!overrided)
8
+ this.on("write", (data) => this.emit("read", data))
9
+ }
10
+ }
11
+
12
+ export default MCPConnection;
@@ -0,0 +1,153 @@
1
+ import * as net from "net";
2
+ import * as fs from "fs";
3
+ import MCPConnection from "./mcp-connection";
4
+
5
+ type Role = "server" | "client" | "pending";
6
+
7
+ export class MCPIPCConnection extends MCPConnection {
8
+ private readonly socketPath: string;
9
+ private socket: net.Socket | null = null;
10
+ private server: net.Server | null = null;
11
+ private role: Role = "pending";
12
+ private writeBuffer: Buffer[] = [];
13
+ private readBuffer = "";
14
+
15
+ constructor(socketPath: string) {
16
+ super(true);
17
+ this.socketPath = socketPath;
18
+
19
+ this.on("write", (data: unknown) => {
20
+ this._sendJSON(data);
21
+ });
22
+
23
+ setTimeout(() => {
24
+ this._tryConnect();
25
+ }, Math.random() * 200); // Pervent Race Condtion
26
+ }
27
+
28
+ close(): void {
29
+ this.socket?.destroy();
30
+ this.server?.close();
31
+ this._cleanSocketFile();
32
+ }
33
+
34
+ private _tryConnect(): void {
35
+ const sock = net.createConnection({ path: this.socketPath });
36
+
37
+ sock.once("connect", () => {
38
+ this.role = "client";
39
+ this.emit("role", "client");
40
+ this._attachSocket(sock);
41
+ });
42
+
43
+ sock.once("error", (err: NodeJS.ErrnoException) => {
44
+ if (err.code === "ENOENT" || err.code === "ECONNREFUSED") {
45
+ sock.destroy();
46
+ this._becomeServer();
47
+ } else {
48
+ this.emit("error", err);
49
+ }
50
+ });
51
+ }
52
+
53
+ private _becomeServer(): void {
54
+ this._cleanSocketFile();
55
+
56
+ const srv = net.createServer((peer) => {
57
+ this.role = "server";
58
+ this.emit("role", "server");
59
+
60
+ srv.close();
61
+
62
+ this._attachSocket(peer);
63
+ });
64
+
65
+ srv.on("error", (err) => this.emit("error", err));
66
+
67
+ srv.listen({ path: this.socketPath }, () => {
68
+ this.server = srv;
69
+ this.emit("listening", this.socketPath);
70
+ });
71
+ }
72
+
73
+ private _attachSocket(sock: net.Socket): void {
74
+ this.socket = sock;
75
+
76
+ for (const chunk of this.writeBuffer) {
77
+ sock.write(chunk);
78
+ }
79
+ this.writeBuffer = [];
80
+
81
+ sock.setEncoding("utf8");
82
+
83
+ sock.on("data", (chunk: string) => {
84
+ this._handleIncoming(chunk);
85
+ });
86
+
87
+ sock.on("end", () => {
88
+ this.emit("disconnected");
89
+ this._maybeReconnect();
90
+ });
91
+
92
+ sock.on("error", (err: NodeJS.ErrnoException) => {
93
+ this.emit("error", err);
94
+ });
95
+
96
+ this.emit("connected", this.role);
97
+ }
98
+
99
+ private _sendJSON(data: unknown): void {
100
+ let line: string;
101
+ try {
102
+ line = JSON.stringify(data) + "\n";
103
+ } catch (err) {
104
+ this.emit("error", new Error(`MCPIPCConnection: cannot serialise data – ${err}`));
105
+ return;
106
+ }
107
+
108
+ const chunk = Buffer.from(line, "utf8");
109
+
110
+ if (this.socket && !this.socket.destroyed) {
111
+ this.socket.write(chunk);
112
+ } else {
113
+ this.writeBuffer.push(chunk);
114
+ }
115
+ }
116
+
117
+ private _handleIncoming(chunk: string): void {
118
+ this.readBuffer += chunk;
119
+
120
+ const lines = this.readBuffer.split("\n");
121
+
122
+ this.readBuffer = lines.pop() ?? "";
123
+
124
+ for (const line of lines) {
125
+ const trimmed = line.trim();
126
+ if (!trimmed) continue;
127
+
128
+ try {
129
+ const parsed = JSON.parse(trimmed);
130
+ this.emit("read", parsed);
131
+ } catch {
132
+ this.emit("error", new Error(`MCPIPCConnection: malformed JSON – ${trimmed}`));
133
+ }
134
+ }
135
+ }
136
+
137
+ private _cleanSocketFile(): void {
138
+ try {
139
+ fs.unlinkSync(this.socketPath);
140
+ } catch { }
141
+ }
142
+
143
+ private _maybeReconnect(): void {
144
+ if (this.role === "client") {
145
+ setTimeout(() => {
146
+ this.role = "pending";
147
+ this._tryConnect();
148
+ }, 500);
149
+ }
150
+ }
151
+ }
152
+
153
+ export default MCPIPCConnection;