@granular-software/sdk 0.2.0 → 0.2.1
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/adapters/anthropic.d.mts +1 -1
- package/dist/adapters/anthropic.d.ts +1 -1
- package/dist/adapters/langchain.d.mts +1 -1
- package/dist/adapters/langchain.d.ts +1 -1
- package/dist/adapters/mastra.d.mts +1 -1
- package/dist/adapters/mastra.d.ts +1 -1
- package/dist/adapters/openai.d.mts +1 -1
- package/dist/adapters/openai.d.ts +1 -1
- package/dist/cli/index.js +238 -12
- package/dist/index.d.mts +149 -4
- package/dist/index.d.ts +149 -4
- package/dist/index.js +441 -34
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +441 -31
- package/dist/index.mjs.map +1 -1
- package/dist/{types-D46q5WTh.d.mts → types-DiMEb3SE.d.mts} +41 -1
- package/dist/{types-D46q5WTh.d.ts → types-DiMEb3SE.d.ts} +41 -1
- package/package.json +2 -3
package/dist/index.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var WebSocket = require('ws');
|
|
4
3
|
var Automerge = require('@automerge/automerge');
|
|
5
4
|
|
|
6
|
-
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
7
|
-
|
|
8
5
|
function _interopNamespace(e) {
|
|
9
6
|
if (e && e.__esModule) return e;
|
|
10
7
|
var n = Object.create(null);
|
|
@@ -23,10 +20,14 @@ function _interopNamespace(e) {
|
|
|
23
20
|
return Object.freeze(n);
|
|
24
21
|
}
|
|
25
22
|
|
|
26
|
-
var WebSocket__default = /*#__PURE__*/_interopDefault(WebSocket);
|
|
27
23
|
var Automerge__namespace = /*#__PURE__*/_interopNamespace(Automerge);
|
|
28
24
|
|
|
29
25
|
// src/ws-client.ts
|
|
26
|
+
var GlobalWebSocket = void 0;
|
|
27
|
+
if (typeof globalThis !== "undefined" && globalThis.WebSocket) {
|
|
28
|
+
GlobalWebSocket = globalThis.WebSocket;
|
|
29
|
+
}
|
|
30
|
+
var READY_STATE_OPEN = 1;
|
|
30
31
|
var WSClient = class {
|
|
31
32
|
ws = null;
|
|
32
33
|
url;
|
|
@@ -41,7 +42,9 @@ var WSClient = class {
|
|
|
41
42
|
syncState = Automerge__namespace.initSyncState();
|
|
42
43
|
reconnectTimer = null;
|
|
43
44
|
isExplicitlyDisconnected = false;
|
|
45
|
+
options;
|
|
44
46
|
constructor(options) {
|
|
47
|
+
this.options = options;
|
|
45
48
|
this.url = options.url;
|
|
46
49
|
this.sessionId = options.sessionId;
|
|
47
50
|
this.token = options.token;
|
|
@@ -52,34 +55,68 @@ var WSClient = class {
|
|
|
52
55
|
*/
|
|
53
56
|
async connect() {
|
|
54
57
|
this.isExplicitlyDisconnected = false;
|
|
58
|
+
const WebSocketClass = this.options.WebSocketCtor || GlobalWebSocket;
|
|
59
|
+
if (!WebSocketClass) {
|
|
60
|
+
throw new Error('No WebSocket implementation found. If using Node.js, please install "ws" and pass the constructor to the SDK options: { WebSocketCtor: WebSocket }.');
|
|
61
|
+
}
|
|
55
62
|
return new Promise((resolve, reject) => {
|
|
56
63
|
try {
|
|
57
64
|
const wsUrl = new URL(this.url);
|
|
58
65
|
wsUrl.searchParams.set("sessionId", this.sessionId);
|
|
59
66
|
wsUrl.searchParams.set("token", this.token);
|
|
60
|
-
this.ws = new
|
|
61
|
-
this.ws
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
67
|
+
this.ws = new WebSocketClass(wsUrl.toString());
|
|
68
|
+
if (!this.ws) throw new Error("Failed to create WebSocket");
|
|
69
|
+
const socket = this.ws;
|
|
70
|
+
if (typeof socket.on === "function") {
|
|
71
|
+
socket.on("open", () => {
|
|
72
|
+
this.emit("open", {});
|
|
73
|
+
resolve();
|
|
74
|
+
});
|
|
75
|
+
socket.on("message", (data) => {
|
|
76
|
+
try {
|
|
77
|
+
const message = JSON.parse(data.toString());
|
|
78
|
+
this.handleMessage(message);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error("[Granular] Failed to parse message:", error);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
socket.on("error", (error) => {
|
|
84
|
+
this.emit("error", error);
|
|
85
|
+
if (socket.readyState !== READY_STATE_OPEN) {
|
|
86
|
+
reject(error);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
socket.on("close", () => {
|
|
90
|
+
this.emit("close", {});
|
|
91
|
+
this.handleDisconnect();
|
|
92
|
+
});
|
|
93
|
+
} else {
|
|
94
|
+
this.ws.onopen = () => {
|
|
95
|
+
this.emit("open", {});
|
|
96
|
+
resolve();
|
|
97
|
+
};
|
|
98
|
+
this.ws.onmessage = (event) => {
|
|
99
|
+
try {
|
|
100
|
+
const data = event.data;
|
|
101
|
+
const message = JSON.parse(data.toString());
|
|
102
|
+
this.handleMessage(message);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error("[Granular] Failed to parse message:", error);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
this.ws.onerror = (event) => {
|
|
108
|
+
const error = new Error("WebSocket error");
|
|
109
|
+
error.event = event;
|
|
110
|
+
this.emit("error", error);
|
|
111
|
+
if (this.ws?.readyState !== READY_STATE_OPEN) {
|
|
112
|
+
reject(error);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
this.ws.onclose = () => {
|
|
116
|
+
this.emit("close", {});
|
|
117
|
+
this.handleDisconnect();
|
|
118
|
+
};
|
|
119
|
+
}
|
|
83
120
|
} catch (error) {
|
|
84
121
|
reject(error);
|
|
85
122
|
}
|
|
@@ -99,8 +136,8 @@ var WSClient = class {
|
|
|
99
136
|
console.log("[Granular DEBUG] Received message:", JSON.stringify(message).slice(0, 500));
|
|
100
137
|
if ("type" in message && message.type === "sync") {
|
|
101
138
|
const syncMessage = message;
|
|
139
|
+
let bytes;
|
|
102
140
|
try {
|
|
103
|
-
let bytes;
|
|
104
141
|
const payload = syncMessage.message || syncMessage.data;
|
|
105
142
|
if (typeof payload === "string") {
|
|
106
143
|
const binaryString = atob(payload);
|
|
@@ -116,6 +153,7 @@ var WSClient = class {
|
|
|
116
153
|
} else {
|
|
117
154
|
return;
|
|
118
155
|
}
|
|
156
|
+
console.log("[Granular DEBUG] Applying sync bytes:", bytes.length);
|
|
119
157
|
const [newDoc, newSyncState] = Automerge__namespace.receiveSyncMessage(
|
|
120
158
|
this.doc,
|
|
121
159
|
this.syncState,
|
|
@@ -123,8 +161,49 @@ var WSClient = class {
|
|
|
123
161
|
);
|
|
124
162
|
this.doc = newDoc;
|
|
125
163
|
this.syncState = newSyncState;
|
|
164
|
+
const docAny = this.doc;
|
|
165
|
+
if (docAny.catalog) {
|
|
166
|
+
console.log("[Granular DEBUG] Doc catalog sync applied. Keys in catalog:", Object.keys(docAny.catalog || {}));
|
|
167
|
+
console.log("[Granular DEBUG] RawToolCatalogs:", Object.keys(docAny.catalog.rawToolCatalogs || {}));
|
|
168
|
+
} else {
|
|
169
|
+
console.log("[Granular DEBUG] Doc synced but no catalog yet. Keys in doc:", Object.keys(docAny));
|
|
170
|
+
}
|
|
126
171
|
this.emit("sync", this.doc);
|
|
127
172
|
} catch (e) {
|
|
173
|
+
try {
|
|
174
|
+
console.log("[Granular DEBUG] receiveSyncMessage failed, trying applyChanges...");
|
|
175
|
+
const [newDoc] = Automerge__namespace.applyChanges(this.doc, [bytes]);
|
|
176
|
+
this.doc = newDoc;
|
|
177
|
+
this.emit("sync", this.doc);
|
|
178
|
+
console.log("[Granular DEBUG] applyChanges succeeded. Doc:", JSON.stringify(Automerge__namespace.toJS(this.doc)));
|
|
179
|
+
} catch (applyError) {
|
|
180
|
+
console.warn("[Granular] Failed to apply sync message (both sync & applyChanges)", e, applyError);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if ("type" in message && message.type === "snapshot") {
|
|
186
|
+
const snapshotMessage = message;
|
|
187
|
+
try {
|
|
188
|
+
const bytes = new Uint8Array(snapshotMessage.data);
|
|
189
|
+
console.log("[Granular DEBUG] Loading snapshot bytes:", bytes.length);
|
|
190
|
+
this.doc = Automerge__namespace.load(bytes);
|
|
191
|
+
this.emit("sync", this.doc);
|
|
192
|
+
console.log("[Granular DEBUG] Snapshot loaded. Doc:", JSON.stringify(Automerge__namespace.toJS(this.doc)));
|
|
193
|
+
} catch (e) {
|
|
194
|
+
console.warn("[Granular] Failed to load snapshot message", e);
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if ("type" in message && message.type === "change") {
|
|
199
|
+
const changeMessage = message;
|
|
200
|
+
try {
|
|
201
|
+
const bytes = new Uint8Array(changeMessage.data);
|
|
202
|
+
const [newDoc] = Automerge__namespace.applyChanges(this.doc, [bytes]);
|
|
203
|
+
this.doc = newDoc;
|
|
204
|
+
this.emit("sync", this.doc);
|
|
205
|
+
} catch (e) {
|
|
206
|
+
console.warn("[Granular] Failed to apply change message", e);
|
|
128
207
|
}
|
|
129
208
|
return;
|
|
130
209
|
}
|
|
@@ -162,7 +241,7 @@ var WSClient = class {
|
|
|
162
241
|
* @throws {Error} If connection is closed or timeout occurs
|
|
163
242
|
*/
|
|
164
243
|
async call(method, params) {
|
|
165
|
-
if (!this.ws || this.ws.readyState !==
|
|
244
|
+
if (!this.ws || this.ws.readyState !== READY_STATE_OPEN) {
|
|
166
245
|
throw new Error("WebSocket not connected");
|
|
167
246
|
}
|
|
168
247
|
const id = `rpc-${this.nextRpcId++}`;
|
|
@@ -291,6 +370,10 @@ var Session = class {
|
|
|
291
370
|
/** Tracks which tools are instance methods (className set, not static) */
|
|
292
371
|
instanceTools = /* @__PURE__ */ new Set();
|
|
293
372
|
currentDomainRevision = null;
|
|
373
|
+
/** Local effect registry: name → full ToolWithHandler */
|
|
374
|
+
effects = /* @__PURE__ */ new Map();
|
|
375
|
+
/** Last known tools for diffing */
|
|
376
|
+
lastKnownTools = /* @__PURE__ */ new Map();
|
|
294
377
|
constructor(client, clientId) {
|
|
295
378
|
this.client = client;
|
|
296
379
|
this.clientId = clientId || `client_${Date.now()}`;
|
|
@@ -406,6 +489,85 @@ var Session = class {
|
|
|
406
489
|
rejected: result.rejected
|
|
407
490
|
};
|
|
408
491
|
}
|
|
492
|
+
/**
|
|
493
|
+
* Publish a single effect (tool) to the sandbox.
|
|
494
|
+
*
|
|
495
|
+
* Adds the effect to the local registry and re-publishes the
|
|
496
|
+
* full tool catalog to the server. If an effect with the same
|
|
497
|
+
* name already exists, it is replaced.
|
|
498
|
+
*
|
|
499
|
+
* @param effect - The effect (tool) to publish
|
|
500
|
+
* @returns PublishToolsResult with domainRevision
|
|
501
|
+
*
|
|
502
|
+
* @example
|
|
503
|
+
* ```typescript
|
|
504
|
+
* await env.publishEffect({
|
|
505
|
+
* name: 'get_bio',
|
|
506
|
+
* description: 'Get biography of an author',
|
|
507
|
+
* className: 'author',
|
|
508
|
+
* inputSchema: { type: 'object', properties: { detailed: { type: 'boolean' } } },
|
|
509
|
+
* handler: async (id, params) => ({ bio: `Bio of ${id}` }),
|
|
510
|
+
* });
|
|
511
|
+
* ```
|
|
512
|
+
*/
|
|
513
|
+
async publishEffect(effect) {
|
|
514
|
+
this.effects.set(effect.name, effect);
|
|
515
|
+
return this._syncEffects();
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Publish multiple effects (tools) at once.
|
|
519
|
+
*
|
|
520
|
+
* Adds all effects to the local registry and re-publishes the
|
|
521
|
+
* full tool catalog in a single RPC call.
|
|
522
|
+
*
|
|
523
|
+
* @param effects - Array of effects to publish
|
|
524
|
+
* @returns PublishToolsResult with domainRevision
|
|
525
|
+
*
|
|
526
|
+
* @example
|
|
527
|
+
* ```typescript
|
|
528
|
+
* await env.publishEffects([
|
|
529
|
+
* { name: 'get_bio', description: '...', inputSchema: {}, handler: async (id) => ({}) },
|
|
530
|
+
* { name: 'search', description: '...', inputSchema: {}, handler: async (params) => ({}) },
|
|
531
|
+
* ]);
|
|
532
|
+
* ```
|
|
533
|
+
*/
|
|
534
|
+
async publishEffects(effects) {
|
|
535
|
+
for (const effect of effects) {
|
|
536
|
+
this.effects.set(effect.name, effect);
|
|
537
|
+
}
|
|
538
|
+
return this._syncEffects();
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Remove an effect by name and re-publish the remaining catalog.
|
|
542
|
+
*
|
|
543
|
+
* @param name - The name of the effect to remove
|
|
544
|
+
* @returns PublishToolsResult with domainRevision
|
|
545
|
+
*/
|
|
546
|
+
async unpublishEffect(name) {
|
|
547
|
+
this.effects.delete(name);
|
|
548
|
+
this.toolHandlers.delete(name);
|
|
549
|
+
this.instanceTools.delete(name);
|
|
550
|
+
return this._syncEffects();
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Remove all effects and publish an empty catalog.
|
|
554
|
+
*
|
|
555
|
+
* @returns PublishToolsResult with domainRevision
|
|
556
|
+
*/
|
|
557
|
+
async unpublishAllEffects() {
|
|
558
|
+
this.effects.clear();
|
|
559
|
+
this.toolHandlers.clear();
|
|
560
|
+
this.instanceTools.clear();
|
|
561
|
+
return this._syncEffects();
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Internal: re-publish the full effect catalog to the server.
|
|
565
|
+
* Called after any mutation to the effects Map.
|
|
566
|
+
*/
|
|
567
|
+
async _syncEffects() {
|
|
568
|
+
const allEffects = Array.from(this.effects.values());
|
|
569
|
+
return this.publishTools(allEffects);
|
|
570
|
+
}
|
|
409
571
|
/**
|
|
410
572
|
* Submit a job to execute code in the sandbox.
|
|
411
573
|
*
|
|
@@ -423,9 +585,9 @@ var Session = class {
|
|
|
423
585
|
* execute locally and return the result to the sandbox.
|
|
424
586
|
*/
|
|
425
587
|
async submitJob(code, domainRevision) {
|
|
426
|
-
const revision = domainRevision || this.currentDomainRevision;
|
|
588
|
+
const revision = domainRevision || this.currentDomainRevision || this.client.doc?.domain?.active || void 0;
|
|
427
589
|
if (!revision) {
|
|
428
|
-
throw new Error("No domain revision available. Call publishTools()
|
|
590
|
+
throw new Error("No domain revision available. Call publishTools() or ensure schema is activated.");
|
|
429
591
|
}
|
|
430
592
|
const result = await this.client.call("job.submit", {
|
|
431
593
|
domainRevision: revision,
|
|
@@ -456,6 +618,72 @@ var Session = class {
|
|
|
456
618
|
async answerPrompt(promptId, answer) {
|
|
457
619
|
await this.client.call("prompt.answer", { promptId, value: answer });
|
|
458
620
|
}
|
|
621
|
+
/**
|
|
622
|
+
* Get the current list of available tools.
|
|
623
|
+
* Consolidates tools from all connected clients.
|
|
624
|
+
*/
|
|
625
|
+
getTools() {
|
|
626
|
+
const doc = this.client.doc;
|
|
627
|
+
const toolMap = /* @__PURE__ */ new Map();
|
|
628
|
+
const domainPkg = doc.domain?.packages?.domain;
|
|
629
|
+
if (domainPkg?.tools && Array.isArray(domainPkg.tools)) {
|
|
630
|
+
for (const tool of domainPkg.tools) {
|
|
631
|
+
if (!tool?.name) continue;
|
|
632
|
+
toolMap.set(tool.name, {
|
|
633
|
+
name: tool.name,
|
|
634
|
+
description: tool.description,
|
|
635
|
+
inputSchema: tool.inputSchema,
|
|
636
|
+
outputSchema: tool.outputSchema,
|
|
637
|
+
className: tool.className || void 0,
|
|
638
|
+
static: tool.static || false,
|
|
639
|
+
ready: false,
|
|
640
|
+
publishedAt: void 0
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
const catalogs = doc.catalog?.rawToolCatalogs || {};
|
|
645
|
+
for (const [clientId, catalog] of Object.entries(catalogs)) {
|
|
646
|
+
const cat = catalog;
|
|
647
|
+
if (!cat.tools) continue;
|
|
648
|
+
for (const tool of cat.tools) {
|
|
649
|
+
if (!tool?.name) continue;
|
|
650
|
+
const existing = toolMap.get(tool.name);
|
|
651
|
+
if (existing?.publishedAt && cat.publishedAt && existing.publishedAt > cat.publishedAt) continue;
|
|
652
|
+
const isLocal = clientId === this.clientId;
|
|
653
|
+
const ready = isLocal ? this.effects.has(tool.name) || this.toolHandlers.has(tool.name) || this.instanceTools.has(tool.name) : true;
|
|
654
|
+
toolMap.set(tool.name, {
|
|
655
|
+
name: tool.name,
|
|
656
|
+
description: tool.description,
|
|
657
|
+
inputSchema: tool.inputSchema,
|
|
658
|
+
outputSchema: tool.outputSchema,
|
|
659
|
+
className: tool.className || existing?.className,
|
|
660
|
+
static: tool.static || existing?.static || false,
|
|
661
|
+
clientId,
|
|
662
|
+
ready,
|
|
663
|
+
publishedAt: cat.publishedAt
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return Array.from(toolMap.values());
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Subscribe to tool changes (added, removed, updated).
|
|
671
|
+
* @param callback - Function called with change events
|
|
672
|
+
* @returns Unsubscribe function
|
|
673
|
+
*/
|
|
674
|
+
onToolsChanged(callback) {
|
|
675
|
+
const handler = (data) => callback(data);
|
|
676
|
+
if (!this.eventListeners.has("tools:changed")) {
|
|
677
|
+
this.eventListeners.set("tools:changed", []);
|
|
678
|
+
}
|
|
679
|
+
this.eventListeners.get("tools:changed").push(handler);
|
|
680
|
+
return () => {
|
|
681
|
+
const listeners = this.eventListeners.get("tools:changed");
|
|
682
|
+
if (listeners) {
|
|
683
|
+
this.eventListeners.set("tools:changed", listeners.filter((h) => h !== handler));
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
}
|
|
459
687
|
/**
|
|
460
688
|
* Get the current domain state and available tools
|
|
461
689
|
*/
|
|
@@ -664,7 +892,10 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
664
892
|
});
|
|
665
893
|
}
|
|
666
894
|
setupEventHandlers() {
|
|
667
|
-
this.client.on("sync", (doc) =>
|
|
895
|
+
this.client.on("sync", (doc) => {
|
|
896
|
+
this.emit("sync", doc);
|
|
897
|
+
this.checkForToolChanges();
|
|
898
|
+
});
|
|
668
899
|
this.client.on("prompt", (prompt) => this.emit("prompt", prompt));
|
|
669
900
|
this.client.on("disconnect", () => this.emit("disconnect", {}));
|
|
670
901
|
this.client.on("job.status", (data) => {
|
|
@@ -683,6 +914,33 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
683
914
|
handlers.forEach((h) => h(data));
|
|
684
915
|
}
|
|
685
916
|
}
|
|
917
|
+
/**
|
|
918
|
+
* Check for changes in the tool catalog and emit 'tools:changed' if needed
|
|
919
|
+
*/
|
|
920
|
+
checkForToolChanges() {
|
|
921
|
+
const currentTools = this.getTools();
|
|
922
|
+
const currentMap = new Map(currentTools.map((t) => [t.name, t]));
|
|
923
|
+
const added = [];
|
|
924
|
+
const removed = [];
|
|
925
|
+
for (const [name, tool] of currentMap) {
|
|
926
|
+
if (!this.lastKnownTools.has(name)) {
|
|
927
|
+
added.push(name);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
for (const name of this.lastKnownTools.keys()) {
|
|
931
|
+
if (!currentMap.has(name)) {
|
|
932
|
+
removed.push(name);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
if (added.length > 0 || removed.length > 0) {
|
|
936
|
+
this.lastKnownTools = currentMap;
|
|
937
|
+
this.emit("tools:changed", {
|
|
938
|
+
tools: currentTools,
|
|
939
|
+
added,
|
|
940
|
+
removed
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
}
|
|
686
944
|
};
|
|
687
945
|
var JobImplementation = class {
|
|
688
946
|
id;
|
|
@@ -756,6 +1014,18 @@ var JobImplementation = class {
|
|
|
756
1014
|
this._rejectResult(jobData.error || new Error("Job failed"));
|
|
757
1015
|
}
|
|
758
1016
|
});
|
|
1017
|
+
this.client.on("tool.call.start", (data) => {
|
|
1018
|
+
const d = data;
|
|
1019
|
+
if (d.jobId === id) {
|
|
1020
|
+
this.emit("toolCallStart", { callId: d.callId, toolName: d.toolName, input: d.input, timestamp: d.timestamp });
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
this.client.on("tool.call.end", (data) => {
|
|
1024
|
+
const d = data;
|
|
1025
|
+
if (d.jobId === id) {
|
|
1026
|
+
this.emit("toolCallEnd", { callId: d.callId, toolName: d.toolName, result: d.result, error: d.error, durationMs: d.durationMs, timestamp: d.timestamp });
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
759
1029
|
}
|
|
760
1030
|
get result() {
|
|
761
1031
|
return this._resultPromise;
|
|
@@ -1470,11 +1740,57 @@ var Environment = class _Environment extends Session {
|
|
|
1470
1740
|
async publishTools(tools, revision = "1.0.0") {
|
|
1471
1741
|
return super.publishTools(tools, revision);
|
|
1472
1742
|
}
|
|
1743
|
+
/**
|
|
1744
|
+
* Publish a single effect (tool) incrementally.
|
|
1745
|
+
*
|
|
1746
|
+
* Adds the effect to the local registry and re-publishes the
|
|
1747
|
+
* full tool catalog to the server. If an effect with the same
|
|
1748
|
+
* name already exists, it is replaced.
|
|
1749
|
+
*
|
|
1750
|
+
* @example
|
|
1751
|
+
* ```typescript
|
|
1752
|
+
* await env.publishEffect({
|
|
1753
|
+
* name: 'get_bio',
|
|
1754
|
+
* description: 'Get biography',
|
|
1755
|
+
* inputSchema: { type: 'object', properties: { detailed: { type: 'boolean' } } },
|
|
1756
|
+
* handler: async (params) => ({ bio: 'Hello' }),
|
|
1757
|
+
* });
|
|
1758
|
+
* ```
|
|
1759
|
+
*/
|
|
1760
|
+
async publishEffect(effect) {
|
|
1761
|
+
return super.publishEffect(effect);
|
|
1762
|
+
}
|
|
1763
|
+
/**
|
|
1764
|
+
* Publish multiple effects (tools) at once.
|
|
1765
|
+
*
|
|
1766
|
+
* Adds all effects to the local registry and re-publishes the
|
|
1767
|
+
* full tool catalog in a single RPC call.
|
|
1768
|
+
*/
|
|
1769
|
+
async publishEffects(effects) {
|
|
1770
|
+
return super.publishEffects(effects);
|
|
1771
|
+
}
|
|
1772
|
+
/**
|
|
1773
|
+
* Remove an effect by name and re-publish the remaining catalog.
|
|
1774
|
+
*/
|
|
1775
|
+
async unpublishEffect(name) {
|
|
1776
|
+
return super.unpublishEffect(name);
|
|
1777
|
+
}
|
|
1778
|
+
/**
|
|
1779
|
+
* Remove all effects and publish an empty catalog.
|
|
1780
|
+
*/
|
|
1781
|
+
async unpublishAllEffects() {
|
|
1782
|
+
return super.unpublishAllEffects();
|
|
1783
|
+
}
|
|
1473
1784
|
};
|
|
1474
1785
|
var Granular = class {
|
|
1475
1786
|
apiKey;
|
|
1476
1787
|
apiUrl;
|
|
1477
1788
|
httpUrl;
|
|
1789
|
+
WebSocketCtor;
|
|
1790
|
+
/** Sandbox-level effect registry: sandboxId → (toolName → ToolWithHandler) */
|
|
1791
|
+
sandboxEffects = /* @__PURE__ */ new Map();
|
|
1792
|
+
/** Active environments tracker: sandboxId → Environment[] */
|
|
1793
|
+
activeEnvironments = /* @__PURE__ */ new Map();
|
|
1478
1794
|
/**
|
|
1479
1795
|
* Create a new Granular client
|
|
1480
1796
|
* @param options - Client configuration
|
|
@@ -1482,6 +1798,7 @@ var Granular = class {
|
|
|
1482
1798
|
constructor(options) {
|
|
1483
1799
|
this.apiKey = options.apiKey;
|
|
1484
1800
|
this.apiUrl = options.apiUrl || "wss://api.granular.dev/v2/ws";
|
|
1801
|
+
this.WebSocketCtor = options.WebSocketCtor;
|
|
1485
1802
|
this.httpUrl = this.apiUrl.replace("wss://", "https://").replace("/ws", "");
|
|
1486
1803
|
}
|
|
1487
1804
|
/**
|
|
@@ -1552,7 +1869,7 @@ var Granular = class {
|
|
|
1552
1869
|
* ```
|
|
1553
1870
|
*/
|
|
1554
1871
|
async connect(options) {
|
|
1555
|
-
const clientId = `client_${Date.now()}`;
|
|
1872
|
+
const clientId = options.clientId || `client_${Date.now()}`;
|
|
1556
1873
|
const sandbox = await this.findOrCreateSandbox(options.sandbox);
|
|
1557
1874
|
for (const profileName of options.user.permissions) {
|
|
1558
1875
|
const profileId = await this.ensurePermissionProfile(sandbox.sandboxId, profileName);
|
|
@@ -1565,14 +1882,104 @@ var Granular = class {
|
|
|
1565
1882
|
const client = new WSClient({
|
|
1566
1883
|
url: this.apiUrl,
|
|
1567
1884
|
sessionId: envData.environmentId,
|
|
1568
|
-
token: this.apiKey
|
|
1885
|
+
token: this.apiKey,
|
|
1886
|
+
WebSocketCtor: this.WebSocketCtor
|
|
1569
1887
|
});
|
|
1570
1888
|
await client.connect();
|
|
1571
1889
|
const graphqlEndpoint = `${this.httpUrl}/orchestrator/graphql`;
|
|
1572
1890
|
const environment = new Environment(client, envData, clientId, this.apiKey, graphqlEndpoint);
|
|
1891
|
+
if (!this.activeEnvironments.has(sandbox.sandboxId)) {
|
|
1892
|
+
this.activeEnvironments.set(sandbox.sandboxId, []);
|
|
1893
|
+
}
|
|
1894
|
+
this.activeEnvironments.get(sandbox.sandboxId).push(environment);
|
|
1895
|
+
environment.on("disconnect", () => {
|
|
1896
|
+
const list = this.activeEnvironments.get(sandbox.sandboxId);
|
|
1897
|
+
if (list) {
|
|
1898
|
+
this.activeEnvironments.set(sandbox.sandboxId, list.filter((e) => e !== environment));
|
|
1899
|
+
}
|
|
1900
|
+
});
|
|
1573
1901
|
await environment.hello();
|
|
1902
|
+
const effects = this.sandboxEffects.get(sandbox.sandboxId);
|
|
1903
|
+
if (effects && effects.size > 0) {
|
|
1904
|
+
const effectsList = Array.from(effects.values());
|
|
1905
|
+
console.log(`[Granular] Auto-publishing ${effectsList.length} effects for sandbox ${sandbox.sandboxId}`);
|
|
1906
|
+
await environment.publishEffects(effectsList);
|
|
1907
|
+
}
|
|
1574
1908
|
return environment;
|
|
1575
1909
|
}
|
|
1910
|
+
// ── Sandbox-Level Effects ──
|
|
1911
|
+
/**
|
|
1912
|
+
* Register an effect (tool) for a specific sandbox.
|
|
1913
|
+
*
|
|
1914
|
+
* The effect will be automatically published to:
|
|
1915
|
+
* 1. Any currently active environments for this sandbox
|
|
1916
|
+
* 2. Any new environments created/connected for this sandbox
|
|
1917
|
+
*
|
|
1918
|
+
* @param sandboxNameOrId - The name or ID of the sandbox
|
|
1919
|
+
* @param effect - The tool definition and handler
|
|
1920
|
+
*/
|
|
1921
|
+
async registerEffect(sandboxNameOrId, effect) {
|
|
1922
|
+
const sandbox = await this.findOrCreateSandbox(sandboxNameOrId);
|
|
1923
|
+
const sandboxId = sandbox.sandboxId;
|
|
1924
|
+
if (!this.sandboxEffects.has(sandboxId)) {
|
|
1925
|
+
this.sandboxEffects.set(sandboxId, /* @__PURE__ */ new Map());
|
|
1926
|
+
}
|
|
1927
|
+
this.sandboxEffects.get(sandboxId).set(effect.name, effect);
|
|
1928
|
+
const activeEnvs = this.activeEnvironments.get(sandboxId) || [];
|
|
1929
|
+
for (const env of activeEnvs) {
|
|
1930
|
+
await env.publishEffect(effect);
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
/**
|
|
1934
|
+
* Register multiple effects (tools) for a specific sandbox.
|
|
1935
|
+
*
|
|
1936
|
+
* batch version of `registerEffect`.
|
|
1937
|
+
*/
|
|
1938
|
+
async registerEffects(sandboxNameOrId, effects) {
|
|
1939
|
+
const sandbox = await this.findOrCreateSandbox(sandboxNameOrId);
|
|
1940
|
+
const sandboxId = sandbox.sandboxId;
|
|
1941
|
+
if (!this.sandboxEffects.has(sandboxId)) {
|
|
1942
|
+
this.sandboxEffects.set(sandboxId, /* @__PURE__ */ new Map());
|
|
1943
|
+
}
|
|
1944
|
+
const map = this.sandboxEffects.get(sandboxId);
|
|
1945
|
+
for (const effect of effects) {
|
|
1946
|
+
map.set(effect.name, effect);
|
|
1947
|
+
}
|
|
1948
|
+
const activeEnvs = this.activeEnvironments.get(sandboxId) || [];
|
|
1949
|
+
for (const env of activeEnvs) {
|
|
1950
|
+
await env.publishEffects(effects);
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
/**
|
|
1954
|
+
* Unregister an effect from a sandbox.
|
|
1955
|
+
*
|
|
1956
|
+
* Removes it from the local registry and unpublishes it from
|
|
1957
|
+
* all active environments.
|
|
1958
|
+
*/
|
|
1959
|
+
async unregisterEffect(sandboxNameOrId, name) {
|
|
1960
|
+
const sandbox = await this.findOrCreateSandbox(sandboxNameOrId);
|
|
1961
|
+
const sandboxId = sandbox.sandboxId;
|
|
1962
|
+
const map = this.sandboxEffects.get(sandboxId);
|
|
1963
|
+
if (map) {
|
|
1964
|
+
map.delete(name);
|
|
1965
|
+
}
|
|
1966
|
+
const activeEnvs = this.activeEnvironments.get(sandboxId) || [];
|
|
1967
|
+
for (const env of activeEnvs) {
|
|
1968
|
+
await env.unpublishEffect(name);
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
/**
|
|
1972
|
+
* Unregister all effects for a sandbox.
|
|
1973
|
+
*/
|
|
1974
|
+
async unregisterAllEffects(sandboxNameOrId) {
|
|
1975
|
+
const sandbox = await this.findOrCreateSandbox(sandboxNameOrId);
|
|
1976
|
+
const sandboxId = sandbox.sandboxId;
|
|
1977
|
+
this.sandboxEffects.delete(sandboxId);
|
|
1978
|
+
const activeEnvs = this.activeEnvironments.get(sandboxId) || [];
|
|
1979
|
+
for (const env of activeEnvs) {
|
|
1980
|
+
await env.unpublishAllEffects();
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1576
1983
|
/**
|
|
1577
1984
|
* Find a sandbox by name or create it if it doesn't exist
|
|
1578
1985
|
*/
|