@koi-language/koi 1.0.5 → 1.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/README.md +4 -125
- package/examples/.build/agent-dialogue.ts +138 -0
- package/examples/.build/agent-dialogue.ts.map +1 -0
- package/examples/.build/chess.ts +77 -0
- package/examples/.build/chess.ts.map +1 -0
- package/examples/.build/delegation-test.ts +140 -0
- package/examples/.build/delegation-test.ts.map +1 -0
- package/examples/.build/dialog-demo.ts +77 -0
- package/examples/.build/dialog-demo.ts.map +1 -0
- package/examples/.build/hello-world.ts +77 -0
- package/examples/.build/hello-world.ts.map +1 -0
- package/examples/.build/lover-dialog-demo.ts +77 -0
- package/examples/.build/lover-dialog-demo.ts.map +1 -0
- package/examples/.build/package.json +3 -0
- package/examples/.build/registry-interactive-demo.ts +202 -0
- package/examples/.build/registry-interactive-demo.ts.map +1 -0
- package/examples/.build/registry-playbook-demo.ts +201 -0
- package/examples/.build/registry-playbook-demo.ts.map +1 -0
- package/examples/.build/tic-tac-toe.ts +77 -0
- package/examples/.build/tic-tac-toe.ts.map +1 -0
- package/examples/actions-demo.koi +8 -9
- package/examples/activists-dialogue.koi +75 -0
- package/examples/agent-dialogue.koi +66 -0
- package/examples/chess.koi +19 -0
- package/examples/counter.koi +20 -69
- package/examples/delegation-test.koi +16 -18
- package/examples/dialog-demo.koi +20 -0
- package/examples/hello-world.koi +7 -43
- package/examples/mcp-stdio-demo.koi +29 -0
- package/examples/memory-test.koi +49 -0
- package/examples/mobile-mcp-demo.koi +32 -0
- package/examples/multi-event-handler-test.koi +16 -18
- package/examples/pipeline.koi +15 -17
- package/examples/prompt-demo.koi +20 -0
- package/examples/{registry-playbook-email-compositor.koi → registry-interactive-demo.koi} +27 -27
- package/examples/registry-playbook-demo.koi +28 -28
- package/examples/skill-import-test.koi +7 -9
- package/examples/skills/.build/math-operations.ts +1656 -0
- package/examples/skills/.build/math-operations.ts.map +1 -0
- package/examples/skills/.build/package.json +3 -0
- package/examples/skills/.build/string-operations.ts +1643 -0
- package/examples/skills/.build/string-operations.ts.map +1 -0
- package/examples/skills/advanced/.build/index.ts +3223 -0
- package/examples/skills/advanced/.build/index.ts.map +1 -0
- package/examples/skills/advanced/.build/package.json +3 -0
- package/examples/skills/advanced/index.koi +3 -5
- package/examples/skills/math-operations.koi +1 -3
- package/examples/skills/string-operations.koi +1 -3
- package/examples/tic-tac-toe.koi +19 -0
- package/examples/utils/echo-mcp-server.js +141 -0
- package/examples/web-delegation-demo.koi +15 -17
- package/package.json +2 -1
- package/src/cli/koi.js +30 -41
- package/src/compiler/build-optimizer.js +204 -289
- package/src/compiler/cache-manager.js +1 -1
- package/src/compiler/import-resolver.js +5 -9
- package/src/compiler/parser.js +6072 -3476
- package/src/compiler/transpiler.js +346 -38
- package/src/grammar/koi.pegjs +302 -62
- package/src/runtime/actions/{format.js → call-llm.js} +37 -44
- package/src/runtime/actions/call-mcp.js +97 -0
- package/src/runtime/actions/if.js +179 -0
- package/src/runtime/actions/print.js +3 -1
- package/src/runtime/actions/prompt-user.js +75 -0
- package/src/runtime/actions/repeat.js +147 -0
- package/src/runtime/actions/shell.js +185 -0
- package/src/runtime/actions/while.js +205 -0
- package/src/runtime/agent.js +592 -178
- package/src/runtime/cli-display.js +26 -0
- package/src/runtime/cli-input.js +421 -0
- package/src/runtime/cli-logger.js +2 -5
- package/src/runtime/cli-markdown.js +61 -0
- package/src/runtime/cli-select.js +106 -0
- package/src/runtime/incremental-json-parser.js +51 -17
- package/src/runtime/index.js +1 -0
- package/src/runtime/llm-provider.js +1083 -572
- package/src/runtime/mcp-registry.js +141 -0
- package/src/runtime/mcp-stdio-client.js +334 -0
- package/src/runtime/planner.js +1 -1
- package/src/runtime/playbook-session.js +259 -0
- package/src/runtime/registry-backends/keyv-sqlite.js +1 -1
- package/src/runtime/registry-backends/local.js +1 -1
- package/src/runtime/router.js +22 -26
- package/src/runtime/runtime.js +7 -1
- package/examples/cache-test.koi +0 -29
- package/examples/calculator.koi +0 -61
- package/examples/clear-registry.js +0 -33
- package/examples/clear-registry.koi +0 -30
- package/examples/code-introspection-test.koi +0 -149
- package/examples/directory-import-test.koi +0 -84
- package/examples/hello-world-claude.koi +0 -52
- package/examples/hello.koi +0 -24
- package/examples/mcp-example.koi +0 -70
- package/examples/new-import-test.koi +0 -89
- package/examples/registry-demo.koi +0 -184
- package/examples/registry-playbook-email-compositor-2.koi +0 -140
- package/examples/sentiment.koi +0 -90
- package/examples/simple.koi +0 -48
- package/examples/task-chaining-demo.koi +0 -244
- package/examples/test-await.koi +0 -22
- package/examples/test-crypto-sha256.koi +0 -196
- package/examples/test-delegation.koi +0 -41
- package/examples/test-multi-team-routing.koi +0 -258
- package/examples/test-no-handler.koi +0 -35
- package/examples/test-npm-import.koi +0 -67
- package/examples/test-parse.koi +0 -10
- package/examples/test-peers-with-team.koi +0 -59
- package/examples/test-permissions-fail.koi +0 -20
- package/examples/test-permissions.koi +0 -36
- package/examples/test-simple-registry.koi +0 -31
- package/examples/test-typescript-import.koi +0 -64
- package/examples/test-uses-team-syntax.koi +0 -25
- package/examples/test-uses-team.koi +0 -31
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { MCPStdioClient } from './mcp-stdio-client.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MCP Registry - Global registry of MCP client instances.
|
|
5
|
+
* Similar to SkillRegistry but for MCP stdio servers.
|
|
6
|
+
*/
|
|
7
|
+
class MCPRegistry {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.clients = new Map();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Register an MCP server configuration.
|
|
14
|
+
* Does NOT connect immediately (lazy connection on first use).
|
|
15
|
+
* @param {string} name - MCP server name (e.g., "mobileMCP")
|
|
16
|
+
* @param {object} config - { command, args, env }
|
|
17
|
+
*/
|
|
18
|
+
register(name, config) {
|
|
19
|
+
if (this.clients.has(name)) {
|
|
20
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
21
|
+
console.error(`[MCPRegistry] Re-registering MCP: ${name}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const client = new MCPStdioClient(name, config);
|
|
26
|
+
this.clients.set(name, client);
|
|
27
|
+
|
|
28
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
29
|
+
console.error(`[MCPRegistry] Registered MCP: ${name} (${config.command} ${(config.args || []).join(' ')})`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get a client by name.
|
|
35
|
+
* @param {string} name - MCP server name
|
|
36
|
+
* @returns {MCPStdioClient|undefined}
|
|
37
|
+
*/
|
|
38
|
+
get(name) {
|
|
39
|
+
return this.clients.get(name);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Connect a specific MCP client (lazy initialization).
|
|
44
|
+
* @param {string} name - MCP server name
|
|
45
|
+
*/
|
|
46
|
+
async connect(name) {
|
|
47
|
+
const client = this.clients.get(name);
|
|
48
|
+
if (!client) {
|
|
49
|
+
throw new Error(`MCP '${name}' not registered`);
|
|
50
|
+
}
|
|
51
|
+
if (!client.initialized) {
|
|
52
|
+
await client.connect();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Connect all registered MCP clients.
|
|
58
|
+
*/
|
|
59
|
+
async connectAll() {
|
|
60
|
+
const promises = [];
|
|
61
|
+
for (const [name, client] of this.clients) {
|
|
62
|
+
if (!client.initialized) {
|
|
63
|
+
promises.push(client.connect().catch(err => {
|
|
64
|
+
console.error(`[MCPRegistry] Failed to connect ${name}: ${err.message}`);
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
await Promise.all(promises);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Call a tool on a specific MCP server.
|
|
73
|
+
* Connects lazily if not already connected.
|
|
74
|
+
* @param {string} mcpName - MCP server name
|
|
75
|
+
* @param {string} toolName - Tool name
|
|
76
|
+
* @param {object} args - Tool arguments
|
|
77
|
+
* @returns {object} Tool result
|
|
78
|
+
*/
|
|
79
|
+
async callTool(mcpName, toolName, args = {}) {
|
|
80
|
+
const client = this.clients.get(mcpName);
|
|
81
|
+
if (!client) {
|
|
82
|
+
throw new Error(`MCP '${mcpName}' not registered`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!client.initialized) {
|
|
86
|
+
await client.connect();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return await client.callTool(toolName, args);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get tool summaries for all registered MCPs.
|
|
94
|
+
* Used for building system prompts.
|
|
95
|
+
* @returns {Array<{name: string, tools: Array}>}
|
|
96
|
+
*/
|
|
97
|
+
getToolSummaries() {
|
|
98
|
+
const summaries = [];
|
|
99
|
+
for (const [name, client] of this.clients) {
|
|
100
|
+
if (client.tools.length > 0) {
|
|
101
|
+
summaries.push({
|
|
102
|
+
name,
|
|
103
|
+
tools: client.tools.map(t => ({
|
|
104
|
+
name: t.name,
|
|
105
|
+
description: t.description || '',
|
|
106
|
+
inputSchema: t.inputSchema
|
|
107
|
+
}))
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return summaries;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Disconnect all MCP clients gracefully.
|
|
116
|
+
*/
|
|
117
|
+
async disconnectAll() {
|
|
118
|
+
const promises = [];
|
|
119
|
+
for (const [name, client] of this.clients) {
|
|
120
|
+
if (client.initialized) {
|
|
121
|
+
promises.push(client.disconnect().catch(err => {
|
|
122
|
+
console.error(`[MCPRegistry] Failed to disconnect ${name}: ${err.message}`);
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
await Promise.all(promises);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Check if any MCP servers are registered.
|
|
131
|
+
*/
|
|
132
|
+
hasRegistered() {
|
|
133
|
+
return this.clients.size > 0;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Singleton instance
|
|
138
|
+
export const mcpRegistry = new MCPRegistry();
|
|
139
|
+
|
|
140
|
+
// Make available globally for transpiled code
|
|
141
|
+
globalThis.mcpRegistry = mcpRegistry;
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MCP Stdio Client - Manages a single MCP server subprocess
|
|
5
|
+
* and communicates via JSON-RPC 2.0 over stdin/stdout.
|
|
6
|
+
*/
|
|
7
|
+
export class MCPStdioClient {
|
|
8
|
+
constructor(name, config) {
|
|
9
|
+
this.name = name;
|
|
10
|
+
this.command = config.command;
|
|
11
|
+
this.args = config.args || [];
|
|
12
|
+
this.env = config.env || {};
|
|
13
|
+
this.process = null;
|
|
14
|
+
this.tools = [];
|
|
15
|
+
this.initialized = false;
|
|
16
|
+
this._requestId = 0;
|
|
17
|
+
this._pendingRequests = new Map();
|
|
18
|
+
this._buffer = '';
|
|
19
|
+
this._stderrLines = [];
|
|
20
|
+
this.lastError = null; // Human-readable error cause when process crashes
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Spawn subprocess, perform MCP initialize handshake, and cache tools/list.
|
|
25
|
+
*/
|
|
26
|
+
async connect() {
|
|
27
|
+
if (this.initialized) return;
|
|
28
|
+
|
|
29
|
+
// Clean up old state from previous crash before reconnecting
|
|
30
|
+
if (this.process) {
|
|
31
|
+
try { this.process.kill('SIGTERM'); } catch (e) { /* already dead */ }
|
|
32
|
+
this.process = null;
|
|
33
|
+
}
|
|
34
|
+
this._buffer = '';
|
|
35
|
+
this._stderrLines = [];
|
|
36
|
+
this._pendingRequests.clear();
|
|
37
|
+
this.lastError = null;
|
|
38
|
+
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const timeout = setTimeout(() => {
|
|
41
|
+
reject(new Error(`[MCP:${this.name}] Connection timeout after 30s`));
|
|
42
|
+
}, 30000);
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
// Build env: config values override process.env, but skip empty/null values
|
|
46
|
+
// so that env: { "API_KEY": "" } in .koi doesn't override a real env var
|
|
47
|
+
const configEnv = {};
|
|
48
|
+
for (const [key, value] of Object.entries(this.env)) {
|
|
49
|
+
if (value !== '' && value !== null && value !== undefined) {
|
|
50
|
+
configEnv[key] = value;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.process = spawn(this.command, this.args, {
|
|
55
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
56
|
+
env: { ...process.env, ...configEnv }
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
this.process.stdout.on('data', (data) => {
|
|
60
|
+
this._buffer += data.toString();
|
|
61
|
+
this._processBuffer();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
this.process.stderr.on('data', (data) => {
|
|
65
|
+
const lines = data.toString().split('\n').filter(l => l.trim());
|
|
66
|
+
for (const line of lines) {
|
|
67
|
+
this._stderrLines.push(line);
|
|
68
|
+
// Keep only last 20 lines to avoid unbounded growth
|
|
69
|
+
if (this._stderrLines.length > 20) {
|
|
70
|
+
this._stderrLines.shift();
|
|
71
|
+
}
|
|
72
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
73
|
+
console.error(`[MCP:${this.name}] ${line}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
this.process.on('error', (err) => {
|
|
79
|
+
clearTimeout(timeout);
|
|
80
|
+
reject(new Error(`[MCP:${this.name}] Failed to spawn: ${err.message}`));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
this.process.on('close', (code) => {
|
|
84
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
85
|
+
console.error(`[MCP:${this.name}] Process exited with code ${code}`);
|
|
86
|
+
}
|
|
87
|
+
this.initialized = false;
|
|
88
|
+
this.process = null;
|
|
89
|
+
|
|
90
|
+
// Capture full stderr buffer for the LLM to interpret (already capped at 20 lines)
|
|
91
|
+
const stderrOutput = this._stderrLines.join('\n');
|
|
92
|
+
this.lastError = stderrOutput || null;
|
|
93
|
+
|
|
94
|
+
const errorMsg = stderrOutput
|
|
95
|
+
? `MCP server "${this.name}" crashed (exit code ${code}). Server output:\n${stderrOutput}`
|
|
96
|
+
: `MCP server "${this.name}" crashed (exit code ${code}). No output captured.`;
|
|
97
|
+
|
|
98
|
+
// Reject all pending requests (process died)
|
|
99
|
+
for (const [id, pending] of this._pendingRequests) {
|
|
100
|
+
clearTimeout(pending.timeout);
|
|
101
|
+
pending.reject(new Error(errorMsg));
|
|
102
|
+
}
|
|
103
|
+
this._pendingRequests.clear();
|
|
104
|
+
|
|
105
|
+
// If we were still connecting, reject the connect promise
|
|
106
|
+
if (!this.initialized) {
|
|
107
|
+
clearTimeout(timeout);
|
|
108
|
+
reject(new Error(errorMsg));
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Perform initialize handshake
|
|
113
|
+
this._initialize().then(async () => {
|
|
114
|
+
// Send initialized notification
|
|
115
|
+
this._sendNotification('notifications/initialized', {});
|
|
116
|
+
|
|
117
|
+
// Cache tools
|
|
118
|
+
try {
|
|
119
|
+
const toolsResult = await this._sendRequest('tools/list', {});
|
|
120
|
+
this.tools = toolsResult.tools || [];
|
|
121
|
+
this.initialized = true;
|
|
122
|
+
|
|
123
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
124
|
+
console.error(`[MCP:${this.name}] Connected. ${this.tools.length} tools available: ${this.tools.map(t => t.name).join(', ')}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
clearTimeout(timeout);
|
|
128
|
+
resolve();
|
|
129
|
+
} catch (err) {
|
|
130
|
+
clearTimeout(timeout);
|
|
131
|
+
reject(new Error(`[MCP:${this.name}] tools/list failed: ${err.message}`));
|
|
132
|
+
}
|
|
133
|
+
}).catch((err) => {
|
|
134
|
+
clearTimeout(timeout);
|
|
135
|
+
reject(err);
|
|
136
|
+
});
|
|
137
|
+
} catch (err) {
|
|
138
|
+
clearTimeout(timeout);
|
|
139
|
+
reject(new Error(`[MCP:${this.name}] Spawn error: ${err.message}`));
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Send shutdown request and kill subprocess.
|
|
146
|
+
*/
|
|
147
|
+
async disconnect() {
|
|
148
|
+
if (!this.process) return;
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
// Try graceful shutdown
|
|
152
|
+
this._sendNotification('notifications/cancelled', {});
|
|
153
|
+
} catch (e) {
|
|
154
|
+
// Ignore errors during shutdown
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Kill the process
|
|
158
|
+
try {
|
|
159
|
+
this.process.kill('SIGTERM');
|
|
160
|
+
} catch (e) {
|
|
161
|
+
// Already dead
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
this.initialized = false;
|
|
165
|
+
this.process = null;
|
|
166
|
+
this.tools = [];
|
|
167
|
+
this._pendingRequests.clear();
|
|
168
|
+
this._buffer = '';
|
|
169
|
+
this._stderrLines = [];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Return cached tools list.
|
|
174
|
+
*/
|
|
175
|
+
async listTools() {
|
|
176
|
+
if (!this.initialized) {
|
|
177
|
+
await this.connect();
|
|
178
|
+
}
|
|
179
|
+
return this.tools;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Call a tool on the MCP server.
|
|
184
|
+
* @param {string} toolName - Name of the tool
|
|
185
|
+
* @param {object} args - Tool input parameters
|
|
186
|
+
* @returns {object} Tool result
|
|
187
|
+
*/
|
|
188
|
+
async callTool(toolName, args = {}) {
|
|
189
|
+
if (!this.initialized) {
|
|
190
|
+
await this.connect();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const result = await this._sendRequest('tools/call', {
|
|
194
|
+
name: toolName,
|
|
195
|
+
arguments: args
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Extract text content from MCP response format
|
|
199
|
+
if (result.content && Array.isArray(result.content)) {
|
|
200
|
+
const textParts = result.content
|
|
201
|
+
.filter(c => c.type === 'text')
|
|
202
|
+
.map(c => c.text);
|
|
203
|
+
|
|
204
|
+
if (textParts.length === 1) {
|
|
205
|
+
// Try to parse as JSON
|
|
206
|
+
try {
|
|
207
|
+
return JSON.parse(textParts[0]);
|
|
208
|
+
} catch (e) {
|
|
209
|
+
return { result: textParts[0] };
|
|
210
|
+
}
|
|
211
|
+
} else if (textParts.length > 1) {
|
|
212
|
+
return { result: textParts.join('\n') };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ---- Private Protocol Methods ----
|
|
220
|
+
|
|
221
|
+
async _initialize() {
|
|
222
|
+
const result = await this._sendRequest('initialize', {
|
|
223
|
+
protocolVersion: '2024-11-05',
|
|
224
|
+
capabilities: {},
|
|
225
|
+
clientInfo: {
|
|
226
|
+
name: 'koi-mcp-client',
|
|
227
|
+
version: '1.0.0'
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
232
|
+
console.error(`[MCP:${this.name}] Server: ${result.serverInfo?.name || 'unknown'} v${result.serverInfo?.version || '?'}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return result;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
_sendRequest(method, params) {
|
|
239
|
+
return new Promise((resolve, reject) => {
|
|
240
|
+
const id = ++this._requestId;
|
|
241
|
+
// tools/call can take minutes (e.g. mobile automation); handshake methods are fast
|
|
242
|
+
const timeoutMs = method === 'tools/call' ? 5 * 60 * 1000 : 30000;
|
|
243
|
+
const timeoutLabel = method === 'tools/call' ? '5m' : '30s';
|
|
244
|
+
const timeout = setTimeout(() => {
|
|
245
|
+
this._pendingRequests.delete(id);
|
|
246
|
+
reject(new Error(`[MCP:${this.name}] Request ${method} (id=${id}) timed out after ${timeoutLabel}`));
|
|
247
|
+
}, timeoutMs);
|
|
248
|
+
|
|
249
|
+
this._pendingRequests.set(id, { resolve, reject, timeout });
|
|
250
|
+
|
|
251
|
+
const message = JSON.stringify({
|
|
252
|
+
jsonrpc: '2.0',
|
|
253
|
+
id,
|
|
254
|
+
method,
|
|
255
|
+
params
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
259
|
+
console.error(`[MCP:${this.name}] → ${method} (id=${id})`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
this.process.stdin.write(message + '\n');
|
|
264
|
+
} catch (err) {
|
|
265
|
+
clearTimeout(timeout);
|
|
266
|
+
this._pendingRequests.delete(id);
|
|
267
|
+
reject(new Error(`[MCP:${this.name}] Write failed: ${err.message}`));
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
_sendNotification(method, params) {
|
|
273
|
+
if (!this.process || !this.process.stdin.writable) return;
|
|
274
|
+
|
|
275
|
+
const message = JSON.stringify({
|
|
276
|
+
jsonrpc: '2.0',
|
|
277
|
+
method,
|
|
278
|
+
params
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
this.process.stdin.write(message + '\n');
|
|
283
|
+
} catch (e) {
|
|
284
|
+
// Ignore write errors for notifications
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
_processBuffer() {
|
|
289
|
+
// Process newline-delimited JSON-RPC messages
|
|
290
|
+
let newlineIndex;
|
|
291
|
+
while ((newlineIndex = this._buffer.indexOf('\n')) !== -1) {
|
|
292
|
+
const line = this._buffer.substring(0, newlineIndex).trim();
|
|
293
|
+
this._buffer = this._buffer.substring(newlineIndex + 1);
|
|
294
|
+
|
|
295
|
+
if (line) {
|
|
296
|
+
this._handleLine(line);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
_handleLine(line) {
|
|
302
|
+
let message;
|
|
303
|
+
try {
|
|
304
|
+
message = JSON.parse(line);
|
|
305
|
+
} catch (e) {
|
|
306
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
307
|
+
console.error(`[MCP:${this.name}] Failed to parse: ${line.substring(0, 200)}`);
|
|
308
|
+
}
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Response to a request
|
|
313
|
+
if (message.id !== undefined && this._pendingRequests.has(message.id)) {
|
|
314
|
+
const pending = this._pendingRequests.get(message.id);
|
|
315
|
+
this._pendingRequests.delete(message.id);
|
|
316
|
+
clearTimeout(pending.timeout);
|
|
317
|
+
|
|
318
|
+
if (message.error) {
|
|
319
|
+
pending.reject(new Error(`[MCP:${this.name}] ${message.error.message || JSON.stringify(message.error)}`));
|
|
320
|
+
} else {
|
|
321
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
322
|
+
const preview = JSON.stringify(message.result).substring(0, 200);
|
|
323
|
+
console.error(`[MCP:${this.name}] ← (id=${message.id}) ${preview}`);
|
|
324
|
+
}
|
|
325
|
+
pending.resolve(message.result);
|
|
326
|
+
}
|
|
327
|
+
} else if (message.method) {
|
|
328
|
+
// Server notification or request (we don't handle server-initiated requests)
|
|
329
|
+
if (process.env.KOI_DEBUG_LLM) {
|
|
330
|
+
console.error(`[MCP:${this.name}] Notification: ${message.method}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
package/src/runtime/planner.js
CHANGED
|
@@ -11,7 +11,7 @@ import { actionRegistry } from './action-registry.js';
|
|
|
11
11
|
export class Planner {
|
|
12
12
|
constructor(config) {
|
|
13
13
|
this.name = config.name || 'DefaultPlanner';
|
|
14
|
-
this.llm = config.llm || { provider: 'openai', model: 'gpt-
|
|
14
|
+
this.llm = config.llm || { provider: 'openai', model: 'gpt-5.2', temperature: 0 };
|
|
15
15
|
this.maxSteps = config.maxSteps || 10;
|
|
16
16
|
this.allowReplanning = config.allowReplanning !== false;
|
|
17
17
|
this.llmProvider = null;
|