@granular-software/sdk 0.4.1 → 0.4.2
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 +17 -14
- 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 +123 -39
- package/dist/index.d.mts +51 -162
- package/dist/index.d.ts +51 -162
- package/dist/index.js +304 -264
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +304 -264
- package/dist/index.mjs.map +1 -1
- package/dist/{types-BOPsFZYi.d.mts → types-C0AVRsVR.d.mts} +85 -31
- package/dist/{types-BOPsFZYi.d.ts → types-C0AVRsVR.d.ts} +85 -31
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -4466,6 +4466,17 @@ var Session = class {
|
|
|
4466
4466
|
this.setupEventHandlers();
|
|
4467
4467
|
this.setupToolInvokeHandler();
|
|
4468
4468
|
}
|
|
4469
|
+
buildLegacyEffectContext() {
|
|
4470
|
+
return {
|
|
4471
|
+
effectClientId: this.clientId,
|
|
4472
|
+
sandboxId: "",
|
|
4473
|
+
environmentId: "",
|
|
4474
|
+
sessionId: "",
|
|
4475
|
+
user: {
|
|
4476
|
+
subjectId: ""
|
|
4477
|
+
}
|
|
4478
|
+
};
|
|
4479
|
+
}
|
|
4469
4480
|
// --- Public API ---
|
|
4470
4481
|
get document() {
|
|
4471
4482
|
return this.client.doc;
|
|
@@ -4504,155 +4515,30 @@ var Session = class {
|
|
|
4504
4515
|
});
|
|
4505
4516
|
return result;
|
|
4506
4517
|
}
|
|
4507
|
-
/**
|
|
4508
|
-
* Publish tools to the sandbox and register handlers for reverse-RPC.
|
|
4509
|
-
*
|
|
4510
|
-
* Tools can be:
|
|
4511
|
-
* - **Instance methods**: set `className` (handler receives `(objectId, params)`)
|
|
4512
|
-
* - **Static methods**: set `className` + `static: true` (handler receives `(params)`)
|
|
4513
|
-
* - **Global tools**: omit `className` (handler receives `(params)`)
|
|
4514
|
-
*
|
|
4515
|
-
* Both `inputSchema` and `outputSchema` accept JSON Schema objects. The
|
|
4516
|
-
* `outputSchema` drives the return type in the auto-generated TypeScript
|
|
4517
|
-
* class declarations that sandbox code imports from `./sandbox-tools`.
|
|
4518
|
-
*
|
|
4519
|
-
* This method:
|
|
4520
|
-
* 1. Extracts tool schemas (including `outputSchema`) from the provided tools
|
|
4521
|
-
* 2. Publishes them via `client.publishRawToolCatalog` RPC
|
|
4522
|
-
* 3. Registers handlers locally for `tool.invoke` RPC calls
|
|
4523
|
-
* 4. Returns the `domainRevision` needed for job submission
|
|
4524
|
-
*
|
|
4525
|
-
* @param tools - Array of tools with handlers
|
|
4526
|
-
* @param revision - Optional revision string (default: "1.0.0")
|
|
4527
|
-
* @returns PublishToolsResult with domainRevision
|
|
4528
|
-
*
|
|
4529
|
-
* @example
|
|
4530
|
-
* ```typescript
|
|
4531
|
-
* await env.publishTools([
|
|
4532
|
-
* {
|
|
4533
|
-
* name: 'get_bio',
|
|
4534
|
-
* description: 'Get biography of an author',
|
|
4535
|
-
* className: 'author',
|
|
4536
|
-
* inputSchema: { type: 'object', properties: { detailed: { type: 'boolean' } } },
|
|
4537
|
-
* outputSchema: { type: 'object', properties: { bio: { type: 'string' } }, required: ['bio'] },
|
|
4538
|
-
* handler: async (id, params) => ({ bio: `Bio of ${id}` }),
|
|
4539
|
-
* },
|
|
4540
|
-
* ]);
|
|
4541
|
-
* ```
|
|
4542
|
-
*/
|
|
4543
4518
|
async publishTools(tools, revision = "1.0.0") {
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
inputSchema: tool.inputSchema,
|
|
4548
|
-
outputSchema: tool.outputSchema,
|
|
4549
|
-
stability: tool.stability || "stable",
|
|
4550
|
-
provenance: tool.provenance || { source: "mcp" },
|
|
4551
|
-
tags: tool.tags,
|
|
4552
|
-
className: tool.className,
|
|
4553
|
-
static: tool.static
|
|
4554
|
-
}));
|
|
4555
|
-
const result = await this.client.call("client.publishRawToolCatalog", {
|
|
4556
|
-
clientId: this.clientId,
|
|
4557
|
-
revision,
|
|
4558
|
-
tools: schemas
|
|
4559
|
-
});
|
|
4560
|
-
if (!result.accepted || !result.domainRevision) {
|
|
4561
|
-
throw new Error(`Failed to publish tools: ${JSON.stringify(result.rejected)}`);
|
|
4562
|
-
}
|
|
4563
|
-
for (const tool of tools) {
|
|
4564
|
-
this.toolHandlers.set(tool.name, tool.handler);
|
|
4565
|
-
if (tool.className && !tool.static) {
|
|
4566
|
-
this.instanceTools.add(tool.name);
|
|
4567
|
-
} else {
|
|
4568
|
-
this.instanceTools.delete(tool.name);
|
|
4569
|
-
}
|
|
4570
|
-
}
|
|
4571
|
-
this.currentDomainRevision = result.domainRevision;
|
|
4572
|
-
return {
|
|
4573
|
-
accepted: result.accepted,
|
|
4574
|
-
domainRevision: result.domainRevision,
|
|
4575
|
-
rejected: result.rejected
|
|
4576
|
-
};
|
|
4519
|
+
throw new Error(
|
|
4520
|
+
"Environment-scoped effect publication was removed. Declare effects in the manifest and register live handlers with granular.registerEffects(environment.sandboxId, effects)."
|
|
4521
|
+
);
|
|
4577
4522
|
}
|
|
4578
|
-
/**
|
|
4579
|
-
* Publish a single effect (tool) to the sandbox.
|
|
4580
|
-
*
|
|
4581
|
-
* Adds the effect to the local registry and re-publishes the
|
|
4582
|
-
* full tool catalog to the server. If an effect with the same
|
|
4583
|
-
* name already exists, it is replaced.
|
|
4584
|
-
*
|
|
4585
|
-
* @param effect - The effect (tool) to publish
|
|
4586
|
-
* @returns PublishToolsResult with domainRevision
|
|
4587
|
-
*
|
|
4588
|
-
* @example
|
|
4589
|
-
* ```typescript
|
|
4590
|
-
* await env.publishEffect({
|
|
4591
|
-
* name: 'get_bio',
|
|
4592
|
-
* description: 'Get biography of an author',
|
|
4593
|
-
* className: 'author',
|
|
4594
|
-
* inputSchema: { type: 'object', properties: { detailed: { type: 'boolean' } } },
|
|
4595
|
-
* handler: async (id, params) => ({ bio: `Bio of ${id}` }),
|
|
4596
|
-
* });
|
|
4597
|
-
* ```
|
|
4598
|
-
*/
|
|
4599
4523
|
async publishEffect(effect) {
|
|
4600
|
-
|
|
4601
|
-
|
|
4524
|
+
throw new Error(
|
|
4525
|
+
"Environment-scoped effect publication was removed. Use granular.registerEffect(environment.sandboxId, effect)."
|
|
4526
|
+
);
|
|
4602
4527
|
}
|
|
4603
|
-
/**
|
|
4604
|
-
* Publish multiple effects (tools) at once.
|
|
4605
|
-
*
|
|
4606
|
-
* Adds all effects to the local registry and re-publishes the
|
|
4607
|
-
* full tool catalog in a single RPC call.
|
|
4608
|
-
*
|
|
4609
|
-
* @param effects - Array of effects to publish
|
|
4610
|
-
* @returns PublishToolsResult with domainRevision
|
|
4611
|
-
*
|
|
4612
|
-
* @example
|
|
4613
|
-
* ```typescript
|
|
4614
|
-
* await env.publishEffects([
|
|
4615
|
-
* { name: 'get_bio', description: '...', inputSchema: {}, handler: async (id) => ({}) },
|
|
4616
|
-
* { name: 'search', description: '...', inputSchema: {}, handler: async (params) => ({}) },
|
|
4617
|
-
* ]);
|
|
4618
|
-
* ```
|
|
4619
|
-
*/
|
|
4620
4528
|
async publishEffects(effects) {
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
return this._syncEffects();
|
|
4529
|
+
throw new Error(
|
|
4530
|
+
"Environment-scoped effect publication was removed. Use granular.registerEffects(environment.sandboxId, effects)."
|
|
4531
|
+
);
|
|
4625
4532
|
}
|
|
4626
|
-
/**
|
|
4627
|
-
* Remove an effect by name and re-publish the remaining catalog.
|
|
4628
|
-
*
|
|
4629
|
-
* @param name - The name of the effect to remove
|
|
4630
|
-
* @returns PublishToolsResult with domainRevision
|
|
4631
|
-
*/
|
|
4632
4533
|
async unpublishEffect(name) {
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
return this._syncEffects();
|
|
4534
|
+
throw new Error(
|
|
4535
|
+
"Environment-scoped effect publication was removed. Use granular.unregisterEffect(environment.sandboxId, effectName)."
|
|
4536
|
+
);
|
|
4637
4537
|
}
|
|
4638
|
-
/**
|
|
4639
|
-
* Remove all effects and publish an empty catalog.
|
|
4640
|
-
*
|
|
4641
|
-
* @returns PublishToolsResult with domainRevision
|
|
4642
|
-
*/
|
|
4643
4538
|
async unpublishAllEffects() {
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
return this._syncEffects();
|
|
4648
|
-
}
|
|
4649
|
-
/**
|
|
4650
|
-
* Internal: re-publish the full effect catalog to the server.
|
|
4651
|
-
* Called after any mutation to the effects Map.
|
|
4652
|
-
*/
|
|
4653
|
-
async _syncEffects() {
|
|
4654
|
-
const allEffects = Array.from(this.effects.values());
|
|
4655
|
-
return this.publishTools(allEffects);
|
|
4539
|
+
throw new Error(
|
|
4540
|
+
"Environment-scoped effect publication was removed. Use granular.unregisterAllEffects(environment.sandboxId)."
|
|
4541
|
+
);
|
|
4656
4542
|
}
|
|
4657
4543
|
/**
|
|
4658
4544
|
* Submit a job to execute code in the sandbox.
|
|
@@ -4666,14 +4552,14 @@ var Session = class {
|
|
|
4666
4552
|
* const books = await tolkien.get_books();
|
|
4667
4553
|
* ```
|
|
4668
4554
|
*
|
|
4669
|
-
*
|
|
4670
|
-
* `
|
|
4555
|
+
* Effect calls (instance methods, static methods, global functions) trigger
|
|
4556
|
+
* `effect.invoke` RPC back to the sandbox effect host, where the registered handlers
|
|
4671
4557
|
* execute locally and return the result to the sandbox.
|
|
4672
4558
|
*/
|
|
4673
4559
|
async submitJob(code, domainRevision) {
|
|
4674
4560
|
const revision = domainRevision || this.currentDomainRevision || this.client.doc?.domain?.active || void 0;
|
|
4675
4561
|
if (!revision) {
|
|
4676
|
-
throw new Error("No domain revision available.
|
|
4562
|
+
throw new Error("No domain revision available. Register live effects or ensure the build schema is activated.");
|
|
4677
4563
|
}
|
|
4678
4564
|
const result = await this.client.call("job.submit", {
|
|
4679
4565
|
domainRevision: revision,
|
|
@@ -4705,10 +4591,10 @@ var Session = class {
|
|
|
4705
4591
|
await this.client.call("prompt.answer", { promptId, value: answer });
|
|
4706
4592
|
}
|
|
4707
4593
|
/**
|
|
4708
|
-
* Get the current list of available
|
|
4709
|
-
* Consolidates
|
|
4594
|
+
* Get the current list of available effects.
|
|
4595
|
+
* Consolidates effect declarations and live availability for the session.
|
|
4710
4596
|
*/
|
|
4711
|
-
|
|
4597
|
+
getEffects() {
|
|
4712
4598
|
const doc = this.client.doc;
|
|
4713
4599
|
const toolMap = /* @__PURE__ */ new Map();
|
|
4714
4600
|
const domainPkg = doc.domain?.packages?.domain;
|
|
@@ -4753,10 +4639,32 @@ var Session = class {
|
|
|
4753
4639
|
return Array.from(toolMap.values());
|
|
4754
4640
|
}
|
|
4755
4641
|
/**
|
|
4756
|
-
*
|
|
4642
|
+
* Backwards-compatible alias for `getEffects()`.
|
|
4643
|
+
*/
|
|
4644
|
+
getTools() {
|
|
4645
|
+
return this.getEffects();
|
|
4646
|
+
}
|
|
4647
|
+
/**
|
|
4648
|
+
* Subscribe to effect changes (added, removed, updated).
|
|
4757
4649
|
* @param callback - Function called with change events
|
|
4758
4650
|
* @returns Unsubscribe function
|
|
4759
4651
|
*/
|
|
4652
|
+
onEffectsChanged(callback) {
|
|
4653
|
+
const handler = (data) => callback(data);
|
|
4654
|
+
if (!this.eventListeners.has("effects:changed")) {
|
|
4655
|
+
this.eventListeners.set("effects:changed", []);
|
|
4656
|
+
}
|
|
4657
|
+
this.eventListeners.get("effects:changed").push(handler);
|
|
4658
|
+
return () => {
|
|
4659
|
+
const listeners = this.eventListeners.get("effects:changed");
|
|
4660
|
+
if (listeners) {
|
|
4661
|
+
this.eventListeners.set("effects:changed", listeners.filter((h) => h !== handler));
|
|
4662
|
+
}
|
|
4663
|
+
};
|
|
4664
|
+
}
|
|
4665
|
+
/**
|
|
4666
|
+
* Backwards-compatible alias for `onEffectsChanged()`.
|
|
4667
|
+
*/
|
|
4760
4668
|
onToolsChanged(callback) {
|
|
4761
4669
|
const handler = (data) => callback(data);
|
|
4762
4670
|
if (!this.eventListeners.has("tools:changed")) {
|
|
@@ -4869,7 +4777,7 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
4869
4777
|
}
|
|
4870
4778
|
}
|
|
4871
4779
|
if (globalTools && globalTools.length > 0) {
|
|
4872
|
-
docs2 += "## Global
|
|
4780
|
+
docs2 += "## Global Effects\n\n";
|
|
4873
4781
|
for (const tool of globalTools) {
|
|
4874
4782
|
docs2 += `### ${tool.name}
|
|
4875
4783
|
|
|
@@ -4887,10 +4795,10 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
4887
4795
|
return docs2;
|
|
4888
4796
|
}
|
|
4889
4797
|
if (!tools || tools.length === 0) {
|
|
4890
|
-
return "No
|
|
4798
|
+
return "No effects available in this domain.";
|
|
4891
4799
|
}
|
|
4892
|
-
let docs = "# Available
|
|
4893
|
-
docs += "Import
|
|
4800
|
+
let docs = "# Available Effects\n\n";
|
|
4801
|
+
docs += "Import effects from `./sandbox-tools` and call them with await:\n\n";
|
|
4894
4802
|
docs += '```typescript\nimport { tools } from "./sandbox-tools";\n\n';
|
|
4895
4803
|
docs += "// Example:\n";
|
|
4896
4804
|
docs += `const result = await tools.${tools[0]?.name || "example"}(input);
|
|
@@ -4954,6 +4862,7 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
4954
4862
|
setupToolInvokeHandler() {
|
|
4955
4863
|
this.client.registerRpcHandler("tool.invoke", async (params) => {
|
|
4956
4864
|
const { callId, toolName, input } = params;
|
|
4865
|
+
this.emit("effect:invoke", { callId, effectKey: toolName, toolName, input });
|
|
4957
4866
|
this.emit("tool:invoke", { callId, toolName, input });
|
|
4958
4867
|
const handler = this.toolHandlers.get(toolName);
|
|
4959
4868
|
if (!handler) {
|
|
@@ -4965,12 +4874,14 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
4965
4874
|
}
|
|
4966
4875
|
try {
|
|
4967
4876
|
let result;
|
|
4877
|
+
const invocationContext = this.buildLegacyEffectContext();
|
|
4968
4878
|
if (this.instanceTools.has(toolName) && input && typeof input === "object" && "_objectId" in input) {
|
|
4969
4879
|
const { _objectId, ...restParams } = input;
|
|
4970
|
-
result = await handler(_objectId, restParams);
|
|
4880
|
+
result = await handler(_objectId, restParams, invocationContext);
|
|
4971
4881
|
} else {
|
|
4972
|
-
result = await handler(input);
|
|
4882
|
+
result = await handler(input, invocationContext);
|
|
4973
4883
|
}
|
|
4884
|
+
this.emit("effect:result", { callId, effectKey: toolName, result });
|
|
4974
4885
|
this.emit("tool:result", { callId, result });
|
|
4975
4886
|
await this.client.call("tool.result", {
|
|
4976
4887
|
callId,
|
|
@@ -4978,6 +4889,7 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
4978
4889
|
});
|
|
4979
4890
|
} catch (error) {
|
|
4980
4891
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4892
|
+
this.emit("effect:result", { callId, effectKey: toolName, error: errorMessage });
|
|
4981
4893
|
this.emit("tool:result", { callId, error: errorMessage });
|
|
4982
4894
|
await this.client.call("tool.result", {
|
|
4983
4895
|
callId,
|
|
@@ -5010,10 +4922,10 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
5010
4922
|
}
|
|
5011
4923
|
}
|
|
5012
4924
|
/**
|
|
5013
|
-
* Check for changes in the
|
|
4925
|
+
* Check for changes in the effect catalog and emit change events if needed.
|
|
5014
4926
|
*/
|
|
5015
4927
|
checkForToolChanges() {
|
|
5016
|
-
const currentTools = this.
|
|
4928
|
+
const currentTools = this.getEffects();
|
|
5017
4929
|
const currentMap = new Map(currentTools.map((t) => [t.name, t]));
|
|
5018
4930
|
const added = [];
|
|
5019
4931
|
const removed = [];
|
|
@@ -5029,6 +4941,12 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
5029
4941
|
}
|
|
5030
4942
|
if (added.length > 0 || removed.length > 0) {
|
|
5031
4943
|
this.lastKnownTools = currentMap;
|
|
4944
|
+
this.emit("effects:changed", {
|
|
4945
|
+
effects: currentTools,
|
|
4946
|
+
tools: currentTools,
|
|
4947
|
+
added,
|
|
4948
|
+
removed
|
|
4949
|
+
});
|
|
5032
4950
|
this.emit("tools:changed", {
|
|
5033
4951
|
tools: currentTools,
|
|
5034
4952
|
added,
|
|
@@ -5197,6 +5115,37 @@ var STANDARD_MODULES_OPERATIONS = [
|
|
|
5197
5115
|
var BUILTIN_MODULES = {
|
|
5198
5116
|
"standard_modules": STANDARD_MODULES_OPERATIONS
|
|
5199
5117
|
};
|
|
5118
|
+
function computeEffectKey(effect) {
|
|
5119
|
+
const attachedClass = effect.className?.trim();
|
|
5120
|
+
if (!attachedClass) {
|
|
5121
|
+
return `global:${effect.name}`;
|
|
5122
|
+
}
|
|
5123
|
+
return effect.static ? `class:${attachedClass}:static:${effect.name}` : `class:${attachedClass}:instance:${effect.name}`;
|
|
5124
|
+
}
|
|
5125
|
+
function buildEffectHostUrl(apiUrl, sandboxId, effectClientId, clientId) {
|
|
5126
|
+
const url = new URL(apiUrl);
|
|
5127
|
+
if (url.pathname.endsWith("/granular/ws/connect")) {
|
|
5128
|
+
url.pathname = url.pathname.replace(/\/ws\/connect$/, "/effects/connect");
|
|
5129
|
+
} else if (url.pathname.endsWith("/granular")) {
|
|
5130
|
+
url.pathname = `${url.pathname.replace(/\/$/, "")}/effects/connect`;
|
|
5131
|
+
} else if (url.pathname.endsWith("/v2/ws/connect")) {
|
|
5132
|
+
url.pathname = url.pathname.replace(/\/ws\/connect$/, "/effects/connect");
|
|
5133
|
+
} else if (url.pathname.endsWith("/v2/ws")) {
|
|
5134
|
+
url.pathname = url.pathname.replace(/\/ws$/, "/effects/connect");
|
|
5135
|
+
} else if (url.pathname.endsWith("/ws/connect")) {
|
|
5136
|
+
url.pathname = url.pathname.replace(/\/ws\/connect$/, "/effects/connect");
|
|
5137
|
+
} else if (url.pathname.endsWith("/ws")) {
|
|
5138
|
+
url.pathname = url.pathname.replace(/\/ws$/, "/effects/connect");
|
|
5139
|
+
} else {
|
|
5140
|
+
url.pathname = "/granular/effects/connect";
|
|
5141
|
+
}
|
|
5142
|
+
url.search = "";
|
|
5143
|
+
url.hash = "";
|
|
5144
|
+
url.searchParams.set("sandboxId", sandboxId);
|
|
5145
|
+
url.searchParams.set("effectClientId", effectClientId);
|
|
5146
|
+
url.searchParams.set("clientId", clientId);
|
|
5147
|
+
return url.toString();
|
|
5148
|
+
}
|
|
5200
5149
|
var Environment = class _Environment extends Session {
|
|
5201
5150
|
envData;
|
|
5202
5151
|
_apiKey;
|
|
@@ -5941,72 +5890,31 @@ var Environment = class _Environment extends Session {
|
|
|
5941
5890
|
}
|
|
5942
5891
|
// ==================== PUBLISH TOOLS ====================
|
|
5943
5892
|
/**
|
|
5944
|
-
*
|
|
5945
|
-
* This is the main entry point for setting up tools.
|
|
5946
|
-
*
|
|
5947
|
-
* @example
|
|
5948
|
-
* ```typescript
|
|
5949
|
-
* const tools = [
|
|
5950
|
-
* {
|
|
5951
|
-
* name: 'get_weather',
|
|
5952
|
-
* description: 'Get weather for a city',
|
|
5953
|
-
* inputSchema: { type: 'object', properties: { city: { type: 'string' } } },
|
|
5954
|
-
* handler: async ({ city }) => ({ temp: 22 }),
|
|
5955
|
-
* },
|
|
5956
|
-
* ];
|
|
5957
|
-
*
|
|
5958
|
-
* const { domainRevision } = await environment.publishTools(tools);
|
|
5959
|
-
*
|
|
5960
|
-
* // Now submit jobs that use those tools
|
|
5961
|
-
* const job = await environment.submitJob(`
|
|
5962
|
-
* import { Author } from './sandbox-tools';
|
|
5963
|
-
* const weather = await Author.get_weather({ city: 'Paris' });
|
|
5964
|
-
* return weather;
|
|
5965
|
-
* `);
|
|
5966
|
-
*
|
|
5967
|
-
* const result = await job.result;
|
|
5968
|
-
* ```
|
|
5893
|
+
* Removed: environment-scoped effect publication is no longer supported.
|
|
5969
5894
|
*/
|
|
5970
5895
|
async publishTools(tools, revision = "1.0.0") {
|
|
5971
5896
|
return super.publishTools(tools, revision);
|
|
5972
5897
|
}
|
|
5973
5898
|
/**
|
|
5974
|
-
*
|
|
5975
|
-
*
|
|
5976
|
-
* Adds the effect to the local registry and re-publishes the
|
|
5977
|
-
* full tool catalog to the server. If an effect with the same
|
|
5978
|
-
* name already exists, it is replaced.
|
|
5979
|
-
*
|
|
5980
|
-
* @example
|
|
5981
|
-
* ```typescript
|
|
5982
|
-
* await env.publishEffect({
|
|
5983
|
-
* name: 'get_bio',
|
|
5984
|
-
* description: 'Get biography',
|
|
5985
|
-
* inputSchema: { type: 'object', properties: { detailed: { type: 'boolean' } } },
|
|
5986
|
-
* handler: async (params) => ({ bio: 'Hello' }),
|
|
5987
|
-
* });
|
|
5988
|
-
* ```
|
|
5899
|
+
* Removed: environment-scoped effect publication is no longer supported.
|
|
5989
5900
|
*/
|
|
5990
5901
|
async publishEffect(effect) {
|
|
5991
5902
|
return super.publishEffect(effect);
|
|
5992
5903
|
}
|
|
5993
5904
|
/**
|
|
5994
|
-
*
|
|
5995
|
-
*
|
|
5996
|
-
* Adds all effects to the local registry and re-publishes the
|
|
5997
|
-
* full tool catalog in a single RPC call.
|
|
5905
|
+
* Removed: environment-scoped effect publication is no longer supported.
|
|
5998
5906
|
*/
|
|
5999
5907
|
async publishEffects(effects) {
|
|
6000
5908
|
return super.publishEffects(effects);
|
|
6001
5909
|
}
|
|
6002
5910
|
/**
|
|
6003
|
-
*
|
|
5911
|
+
* Removed: environment-scoped effect publication is no longer supported.
|
|
6004
5912
|
*/
|
|
6005
5913
|
async unpublishEffect(name) {
|
|
6006
5914
|
return super.unpublishEffect(name);
|
|
6007
5915
|
}
|
|
6008
5916
|
/**
|
|
6009
|
-
*
|
|
5917
|
+
* Removed: environment-scoped effect publication is no longer supported.
|
|
6010
5918
|
*/
|
|
6011
5919
|
async unpublishAllEffects() {
|
|
6012
5920
|
return super.unpublishAllEffects();
|
|
@@ -6020,10 +5928,12 @@ var Granular = class {
|
|
|
6020
5928
|
WebSocketCtor;
|
|
6021
5929
|
onUnexpectedClose;
|
|
6022
5930
|
onReconnectError;
|
|
6023
|
-
/** Sandbox-level effect registry: sandboxId → (
|
|
5931
|
+
/** Sandbox-level effect registry: sandboxId → (effectKey → ToolWithHandler) */
|
|
6024
5932
|
sandboxEffects = /* @__PURE__ */ new Map();
|
|
6025
|
-
/**
|
|
6026
|
-
|
|
5933
|
+
/** Live sandbox-scoped effect hosts keyed by sandboxId */
|
|
5934
|
+
sandboxEffectHosts = /* @__PURE__ */ new Map();
|
|
5935
|
+
/** In-flight host connection promises to avoid duplicate concurrent connects */
|
|
5936
|
+
sandboxEffectHostPromises = /* @__PURE__ */ new Map();
|
|
6027
5937
|
/**
|
|
6028
5938
|
* Create a new Granular client
|
|
6029
5939
|
* @param options - Client configuration
|
|
@@ -6076,8 +5986,9 @@ var Granular = class {
|
|
|
6076
5986
|
/**
|
|
6077
5987
|
* Connect to a sandbox and establish a real-time environment session.
|
|
6078
5988
|
*
|
|
6079
|
-
*
|
|
6080
|
-
*
|
|
5989
|
+
* Effects are registered at the sandbox level via `granular.registerEffect()`
|
|
5990
|
+
* or `granular.registerEffects()`. Sessions pick up live availability from
|
|
5991
|
+
* the sandbox registry automatically.
|
|
6081
5992
|
*
|
|
6082
5993
|
* @param options - Connection options
|
|
6083
5994
|
* @returns An active environment session
|
|
@@ -6094,10 +6005,12 @@ var Granular = class {
|
|
|
6094
6005
|
* user,
|
|
6095
6006
|
* });
|
|
6096
6007
|
*
|
|
6097
|
-
*
|
|
6098
|
-
*
|
|
6099
|
-
*
|
|
6100
|
-
*
|
|
6008
|
+
* await granular.registerEffect('my-sandbox', {
|
|
6009
|
+
* name: 'greet',
|
|
6010
|
+
* description: 'Say hello',
|
|
6011
|
+
* inputSchema: { type: 'object', properties: {} },
|
|
6012
|
+
* handler: async () => 'Hello!',
|
|
6013
|
+
* });
|
|
6101
6014
|
*
|
|
6102
6015
|
* // Submit job
|
|
6103
6016
|
* const job = await environment.submitJob(`
|
|
@@ -6119,10 +6032,18 @@ var Granular = class {
|
|
|
6119
6032
|
subjectId: options.user.subjectId,
|
|
6120
6033
|
permissionProfileId: null
|
|
6121
6034
|
});
|
|
6035
|
+
await this.activateEnvironment(envData.environmentId);
|
|
6036
|
+
const session = await this.request("/ws/sessions", {
|
|
6037
|
+
method: "POST",
|
|
6038
|
+
body: JSON.stringify({
|
|
6039
|
+
environmentId: envData.environmentId,
|
|
6040
|
+
clientId
|
|
6041
|
+
})
|
|
6042
|
+
});
|
|
6122
6043
|
const client = new WSClient({
|
|
6123
|
-
url:
|
|
6124
|
-
sessionId:
|
|
6125
|
-
token:
|
|
6044
|
+
url: session.wsUrl,
|
|
6045
|
+
sessionId: session.sessionId,
|
|
6046
|
+
token: session.token,
|
|
6126
6047
|
tokenProvider: this.tokenProvider,
|
|
6127
6048
|
WebSocketCtor: this.WebSocketCtor,
|
|
6128
6049
|
onUnexpectedClose: this.onUnexpectedClose,
|
|
@@ -6131,47 +6052,165 @@ var Granular = class {
|
|
|
6131
6052
|
await client.connect();
|
|
6132
6053
|
const graphqlEndpoint = `${this.httpUrl}/orchestrator/graphql`;
|
|
6133
6054
|
const environment = new Environment(client, envData, clientId, this.apiKey, graphqlEndpoint);
|
|
6134
|
-
if (!this.activeEnvironments.has(sandbox.sandboxId)) {
|
|
6135
|
-
this.activeEnvironments.set(sandbox.sandboxId, []);
|
|
6136
|
-
}
|
|
6137
|
-
this.activeEnvironments.get(sandbox.sandboxId).push(environment);
|
|
6138
|
-
environment.on("disconnect", () => {
|
|
6139
|
-
const list = this.activeEnvironments.get(sandbox.sandboxId);
|
|
6140
|
-
if (list) {
|
|
6141
|
-
this.activeEnvironments.set(sandbox.sandboxId, list.filter((e) => e !== environment));
|
|
6142
|
-
}
|
|
6143
|
-
});
|
|
6144
6055
|
await environment.hello();
|
|
6145
|
-
const effects = this.sandboxEffects.get(sandbox.sandboxId);
|
|
6146
|
-
if (effects && effects.size > 0) {
|
|
6147
|
-
const effectsList = Array.from(effects.values());
|
|
6148
|
-
console.log(`[Granular] Auto-publishing ${effectsList.length} effects for sandbox ${sandbox.sandboxId}`);
|
|
6149
|
-
await environment.publishEffects(effectsList);
|
|
6150
|
-
}
|
|
6151
6056
|
return environment;
|
|
6152
6057
|
}
|
|
6058
|
+
async activateEnvironment(environmentId) {
|
|
6059
|
+
await this.request(`/orchestrator/runtime/environments/${environmentId}/activate`, {
|
|
6060
|
+
method: "POST",
|
|
6061
|
+
body: JSON.stringify({})
|
|
6062
|
+
});
|
|
6063
|
+
}
|
|
6153
6064
|
// ── Sandbox-Level Effects ──
|
|
6065
|
+
getSandboxEffectMap(sandboxId) {
|
|
6066
|
+
let effects = this.sandboxEffects.get(sandboxId);
|
|
6067
|
+
if (!effects) {
|
|
6068
|
+
effects = /* @__PURE__ */ new Map();
|
|
6069
|
+
this.sandboxEffects.set(sandboxId, effects);
|
|
6070
|
+
}
|
|
6071
|
+
return effects;
|
|
6072
|
+
}
|
|
6073
|
+
serializeEffect(effect) {
|
|
6074
|
+
return {
|
|
6075
|
+
effectKey: computeEffectKey(effect),
|
|
6076
|
+
name: effect.name,
|
|
6077
|
+
description: effect.description,
|
|
6078
|
+
inputSchema: effect.inputSchema,
|
|
6079
|
+
outputSchema: effect.outputSchema,
|
|
6080
|
+
stability: effect.stability || "stable",
|
|
6081
|
+
provenance: effect.provenance || { source: "custom" },
|
|
6082
|
+
tags: effect.tags,
|
|
6083
|
+
className: effect.className,
|
|
6084
|
+
static: effect.static
|
|
6085
|
+
};
|
|
6086
|
+
}
|
|
6087
|
+
async publishSandboxEffectCatalog(host) {
|
|
6088
|
+
const effects = Array.from(this.getSandboxEffectMap(host.sandboxId).values()).map(
|
|
6089
|
+
(effect) => this.serializeEffect(effect)
|
|
6090
|
+
);
|
|
6091
|
+
await host.wsClient.call("effects.publishCatalog", { effects });
|
|
6092
|
+
}
|
|
6093
|
+
async syncSandboxEffectCatalog(sandboxId) {
|
|
6094
|
+
const host = await this.ensureSandboxEffectHost(sandboxId);
|
|
6095
|
+
await this.publishSandboxEffectCatalog(host);
|
|
6096
|
+
}
|
|
6097
|
+
startEffectHostHeartbeat(host) {
|
|
6098
|
+
if (host.heartbeatTimer) {
|
|
6099
|
+
clearInterval(host.heartbeatTimer);
|
|
6100
|
+
}
|
|
6101
|
+
host.wsClient.call("client.heartbeat", {}).catch((error) => {
|
|
6102
|
+
console.warn(
|
|
6103
|
+
`[Granular] Initial effect host heartbeat failed for sandbox ${host.sandboxId}:`,
|
|
6104
|
+
error
|
|
6105
|
+
);
|
|
6106
|
+
});
|
|
6107
|
+
host.heartbeatTimer = setInterval(() => {
|
|
6108
|
+
host.wsClient.call("client.heartbeat", {}).catch((error) => {
|
|
6109
|
+
console.warn(
|
|
6110
|
+
`[Granular] Effect host heartbeat failed for sandbox ${host.sandboxId}:`,
|
|
6111
|
+
error
|
|
6112
|
+
);
|
|
6113
|
+
});
|
|
6114
|
+
}, 1e4);
|
|
6115
|
+
}
|
|
6116
|
+
stopEffectHostHeartbeat(host) {
|
|
6117
|
+
if (!host?.heartbeatTimer) {
|
|
6118
|
+
return;
|
|
6119
|
+
}
|
|
6120
|
+
clearInterval(host.heartbeatTimer);
|
|
6121
|
+
host.heartbeatTimer = null;
|
|
6122
|
+
}
|
|
6123
|
+
async synchronizeEffectHost(host) {
|
|
6124
|
+
await host.wsClient.call("client.hello", {
|
|
6125
|
+
clientId: host.clientId,
|
|
6126
|
+
protocolVersion: "2.0"
|
|
6127
|
+
});
|
|
6128
|
+
this.startEffectHostHeartbeat(host);
|
|
6129
|
+
await this.publishSandboxEffectCatalog(host);
|
|
6130
|
+
}
|
|
6131
|
+
async ensureSandboxEffectHost(sandboxId) {
|
|
6132
|
+
const existing = this.sandboxEffectHosts.get(sandboxId);
|
|
6133
|
+
if (existing) {
|
|
6134
|
+
return existing;
|
|
6135
|
+
}
|
|
6136
|
+
const inflight = this.sandboxEffectHostPromises.get(sandboxId);
|
|
6137
|
+
if (inflight) {
|
|
6138
|
+
return inflight;
|
|
6139
|
+
}
|
|
6140
|
+
const connectPromise = (async () => {
|
|
6141
|
+
const effectClientId = crypto.randomUUID();
|
|
6142
|
+
const clientId = `effect-host:${sandboxId}:${effectClientId}`;
|
|
6143
|
+
const wsClient = new WSClient({
|
|
6144
|
+
url: buildEffectHostUrl(this.apiUrl, sandboxId, effectClientId, clientId),
|
|
6145
|
+
sessionId: `effect-host:${effectClientId}`,
|
|
6146
|
+
token: this.apiKey,
|
|
6147
|
+
tokenProvider: this.tokenProvider,
|
|
6148
|
+
WebSocketCtor: this.WebSocketCtor,
|
|
6149
|
+
onUnexpectedClose: this.onUnexpectedClose,
|
|
6150
|
+
onReconnectError: this.onReconnectError
|
|
6151
|
+
});
|
|
6152
|
+
const host = {
|
|
6153
|
+
sandboxId,
|
|
6154
|
+
effectClientId,
|
|
6155
|
+
clientId,
|
|
6156
|
+
wsClient,
|
|
6157
|
+
heartbeatTimer: null
|
|
6158
|
+
};
|
|
6159
|
+
wsClient.registerRpcHandler("effect.invoke", async (params) => {
|
|
6160
|
+
const request = params;
|
|
6161
|
+
const effect = this.getSandboxEffectMap(sandboxId).get(request.effectKey);
|
|
6162
|
+
if (!effect) {
|
|
6163
|
+
throw new Error(`Effect handler not found: ${request.effectKey}`);
|
|
6164
|
+
}
|
|
6165
|
+
if (effect.className && !effect.static && request.input && typeof request.input === "object" && "_objectId" in request.input) {
|
|
6166
|
+
const { _objectId, ...rest } = request.input;
|
|
6167
|
+
return effect.handler(_objectId, rest, request.context);
|
|
6168
|
+
}
|
|
6169
|
+
return effect.handler(request.input, request.context);
|
|
6170
|
+
});
|
|
6171
|
+
wsClient.on("open", () => {
|
|
6172
|
+
void this.synchronizeEffectHost(host).catch((error) => {
|
|
6173
|
+
console.error(
|
|
6174
|
+
`[Granular] Failed to re-sync effect host for sandbox ${sandboxId}:`,
|
|
6175
|
+
error
|
|
6176
|
+
);
|
|
6177
|
+
});
|
|
6178
|
+
});
|
|
6179
|
+
wsClient.on("disconnect", () => {
|
|
6180
|
+
this.stopEffectHostHeartbeat(host);
|
|
6181
|
+
});
|
|
6182
|
+
await wsClient.connect();
|
|
6183
|
+
await this.synchronizeEffectHost(host);
|
|
6184
|
+
this.sandboxEffectHosts.set(sandboxId, host);
|
|
6185
|
+
return host;
|
|
6186
|
+
})();
|
|
6187
|
+
this.sandboxEffectHostPromises.set(sandboxId, connectPromise);
|
|
6188
|
+
try {
|
|
6189
|
+
return await connectPromise;
|
|
6190
|
+
} finally {
|
|
6191
|
+
this.sandboxEffectHostPromises.delete(sandboxId);
|
|
6192
|
+
}
|
|
6193
|
+
}
|
|
6194
|
+
disconnectSandboxEffectHost(sandboxId) {
|
|
6195
|
+
const host = this.sandboxEffectHosts.get(sandboxId);
|
|
6196
|
+
if (!host) {
|
|
6197
|
+
return;
|
|
6198
|
+
}
|
|
6199
|
+
this.stopEffectHostHeartbeat(host);
|
|
6200
|
+
host.wsClient.disconnect();
|
|
6201
|
+
this.sandboxEffectHosts.delete(sandboxId);
|
|
6202
|
+
}
|
|
6154
6203
|
/**
|
|
6155
6204
|
* Register an effect (tool) for a specific sandbox.
|
|
6156
|
-
*
|
|
6157
|
-
* The effect will be automatically published to:
|
|
6158
|
-
* 1. Any currently active environments for this sandbox
|
|
6159
|
-
* 2. Any new environments created/connected for this sandbox
|
|
6160
|
-
*
|
|
6205
|
+
*
|
|
6161
6206
|
* @param sandboxNameOrId - The name or ID of the sandbox
|
|
6162
6207
|
* @param effect - The tool definition and handler
|
|
6163
6208
|
*/
|
|
6164
6209
|
async registerEffect(sandboxNameOrId, effect) {
|
|
6165
6210
|
const sandbox = await this.findOrCreateSandbox(sandboxNameOrId);
|
|
6166
6211
|
const sandboxId = sandbox.sandboxId;
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
}
|
|
6170
|
-
this.sandboxEffects.get(sandboxId).set(effect.name, effect);
|
|
6171
|
-
const activeEnvs = this.activeEnvironments.get(sandboxId) || [];
|
|
6172
|
-
for (const env of activeEnvs) {
|
|
6173
|
-
await env.publishEffect(effect);
|
|
6174
|
-
}
|
|
6212
|
+
this.getSandboxEffectMap(sandboxId).set(computeEffectKey(effect), effect);
|
|
6213
|
+
await this.syncSandboxEffectCatalog(sandboxId);
|
|
6175
6214
|
}
|
|
6176
6215
|
/**
|
|
6177
6216
|
* Register multiple effects (tools) for a specific sandbox.
|
|
@@ -6181,35 +6220,39 @@ var Granular = class {
|
|
|
6181
6220
|
async registerEffects(sandboxNameOrId, effects) {
|
|
6182
6221
|
const sandbox = await this.findOrCreateSandbox(sandboxNameOrId);
|
|
6183
6222
|
const sandboxId = sandbox.sandboxId;
|
|
6184
|
-
|
|
6185
|
-
this.sandboxEffects.set(sandboxId, /* @__PURE__ */ new Map());
|
|
6186
|
-
}
|
|
6187
|
-
const map = this.sandboxEffects.get(sandboxId);
|
|
6223
|
+
const map = this.getSandboxEffectMap(sandboxId);
|
|
6188
6224
|
for (const effect of effects) {
|
|
6189
|
-
map.set(effect
|
|
6190
|
-
}
|
|
6191
|
-
const activeEnvs = this.activeEnvironments.get(sandboxId) || [];
|
|
6192
|
-
for (const env of activeEnvs) {
|
|
6193
|
-
await env.publishEffects(effects);
|
|
6225
|
+
map.set(computeEffectKey(effect), effect);
|
|
6194
6226
|
}
|
|
6227
|
+
await this.syncSandboxEffectCatalog(sandboxId);
|
|
6195
6228
|
}
|
|
6196
6229
|
/**
|
|
6197
6230
|
* Unregister an effect from a sandbox.
|
|
6198
6231
|
*
|
|
6199
|
-
* Removes it from the local registry and
|
|
6200
|
-
*
|
|
6232
|
+
* Removes it from the local sandbox registry and updates the
|
|
6233
|
+
* sandbox-scoped live catalog.
|
|
6201
6234
|
*/
|
|
6202
6235
|
async unregisterEffect(sandboxNameOrId, name) {
|
|
6203
6236
|
const sandbox = await this.findOrCreateSandbox(sandboxNameOrId);
|
|
6204
6237
|
const sandboxId = sandbox.sandboxId;
|
|
6205
|
-
const
|
|
6206
|
-
if (
|
|
6207
|
-
|
|
6238
|
+
const currentMap = this.sandboxEffects.get(sandboxId);
|
|
6239
|
+
if (!currentMap) {
|
|
6240
|
+
return;
|
|
6208
6241
|
}
|
|
6209
|
-
const
|
|
6210
|
-
|
|
6211
|
-
|
|
6242
|
+
const nextEntries = Array.from(currentMap.entries()).filter(
|
|
6243
|
+
([effectKey, effect]) => effectKey !== name && effect.name !== name
|
|
6244
|
+
);
|
|
6245
|
+
if (nextEntries.length === currentMap.size) {
|
|
6246
|
+
return;
|
|
6247
|
+
}
|
|
6248
|
+
const nextMap = new Map(nextEntries);
|
|
6249
|
+
this.sandboxEffects.set(sandboxId, nextMap);
|
|
6250
|
+
if (nextMap.size === 0) {
|
|
6251
|
+
this.disconnectSandboxEffectHost(sandboxId);
|
|
6252
|
+
this.sandboxEffects.delete(sandboxId);
|
|
6253
|
+
return;
|
|
6212
6254
|
}
|
|
6255
|
+
await this.syncSandboxEffectCatalog(sandboxId);
|
|
6213
6256
|
}
|
|
6214
6257
|
/**
|
|
6215
6258
|
* Unregister all effects for a sandbox.
|
|
@@ -6218,10 +6261,7 @@ var Granular = class {
|
|
|
6218
6261
|
const sandbox = await this.findOrCreateSandbox(sandboxNameOrId);
|
|
6219
6262
|
const sandboxId = sandbox.sandboxId;
|
|
6220
6263
|
this.sandboxEffects.delete(sandboxId);
|
|
6221
|
-
|
|
6222
|
-
for (const env of activeEnvs) {
|
|
6223
|
-
await env.unpublishAllEffects();
|
|
6224
|
-
}
|
|
6264
|
+
this.disconnectSandboxEffectHost(sandboxId);
|
|
6225
6265
|
}
|
|
6226
6266
|
/**
|
|
6227
6267
|
* Find a sandbox by name or create it if it doesn't exist
|
|
@@ -6257,7 +6297,7 @@ var Granular = class {
|
|
|
6257
6297
|
const created = await this.permissionProfiles.create(sandboxId, {
|
|
6258
6298
|
name: profileName,
|
|
6259
6299
|
rules: {
|
|
6260
|
-
|
|
6300
|
+
effects: { allow: ["*"] },
|
|
6261
6301
|
resources: { allow: ["*"] }
|
|
6262
6302
|
}
|
|
6263
6303
|
});
|