operon-agents 0.1.0-alpha.2 → 0.1.0-alpha.21
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 +42 -0
- package/dist/app-server/client.js +8 -5
- package/dist/app-server/main.js +1 -1
- package/dist/app-server/params.d.ts +115 -0
- package/dist/app-server/params.js +81 -0
- package/dist/app-server/protocol.d.ts +10 -4
- package/dist/app-server/protocol.js +45 -37
- package/dist/app-server/server.d.ts +0 -1
- package/dist/app-server/server.js +46 -35
- package/dist/core.d.ts +2 -2
- package/dist/core.js +2 -2
- package/dist/cron/cron-expr.js +1 -1
- package/dist/cron/extension.js +2 -2
- package/dist/extensions/index.d.ts +19 -5
- package/dist/extensions/index.js +66 -10
- package/dist/extensions/loader.d.ts +2 -2
- package/dist/extensions/loader.js +23 -15
- package/dist/extensions/manager.d.ts +15 -13
- package/dist/extensions/manager.js +28 -25
- package/dist/extensions/runtime.d.ts +69 -74
- package/dist/extensions/runtime.js +183 -381
- package/dist/extensions/types.d.ts +55 -45
- package/dist/harness.d.ts +339 -180
- package/dist/harness.js +673 -420
- package/dist/index.d.ts +3 -0
- package/dist/index.js +5 -0
- package/dist/internal.d.ts +1 -0
- package/dist/internal.js +1 -0
- package/dist/local.d.ts +25 -8
- package/dist/local.js +103 -33
- package/dist/providers.d.ts +144 -0
- package/dist/providers.js +280 -0
- package/dist/telemetry-posthog.d.ts +88 -0
- package/dist/telemetry-posthog.js +142 -0
- package/dist/telemetry.d.ts +4 -0
- package/dist/telemetry.js +3 -0
- package/dist/workspace-mcp.d.ts +62 -0
- package/dist/workspace-mcp.js +222 -0
- package/package.json +10 -3
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP connections shared by every session opened in the same directory.
|
|
3
|
+
*
|
|
4
|
+
* A desktop host keeps one process up all day and opens many conversations in one project at
|
|
5
|
+
* once. Each of them used to connect the project's MCP servers again — an stdio server is a child
|
|
6
|
+
* process, so N chats in one folder meant N copies of every server. The pool keys one set of
|
|
7
|
+
* connections by the directory's real path and lends it to each session that opens there.
|
|
8
|
+
*
|
|
9
|
+
* This is deliberately NOT a lifetime tier of the engine (that was the workspace `Scope`, removed
|
|
10
|
+
* for the rules it cost). Core still knows two lifetimes, the process and the session; the pool
|
|
11
|
+
* is a host-side object the session hook hands to `defaultCapabilities`, and the MCP capability
|
|
12
|
+
* borrows from it at `openSession` and gives back at `closeSession`. Because the borrowing lives
|
|
13
|
+
* inside the capability's own open/close, a failed open undoes it like everything else the
|
|
14
|
+
* session opened — no hold is taken anywhere the session cannot see.
|
|
15
|
+
*
|
|
16
|
+
* Lifetime of one directory's set, a "generation":
|
|
17
|
+
* - built on the first borrow, connected once; concurrent borrows share that one connect;
|
|
18
|
+
* - kept while any session holds it, then for `idleMs` more, so closing the last chat and
|
|
19
|
+
* starting the next one does not respawn every server;
|
|
20
|
+
* - replaced, not mutated, when a borrow arrives with a different configuration (an edited
|
|
21
|
+
* `mcp.json`, a plugin enabled): new sessions get a new generation, the sessions already
|
|
22
|
+
* holding the old one keep it, and it closes when the last of them gives it back.
|
|
23
|
+
*
|
|
24
|
+
* Sharing is the point, so it is total: two sessions in one directory drive the SAME server.
|
|
25
|
+
* One with state of its own (a browser) is therefore shared state between them; a reconnect or an
|
|
26
|
+
* OAuth sign-in in one session is seen by all. A server that must be private belongs in the
|
|
27
|
+
* session's own `mcpServers`, which are layered over the shared set and never pooled.
|
|
28
|
+
*/
|
|
29
|
+
import { realpath } from "node:fs/promises";
|
|
30
|
+
import { resolve } from "node:path";
|
|
31
|
+
import { createMcpServers, mcpServersCapability, McpTransportNotPermittedError, } from "operon-agents-core";
|
|
32
|
+
/** How long a directory's connections outlive its last session by default: five minutes. */
|
|
33
|
+
export const DEFAULT_WORKSPACE_MCP_IDLE_MS = 5 * 60_000;
|
|
34
|
+
export class WorkspaceMcpPool {
|
|
35
|
+
options;
|
|
36
|
+
/** The generation a NEW borrow in each directory gets. */
|
|
37
|
+
current = new Map();
|
|
38
|
+
/** Every generation not yet shut down — current ones and retired ones still on loan. */
|
|
39
|
+
live = new Set();
|
|
40
|
+
closing;
|
|
41
|
+
constructor(options = {}) {
|
|
42
|
+
this.options = options;
|
|
43
|
+
}
|
|
44
|
+
/** Directories whose connections are currently up (on loan or idling). */
|
|
45
|
+
get size() {
|
|
46
|
+
return this.current.size;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Borrow the connections for `workDir`, building them if this directory has none for exactly
|
|
50
|
+
* these `configs`. Resolves once they have connected — fault-isolated, so a server that failed
|
|
51
|
+
* is a status on the handle, never a rejection. Rejects for what `createMcpServers` refuses
|
|
52
|
+
* up front (a transport this host does not run), before anything is built.
|
|
53
|
+
*/
|
|
54
|
+
async acquire(workDir, configs) {
|
|
55
|
+
this.assertOpen();
|
|
56
|
+
const key = await canonicalDir(workDir);
|
|
57
|
+
this.assertOpen();
|
|
58
|
+
const fingerprint = fingerprintOf(configs);
|
|
59
|
+
let generation = this.current.get(key);
|
|
60
|
+
if (generation !== undefined && generation.fingerprint !== fingerprint) {
|
|
61
|
+
this.current.delete(key);
|
|
62
|
+
if (generation.leases === 0)
|
|
63
|
+
void this.shutdown(generation);
|
|
64
|
+
generation = undefined;
|
|
65
|
+
}
|
|
66
|
+
if (generation === undefined) {
|
|
67
|
+
const { idleMs: _idleMs, ...serverOptions } = this.options;
|
|
68
|
+
const servers = createMcpServers(configs, serverOptions);
|
|
69
|
+
// Pooled connections belong to no session: their failures reach each borrower through
|
|
70
|
+
// `onStatusChange`, which the MCP capability subscribes to when it opens.
|
|
71
|
+
generation = { key, fingerprint, servers, ready: servers.connect({ sessionId: "" }), leases: 0 };
|
|
72
|
+
this.current.set(key, generation);
|
|
73
|
+
this.live.add(generation);
|
|
74
|
+
}
|
|
75
|
+
const held = generation;
|
|
76
|
+
held.leases += 1;
|
|
77
|
+
if (held.idle !== undefined) {
|
|
78
|
+
clearTimeout(held.idle);
|
|
79
|
+
held.idle = undefined;
|
|
80
|
+
}
|
|
81
|
+
let released = false;
|
|
82
|
+
const release = async () => {
|
|
83
|
+
if (released)
|
|
84
|
+
return;
|
|
85
|
+
released = true;
|
|
86
|
+
held.leases -= 1;
|
|
87
|
+
if (held.leases === 0)
|
|
88
|
+
await this.idle(held);
|
|
89
|
+
};
|
|
90
|
+
try {
|
|
91
|
+
await held.ready;
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
await release();
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
return { servers: held.servers, release };
|
|
98
|
+
}
|
|
99
|
+
/** Shut every directory's connections down, on loan or not. Later borrows throw. Idempotent. */
|
|
100
|
+
close() {
|
|
101
|
+
this.closing ??= (async () => {
|
|
102
|
+
this.current.clear();
|
|
103
|
+
await Promise.allSettled([...this.live].map((generation) => this.shutdown(generation)));
|
|
104
|
+
})();
|
|
105
|
+
return this.closing;
|
|
106
|
+
}
|
|
107
|
+
/** The last lease came back: close a retired generation now, a current one after the grace. */
|
|
108
|
+
async idle(generation) {
|
|
109
|
+
const idleMs = this.options.idleMs ?? DEFAULT_WORKSPACE_MCP_IDLE_MS;
|
|
110
|
+
if (this.current.get(generation.key) !== generation || idleMs <= 0) {
|
|
111
|
+
if (this.current.get(generation.key) === generation)
|
|
112
|
+
this.current.delete(generation.key);
|
|
113
|
+
await this.shutdown(generation);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
generation.idle = setTimeout(() => {
|
|
117
|
+
generation.idle = undefined;
|
|
118
|
+
if (generation.leases !== 0 || this.current.get(generation.key) !== generation)
|
|
119
|
+
return;
|
|
120
|
+
this.current.delete(generation.key);
|
|
121
|
+
void this.shutdown(generation);
|
|
122
|
+
}, idleMs);
|
|
123
|
+
// An idling set must not be what keeps the process alive.
|
|
124
|
+
generation.idle.unref?.();
|
|
125
|
+
}
|
|
126
|
+
shutdown(generation) {
|
|
127
|
+
if (generation.idle !== undefined) {
|
|
128
|
+
clearTimeout(generation.idle);
|
|
129
|
+
generation.idle = undefined;
|
|
130
|
+
}
|
|
131
|
+
this.live.delete(generation);
|
|
132
|
+
generation.closed ??= generation.ready.catch(() => undefined).then(() => generation.servers.shutdown());
|
|
133
|
+
return generation.closed;
|
|
134
|
+
}
|
|
135
|
+
assertOpen() {
|
|
136
|
+
if (this.closing !== undefined)
|
|
137
|
+
throw new Error("WorkspaceMcpPool is closed");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The `mcp` capability over a directory's pooled connections: borrows them at `openSession`,
|
|
142
|
+
* gives them back at `closeSession` (session close or detach alike). In between it is exactly
|
|
143
|
+
* `mcpServersCapability` viewing a shared set — same tools, same `session.mcp`, same overlay.
|
|
144
|
+
*/
|
|
145
|
+
export function workspaceMcpCapability(options) {
|
|
146
|
+
const allowed = options.overlayOptions?.allowedTransports;
|
|
147
|
+
if (allowed !== undefined) {
|
|
148
|
+
for (const [name, config] of Object.entries(options.servers)) {
|
|
149
|
+
if (config.enabled !== false && !allowed.includes(config.transport)) {
|
|
150
|
+
throw new McpTransportNotPermittedError(name, config.transport, allowed);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
let lease;
|
|
155
|
+
/** The borrowed set while a lease is held; the view below is empty without one. */
|
|
156
|
+
let bound;
|
|
157
|
+
const view = {
|
|
158
|
+
list: () => bound?.list() ?? [],
|
|
159
|
+
listTools: async (name) => (bound !== undefined ? bound.listTools(name) : []),
|
|
160
|
+
reconnect: async (name) => {
|
|
161
|
+
if (bound === undefined)
|
|
162
|
+
throw new Error(`Unknown MCP server: ${name}`);
|
|
163
|
+
await bound.reconnect(name);
|
|
164
|
+
},
|
|
165
|
+
onStatusChange: (listener) => bound?.onStatusChange(listener) ?? (() => { }),
|
|
166
|
+
toolProviders: () => bound?.toolProviders() ?? [],
|
|
167
|
+
// The pool connects and shuts down; a viewer does neither.
|
|
168
|
+
connect: async () => { },
|
|
169
|
+
shutdown: async () => { },
|
|
170
|
+
};
|
|
171
|
+
const inner = mcpServersCapability(options.overlay ?? {}, { ...(options.overlayOptions ?? {}), sharedServers: view });
|
|
172
|
+
const giveBack = async () => {
|
|
173
|
+
const held = lease;
|
|
174
|
+
lease = undefined;
|
|
175
|
+
bound = undefined;
|
|
176
|
+
await held?.release();
|
|
177
|
+
};
|
|
178
|
+
return {
|
|
179
|
+
name: inner.name,
|
|
180
|
+
contract: inner.contract,
|
|
181
|
+
...(inner.toolProviders !== undefined ? { toolProviders: inner.toolProviders } : {}),
|
|
182
|
+
get service() {
|
|
183
|
+
return inner.service;
|
|
184
|
+
},
|
|
185
|
+
openSession: async (ctx) => {
|
|
186
|
+
lease = await options.pool.acquire(options.workDir, options.servers);
|
|
187
|
+
bound = lease.servers;
|
|
188
|
+
try {
|
|
189
|
+
await inner.openSession?.(ctx);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
await giveBack();
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
closeSession: async (reason) => {
|
|
197
|
+
try {
|
|
198
|
+
await inner.closeSession?.(reason);
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
await giveBack();
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/** The directory's real path, so `./proj`, `/tmp/proj` and a symlink to it share one set. */
|
|
207
|
+
async function canonicalDir(workDir) {
|
|
208
|
+
const absolute = resolve(workDir);
|
|
209
|
+
try {
|
|
210
|
+
return await realpath(absolute);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return absolute;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/** Order-independent identity of a configuration: same servers, same settings, same string. */
|
|
217
|
+
function fingerprintOf(configs) {
|
|
218
|
+
return JSON.stringify(configs, (_key, value) => value !== null && typeof value === "object" && !Array.isArray(value)
|
|
219
|
+
? Object.fromEntries(Object.entries(value).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)))
|
|
220
|
+
: value);
|
|
221
|
+
}
|
|
222
|
+
//# sourceMappingURL=workspace-mcp.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "operon-agents",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.21",
|
|
4
4
|
"description": "Public SDK entrypoint for building agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -58,6 +58,10 @@
|
|
|
58
58
|
"types": "./dist/tracing.d.ts",
|
|
59
59
|
"import": "./dist/tracing.js"
|
|
60
60
|
},
|
|
61
|
+
"./telemetry": {
|
|
62
|
+
"types": "./dist/telemetry.d.ts",
|
|
63
|
+
"import": "./dist/telemetry.js"
|
|
64
|
+
},
|
|
61
65
|
"./package.json": "./package.json"
|
|
62
66
|
},
|
|
63
67
|
"files": [
|
|
@@ -66,11 +70,14 @@
|
|
|
66
70
|
"README.md"
|
|
67
71
|
],
|
|
68
72
|
"dependencies": {
|
|
73
|
+
"posthog-node": "^5.51.6",
|
|
69
74
|
"zod": "^4.4.3",
|
|
70
|
-
"
|
|
75
|
+
"smol-toml": "^1.6.1",
|
|
76
|
+
"@earendil-works/pi-ai": "1.3.0",
|
|
77
|
+
"operon-agents-core": "0.1.0-alpha.21"
|
|
71
78
|
},
|
|
72
79
|
"devDependencies": {
|
|
73
|
-
"@earendil-works/pi-ai": "
|
|
80
|
+
"@earendil-works/pi-ai": "1.3.0"
|
|
74
81
|
},
|
|
75
82
|
"scripts": {
|
|
76
83
|
"clean": "rm -rf dist *.tsbuildinfo",
|