@granular-software/sdk 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -15
- 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 +137 -46
- package/dist/index.d.mts +51 -162
- package/dist/index.d.ts +51 -162
- package/dist/index.js +305 -265
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +305 -265
- 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.js
CHANGED
|
@@ -4488,6 +4488,17 @@ var Session = class {
|
|
|
4488
4488
|
this.setupEventHandlers();
|
|
4489
4489
|
this.setupToolInvokeHandler();
|
|
4490
4490
|
}
|
|
4491
|
+
buildLegacyEffectContext() {
|
|
4492
|
+
return {
|
|
4493
|
+
effectClientId: this.clientId,
|
|
4494
|
+
sandboxId: "",
|
|
4495
|
+
environmentId: "",
|
|
4496
|
+
sessionId: "",
|
|
4497
|
+
user: {
|
|
4498
|
+
subjectId: ""
|
|
4499
|
+
}
|
|
4500
|
+
};
|
|
4501
|
+
}
|
|
4491
4502
|
// --- Public API ---
|
|
4492
4503
|
get document() {
|
|
4493
4504
|
return this.client.doc;
|
|
@@ -4526,155 +4537,30 @@ var Session = class {
|
|
|
4526
4537
|
});
|
|
4527
4538
|
return result;
|
|
4528
4539
|
}
|
|
4529
|
-
/**
|
|
4530
|
-
* Publish tools to the sandbox and register handlers for reverse-RPC.
|
|
4531
|
-
*
|
|
4532
|
-
* Tools can be:
|
|
4533
|
-
* - **Instance methods**: set `className` (handler receives `(objectId, params)`)
|
|
4534
|
-
* - **Static methods**: set `className` + `static: true` (handler receives `(params)`)
|
|
4535
|
-
* - **Global tools**: omit `className` (handler receives `(params)`)
|
|
4536
|
-
*
|
|
4537
|
-
* Both `inputSchema` and `outputSchema` accept JSON Schema objects. The
|
|
4538
|
-
* `outputSchema` drives the return type in the auto-generated TypeScript
|
|
4539
|
-
* class declarations that sandbox code imports from `./sandbox-tools`.
|
|
4540
|
-
*
|
|
4541
|
-
* This method:
|
|
4542
|
-
* 1. Extracts tool schemas (including `outputSchema`) from the provided tools
|
|
4543
|
-
* 2. Publishes them via `client.publishRawToolCatalog` RPC
|
|
4544
|
-
* 3. Registers handlers locally for `tool.invoke` RPC calls
|
|
4545
|
-
* 4. Returns the `domainRevision` needed for job submission
|
|
4546
|
-
*
|
|
4547
|
-
* @param tools - Array of tools with handlers
|
|
4548
|
-
* @param revision - Optional revision string (default: "1.0.0")
|
|
4549
|
-
* @returns PublishToolsResult with domainRevision
|
|
4550
|
-
*
|
|
4551
|
-
* @example
|
|
4552
|
-
* ```typescript
|
|
4553
|
-
* await env.publishTools([
|
|
4554
|
-
* {
|
|
4555
|
-
* name: 'get_bio',
|
|
4556
|
-
* description: 'Get biography of an author',
|
|
4557
|
-
* className: 'author',
|
|
4558
|
-
* inputSchema: { type: 'object', properties: { detailed: { type: 'boolean' } } },
|
|
4559
|
-
* outputSchema: { type: 'object', properties: { bio: { type: 'string' } }, required: ['bio'] },
|
|
4560
|
-
* handler: async (id, params) => ({ bio: `Bio of ${id}` }),
|
|
4561
|
-
* },
|
|
4562
|
-
* ]);
|
|
4563
|
-
* ```
|
|
4564
|
-
*/
|
|
4565
4540
|
async publishTools(tools, revision = "1.0.0") {
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
inputSchema: tool.inputSchema,
|
|
4570
|
-
outputSchema: tool.outputSchema,
|
|
4571
|
-
stability: tool.stability || "stable",
|
|
4572
|
-
provenance: tool.provenance || { source: "mcp" },
|
|
4573
|
-
tags: tool.tags,
|
|
4574
|
-
className: tool.className,
|
|
4575
|
-
static: tool.static
|
|
4576
|
-
}));
|
|
4577
|
-
const result = await this.client.call("client.publishRawToolCatalog", {
|
|
4578
|
-
clientId: this.clientId,
|
|
4579
|
-
revision,
|
|
4580
|
-
tools: schemas
|
|
4581
|
-
});
|
|
4582
|
-
if (!result.accepted || !result.domainRevision) {
|
|
4583
|
-
throw new Error(`Failed to publish tools: ${JSON.stringify(result.rejected)}`);
|
|
4584
|
-
}
|
|
4585
|
-
for (const tool of tools) {
|
|
4586
|
-
this.toolHandlers.set(tool.name, tool.handler);
|
|
4587
|
-
if (tool.className && !tool.static) {
|
|
4588
|
-
this.instanceTools.add(tool.name);
|
|
4589
|
-
} else {
|
|
4590
|
-
this.instanceTools.delete(tool.name);
|
|
4591
|
-
}
|
|
4592
|
-
}
|
|
4593
|
-
this.currentDomainRevision = result.domainRevision;
|
|
4594
|
-
return {
|
|
4595
|
-
accepted: result.accepted,
|
|
4596
|
-
domainRevision: result.domainRevision,
|
|
4597
|
-
rejected: result.rejected
|
|
4598
|
-
};
|
|
4541
|
+
throw new Error(
|
|
4542
|
+
"Environment-scoped effect publication was removed. Declare effects in the manifest and register live handlers with granular.registerEffects(environment.sandboxId, effects)."
|
|
4543
|
+
);
|
|
4599
4544
|
}
|
|
4600
|
-
/**
|
|
4601
|
-
* Publish a single effect (tool) to the sandbox.
|
|
4602
|
-
*
|
|
4603
|
-
* Adds the effect to the local registry and re-publishes the
|
|
4604
|
-
* full tool catalog to the server. If an effect with the same
|
|
4605
|
-
* name already exists, it is replaced.
|
|
4606
|
-
*
|
|
4607
|
-
* @param effect - The effect (tool) to publish
|
|
4608
|
-
* @returns PublishToolsResult with domainRevision
|
|
4609
|
-
*
|
|
4610
|
-
* @example
|
|
4611
|
-
* ```typescript
|
|
4612
|
-
* await env.publishEffect({
|
|
4613
|
-
* name: 'get_bio',
|
|
4614
|
-
* description: 'Get biography of an author',
|
|
4615
|
-
* className: 'author',
|
|
4616
|
-
* inputSchema: { type: 'object', properties: { detailed: { type: 'boolean' } } },
|
|
4617
|
-
* handler: async (id, params) => ({ bio: `Bio of ${id}` }),
|
|
4618
|
-
* });
|
|
4619
|
-
* ```
|
|
4620
|
-
*/
|
|
4621
4545
|
async publishEffect(effect) {
|
|
4622
|
-
|
|
4623
|
-
|
|
4546
|
+
throw new Error(
|
|
4547
|
+
"Environment-scoped effect publication was removed. Use granular.registerEffect(environment.sandboxId, effect)."
|
|
4548
|
+
);
|
|
4624
4549
|
}
|
|
4625
|
-
/**
|
|
4626
|
-
* Publish multiple effects (tools) at once.
|
|
4627
|
-
*
|
|
4628
|
-
* Adds all effects to the local registry and re-publishes the
|
|
4629
|
-
* full tool catalog in a single RPC call.
|
|
4630
|
-
*
|
|
4631
|
-
* @param effects - Array of effects to publish
|
|
4632
|
-
* @returns PublishToolsResult with domainRevision
|
|
4633
|
-
*
|
|
4634
|
-
* @example
|
|
4635
|
-
* ```typescript
|
|
4636
|
-
* await env.publishEffects([
|
|
4637
|
-
* { name: 'get_bio', description: '...', inputSchema: {}, handler: async (id) => ({}) },
|
|
4638
|
-
* { name: 'search', description: '...', inputSchema: {}, handler: async (params) => ({}) },
|
|
4639
|
-
* ]);
|
|
4640
|
-
* ```
|
|
4641
|
-
*/
|
|
4642
4550
|
async publishEffects(effects) {
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
return this._syncEffects();
|
|
4551
|
+
throw new Error(
|
|
4552
|
+
"Environment-scoped effect publication was removed. Use granular.registerEffects(environment.sandboxId, effects)."
|
|
4553
|
+
);
|
|
4647
4554
|
}
|
|
4648
|
-
/**
|
|
4649
|
-
* Remove an effect by name and re-publish the remaining catalog.
|
|
4650
|
-
*
|
|
4651
|
-
* @param name - The name of the effect to remove
|
|
4652
|
-
* @returns PublishToolsResult with domainRevision
|
|
4653
|
-
*/
|
|
4654
4555
|
async unpublishEffect(name) {
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
return this._syncEffects();
|
|
4556
|
+
throw new Error(
|
|
4557
|
+
"Environment-scoped effect publication was removed. Use granular.unregisterEffect(environment.sandboxId, effectName)."
|
|
4558
|
+
);
|
|
4659
4559
|
}
|
|
4660
|
-
/**
|
|
4661
|
-
* Remove all effects and publish an empty catalog.
|
|
4662
|
-
*
|
|
4663
|
-
* @returns PublishToolsResult with domainRevision
|
|
4664
|
-
*/
|
|
4665
4560
|
async unpublishAllEffects() {
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
return this._syncEffects();
|
|
4670
|
-
}
|
|
4671
|
-
/**
|
|
4672
|
-
* Internal: re-publish the full effect catalog to the server.
|
|
4673
|
-
* Called after any mutation to the effects Map.
|
|
4674
|
-
*/
|
|
4675
|
-
async _syncEffects() {
|
|
4676
|
-
const allEffects = Array.from(this.effects.values());
|
|
4677
|
-
return this.publishTools(allEffects);
|
|
4561
|
+
throw new Error(
|
|
4562
|
+
"Environment-scoped effect publication was removed. Use granular.unregisterAllEffects(environment.sandboxId)."
|
|
4563
|
+
);
|
|
4678
4564
|
}
|
|
4679
4565
|
/**
|
|
4680
4566
|
* Submit a job to execute code in the sandbox.
|
|
@@ -4688,14 +4574,14 @@ var Session = class {
|
|
|
4688
4574
|
* const books = await tolkien.get_books();
|
|
4689
4575
|
* ```
|
|
4690
4576
|
*
|
|
4691
|
-
*
|
|
4692
|
-
* `
|
|
4577
|
+
* Effect calls (instance methods, static methods, global functions) trigger
|
|
4578
|
+
* `effect.invoke` RPC back to the sandbox effect host, where the registered handlers
|
|
4693
4579
|
* execute locally and return the result to the sandbox.
|
|
4694
4580
|
*/
|
|
4695
4581
|
async submitJob(code, domainRevision) {
|
|
4696
4582
|
const revision = domainRevision || this.currentDomainRevision || this.client.doc?.domain?.active || void 0;
|
|
4697
4583
|
if (!revision) {
|
|
4698
|
-
throw new Error("No domain revision available.
|
|
4584
|
+
throw new Error("No domain revision available. Register live effects or ensure the build schema is activated.");
|
|
4699
4585
|
}
|
|
4700
4586
|
const result = await this.client.call("job.submit", {
|
|
4701
4587
|
domainRevision: revision,
|
|
@@ -4727,10 +4613,10 @@ var Session = class {
|
|
|
4727
4613
|
await this.client.call("prompt.answer", { promptId, value: answer });
|
|
4728
4614
|
}
|
|
4729
4615
|
/**
|
|
4730
|
-
* Get the current list of available
|
|
4731
|
-
* Consolidates
|
|
4616
|
+
* Get the current list of available effects.
|
|
4617
|
+
* Consolidates effect declarations and live availability for the session.
|
|
4732
4618
|
*/
|
|
4733
|
-
|
|
4619
|
+
getEffects() {
|
|
4734
4620
|
const doc = this.client.doc;
|
|
4735
4621
|
const toolMap = /* @__PURE__ */ new Map();
|
|
4736
4622
|
const domainPkg = doc.domain?.packages?.domain;
|
|
@@ -4775,10 +4661,32 @@ var Session = class {
|
|
|
4775
4661
|
return Array.from(toolMap.values());
|
|
4776
4662
|
}
|
|
4777
4663
|
/**
|
|
4778
|
-
*
|
|
4664
|
+
* Backwards-compatible alias for `getEffects()`.
|
|
4665
|
+
*/
|
|
4666
|
+
getTools() {
|
|
4667
|
+
return this.getEffects();
|
|
4668
|
+
}
|
|
4669
|
+
/**
|
|
4670
|
+
* Subscribe to effect changes (added, removed, updated).
|
|
4779
4671
|
* @param callback - Function called with change events
|
|
4780
4672
|
* @returns Unsubscribe function
|
|
4781
4673
|
*/
|
|
4674
|
+
onEffectsChanged(callback) {
|
|
4675
|
+
const handler = (data) => callback(data);
|
|
4676
|
+
if (!this.eventListeners.has("effects:changed")) {
|
|
4677
|
+
this.eventListeners.set("effects:changed", []);
|
|
4678
|
+
}
|
|
4679
|
+
this.eventListeners.get("effects:changed").push(handler);
|
|
4680
|
+
return () => {
|
|
4681
|
+
const listeners = this.eventListeners.get("effects:changed");
|
|
4682
|
+
if (listeners) {
|
|
4683
|
+
this.eventListeners.set("effects:changed", listeners.filter((h) => h !== handler));
|
|
4684
|
+
}
|
|
4685
|
+
};
|
|
4686
|
+
}
|
|
4687
|
+
/**
|
|
4688
|
+
* Backwards-compatible alias for `onEffectsChanged()`.
|
|
4689
|
+
*/
|
|
4782
4690
|
onToolsChanged(callback) {
|
|
4783
4691
|
const handler = (data) => callback(data);
|
|
4784
4692
|
if (!this.eventListeners.has("tools:changed")) {
|
|
@@ -4891,7 +4799,7 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
4891
4799
|
}
|
|
4892
4800
|
}
|
|
4893
4801
|
if (globalTools && globalTools.length > 0) {
|
|
4894
|
-
docs2 += "## Global
|
|
4802
|
+
docs2 += "## Global Effects\n\n";
|
|
4895
4803
|
for (const tool of globalTools) {
|
|
4896
4804
|
docs2 += `### ${tool.name}
|
|
4897
4805
|
|
|
@@ -4909,10 +4817,10 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
4909
4817
|
return docs2;
|
|
4910
4818
|
}
|
|
4911
4819
|
if (!tools || tools.length === 0) {
|
|
4912
|
-
return "No
|
|
4820
|
+
return "No effects available in this domain.";
|
|
4913
4821
|
}
|
|
4914
|
-
let docs = "# Available
|
|
4915
|
-
docs += "Import
|
|
4822
|
+
let docs = "# Available Effects\n\n";
|
|
4823
|
+
docs += "Import effects from `./sandbox-tools` and call them with await:\n\n";
|
|
4916
4824
|
docs += '```typescript\nimport { tools } from "./sandbox-tools";\n\n';
|
|
4917
4825
|
docs += "// Example:\n";
|
|
4918
4826
|
docs += `const result = await tools.${tools[0]?.name || "example"}(input);
|
|
@@ -4976,6 +4884,7 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
4976
4884
|
setupToolInvokeHandler() {
|
|
4977
4885
|
this.client.registerRpcHandler("tool.invoke", async (params) => {
|
|
4978
4886
|
const { callId, toolName, input } = params;
|
|
4887
|
+
this.emit("effect:invoke", { callId, effectKey: toolName, toolName, input });
|
|
4979
4888
|
this.emit("tool:invoke", { callId, toolName, input });
|
|
4980
4889
|
const handler = this.toolHandlers.get(toolName);
|
|
4981
4890
|
if (!handler) {
|
|
@@ -4987,12 +4896,14 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
4987
4896
|
}
|
|
4988
4897
|
try {
|
|
4989
4898
|
let result;
|
|
4899
|
+
const invocationContext = this.buildLegacyEffectContext();
|
|
4990
4900
|
if (this.instanceTools.has(toolName) && input && typeof input === "object" && "_objectId" in input) {
|
|
4991
4901
|
const { _objectId, ...restParams } = input;
|
|
4992
|
-
result = await handler(_objectId, restParams);
|
|
4902
|
+
result = await handler(_objectId, restParams, invocationContext);
|
|
4993
4903
|
} else {
|
|
4994
|
-
result = await handler(input);
|
|
4904
|
+
result = await handler(input, invocationContext);
|
|
4995
4905
|
}
|
|
4906
|
+
this.emit("effect:result", { callId, effectKey: toolName, result });
|
|
4996
4907
|
this.emit("tool:result", { callId, result });
|
|
4997
4908
|
await this.client.call("tool.result", {
|
|
4998
4909
|
callId,
|
|
@@ -5000,6 +4911,7 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
5000
4911
|
});
|
|
5001
4912
|
} catch (error) {
|
|
5002
4913
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4914
|
+
this.emit("effect:result", { callId, effectKey: toolName, error: errorMessage });
|
|
5003
4915
|
this.emit("tool:result", { callId, error: errorMessage });
|
|
5004
4916
|
await this.client.call("tool.result", {
|
|
5005
4917
|
callId,
|
|
@@ -5032,10 +4944,10 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
5032
4944
|
}
|
|
5033
4945
|
}
|
|
5034
4946
|
/**
|
|
5035
|
-
* Check for changes in the
|
|
4947
|
+
* Check for changes in the effect catalog and emit change events if needed.
|
|
5036
4948
|
*/
|
|
5037
4949
|
checkForToolChanges() {
|
|
5038
|
-
const currentTools = this.
|
|
4950
|
+
const currentTools = this.getEffects();
|
|
5039
4951
|
const currentMap = new Map(currentTools.map((t) => [t.name, t]));
|
|
5040
4952
|
const added = [];
|
|
5041
4953
|
const removed = [];
|
|
@@ -5051,6 +4963,12 @@ import { ${allImports} } from "./sandbox-tools";
|
|
|
5051
4963
|
}
|
|
5052
4964
|
if (added.length > 0 || removed.length > 0) {
|
|
5053
4965
|
this.lastKnownTools = currentMap;
|
|
4966
|
+
this.emit("effects:changed", {
|
|
4967
|
+
effects: currentTools,
|
|
4968
|
+
tools: currentTools,
|
|
4969
|
+
added,
|
|
4970
|
+
removed
|
|
4971
|
+
});
|
|
5054
4972
|
this.emit("tools:changed", {
|
|
5055
4973
|
tools: currentTools,
|
|
5056
4974
|
added,
|
|
@@ -5163,7 +5081,7 @@ var JobImplementation = class {
|
|
|
5163
5081
|
|
|
5164
5082
|
// src/endpoints.ts
|
|
5165
5083
|
var LOCAL_API_URL = "ws://localhost:8787/granular";
|
|
5166
|
-
var PRODUCTION_API_URL = "wss://api.
|
|
5084
|
+
var PRODUCTION_API_URL = "wss://cf-api-gateway.arthur6084.workers.dev/granular";
|
|
5167
5085
|
function readEnv(name) {
|
|
5168
5086
|
if (typeof process === "undefined" || !process.env) return void 0;
|
|
5169
5087
|
return process.env[name];
|
|
@@ -5219,6 +5137,37 @@ var STANDARD_MODULES_OPERATIONS = [
|
|
|
5219
5137
|
var BUILTIN_MODULES = {
|
|
5220
5138
|
"standard_modules": STANDARD_MODULES_OPERATIONS
|
|
5221
5139
|
};
|
|
5140
|
+
function computeEffectKey(effect) {
|
|
5141
|
+
const attachedClass = effect.className?.trim();
|
|
5142
|
+
if (!attachedClass) {
|
|
5143
|
+
return `global:${effect.name}`;
|
|
5144
|
+
}
|
|
5145
|
+
return effect.static ? `class:${attachedClass}:static:${effect.name}` : `class:${attachedClass}:instance:${effect.name}`;
|
|
5146
|
+
}
|
|
5147
|
+
function buildEffectHostUrl(apiUrl, sandboxId, effectClientId, clientId) {
|
|
5148
|
+
const url = new URL(apiUrl);
|
|
5149
|
+
if (url.pathname.endsWith("/granular/ws/connect")) {
|
|
5150
|
+
url.pathname = url.pathname.replace(/\/ws\/connect$/, "/effects/connect");
|
|
5151
|
+
} else if (url.pathname.endsWith("/granular")) {
|
|
5152
|
+
url.pathname = `${url.pathname.replace(/\/$/, "")}/effects/connect`;
|
|
5153
|
+
} else if (url.pathname.endsWith("/v2/ws/connect")) {
|
|
5154
|
+
url.pathname = url.pathname.replace(/\/ws\/connect$/, "/effects/connect");
|
|
5155
|
+
} else if (url.pathname.endsWith("/v2/ws")) {
|
|
5156
|
+
url.pathname = url.pathname.replace(/\/ws$/, "/effects/connect");
|
|
5157
|
+
} else if (url.pathname.endsWith("/ws/connect")) {
|
|
5158
|
+
url.pathname = url.pathname.replace(/\/ws\/connect$/, "/effects/connect");
|
|
5159
|
+
} else if (url.pathname.endsWith("/ws")) {
|
|
5160
|
+
url.pathname = url.pathname.replace(/\/ws$/, "/effects/connect");
|
|
5161
|
+
} else {
|
|
5162
|
+
url.pathname = "/granular/effects/connect";
|
|
5163
|
+
}
|
|
5164
|
+
url.search = "";
|
|
5165
|
+
url.hash = "";
|
|
5166
|
+
url.searchParams.set("sandboxId", sandboxId);
|
|
5167
|
+
url.searchParams.set("effectClientId", effectClientId);
|
|
5168
|
+
url.searchParams.set("clientId", clientId);
|
|
5169
|
+
return url.toString();
|
|
5170
|
+
}
|
|
5222
5171
|
var Environment = class _Environment extends Session {
|
|
5223
5172
|
envData;
|
|
5224
5173
|
_apiKey;
|
|
@@ -5963,72 +5912,31 @@ var Environment = class _Environment extends Session {
|
|
|
5963
5912
|
}
|
|
5964
5913
|
// ==================== PUBLISH TOOLS ====================
|
|
5965
5914
|
/**
|
|
5966
|
-
*
|
|
5967
|
-
* This is the main entry point for setting up tools.
|
|
5968
|
-
*
|
|
5969
|
-
* @example
|
|
5970
|
-
* ```typescript
|
|
5971
|
-
* const tools = [
|
|
5972
|
-
* {
|
|
5973
|
-
* name: 'get_weather',
|
|
5974
|
-
* description: 'Get weather for a city',
|
|
5975
|
-
* inputSchema: { type: 'object', properties: { city: { type: 'string' } } },
|
|
5976
|
-
* handler: async ({ city }) => ({ temp: 22 }),
|
|
5977
|
-
* },
|
|
5978
|
-
* ];
|
|
5979
|
-
*
|
|
5980
|
-
* const { domainRevision } = await environment.publishTools(tools);
|
|
5981
|
-
*
|
|
5982
|
-
* // Now submit jobs that use those tools
|
|
5983
|
-
* const job = await environment.submitJob(`
|
|
5984
|
-
* import { Author } from './sandbox-tools';
|
|
5985
|
-
* const weather = await Author.get_weather({ city: 'Paris' });
|
|
5986
|
-
* return weather;
|
|
5987
|
-
* `);
|
|
5988
|
-
*
|
|
5989
|
-
* const result = await job.result;
|
|
5990
|
-
* ```
|
|
5915
|
+
* Removed: environment-scoped effect publication is no longer supported.
|
|
5991
5916
|
*/
|
|
5992
5917
|
async publishTools(tools, revision = "1.0.0") {
|
|
5993
5918
|
return super.publishTools(tools, revision);
|
|
5994
5919
|
}
|
|
5995
5920
|
/**
|
|
5996
|
-
*
|
|
5997
|
-
*
|
|
5998
|
-
* Adds the effect to the local registry and re-publishes the
|
|
5999
|
-
* full tool catalog to the server. If an effect with the same
|
|
6000
|
-
* name already exists, it is replaced.
|
|
6001
|
-
*
|
|
6002
|
-
* @example
|
|
6003
|
-
* ```typescript
|
|
6004
|
-
* await env.publishEffect({
|
|
6005
|
-
* name: 'get_bio',
|
|
6006
|
-
* description: 'Get biography',
|
|
6007
|
-
* inputSchema: { type: 'object', properties: { detailed: { type: 'boolean' } } },
|
|
6008
|
-
* handler: async (params) => ({ bio: 'Hello' }),
|
|
6009
|
-
* });
|
|
6010
|
-
* ```
|
|
5921
|
+
* Removed: environment-scoped effect publication is no longer supported.
|
|
6011
5922
|
*/
|
|
6012
5923
|
async publishEffect(effect) {
|
|
6013
5924
|
return super.publishEffect(effect);
|
|
6014
5925
|
}
|
|
6015
5926
|
/**
|
|
6016
|
-
*
|
|
6017
|
-
*
|
|
6018
|
-
* Adds all effects to the local registry and re-publishes the
|
|
6019
|
-
* full tool catalog in a single RPC call.
|
|
5927
|
+
* Removed: environment-scoped effect publication is no longer supported.
|
|
6020
5928
|
*/
|
|
6021
5929
|
async publishEffects(effects) {
|
|
6022
5930
|
return super.publishEffects(effects);
|
|
6023
5931
|
}
|
|
6024
5932
|
/**
|
|
6025
|
-
*
|
|
5933
|
+
* Removed: environment-scoped effect publication is no longer supported.
|
|
6026
5934
|
*/
|
|
6027
5935
|
async unpublishEffect(name) {
|
|
6028
5936
|
return super.unpublishEffect(name);
|
|
6029
5937
|
}
|
|
6030
5938
|
/**
|
|
6031
|
-
*
|
|
5939
|
+
* Removed: environment-scoped effect publication is no longer supported.
|
|
6032
5940
|
*/
|
|
6033
5941
|
async unpublishAllEffects() {
|
|
6034
5942
|
return super.unpublishAllEffects();
|
|
@@ -6042,10 +5950,12 @@ var Granular = class {
|
|
|
6042
5950
|
WebSocketCtor;
|
|
6043
5951
|
onUnexpectedClose;
|
|
6044
5952
|
onReconnectError;
|
|
6045
|
-
/** Sandbox-level effect registry: sandboxId → (
|
|
5953
|
+
/** Sandbox-level effect registry: sandboxId → (effectKey → ToolWithHandler) */
|
|
6046
5954
|
sandboxEffects = /* @__PURE__ */ new Map();
|
|
6047
|
-
/**
|
|
6048
|
-
|
|
5955
|
+
/** Live sandbox-scoped effect hosts keyed by sandboxId */
|
|
5956
|
+
sandboxEffectHosts = /* @__PURE__ */ new Map();
|
|
5957
|
+
/** In-flight host connection promises to avoid duplicate concurrent connects */
|
|
5958
|
+
sandboxEffectHostPromises = /* @__PURE__ */ new Map();
|
|
6049
5959
|
/**
|
|
6050
5960
|
* Create a new Granular client
|
|
6051
5961
|
* @param options - Client configuration
|
|
@@ -6098,8 +6008,9 @@ var Granular = class {
|
|
|
6098
6008
|
/**
|
|
6099
6009
|
* Connect to a sandbox and establish a real-time environment session.
|
|
6100
6010
|
*
|
|
6101
|
-
*
|
|
6102
|
-
*
|
|
6011
|
+
* Effects are registered at the sandbox level via `granular.registerEffect()`
|
|
6012
|
+
* or `granular.registerEffects()`. Sessions pick up live availability from
|
|
6013
|
+
* the sandbox registry automatically.
|
|
6103
6014
|
*
|
|
6104
6015
|
* @param options - Connection options
|
|
6105
6016
|
* @returns An active environment session
|
|
@@ -6116,10 +6027,12 @@ var Granular = class {
|
|
|
6116
6027
|
* user,
|
|
6117
6028
|
* });
|
|
6118
6029
|
*
|
|
6119
|
-
*
|
|
6120
|
-
*
|
|
6121
|
-
*
|
|
6122
|
-
*
|
|
6030
|
+
* await granular.registerEffect('my-sandbox', {
|
|
6031
|
+
* name: 'greet',
|
|
6032
|
+
* description: 'Say hello',
|
|
6033
|
+
* inputSchema: { type: 'object', properties: {} },
|
|
6034
|
+
* handler: async () => 'Hello!',
|
|
6035
|
+
* });
|
|
6123
6036
|
*
|
|
6124
6037
|
* // Submit job
|
|
6125
6038
|
* const job = await environment.submitJob(`
|
|
@@ -6141,10 +6054,18 @@ var Granular = class {
|
|
|
6141
6054
|
subjectId: options.user.subjectId,
|
|
6142
6055
|
permissionProfileId: null
|
|
6143
6056
|
});
|
|
6057
|
+
await this.activateEnvironment(envData.environmentId);
|
|
6058
|
+
const session = await this.request("/ws/sessions", {
|
|
6059
|
+
method: "POST",
|
|
6060
|
+
body: JSON.stringify({
|
|
6061
|
+
environmentId: envData.environmentId,
|
|
6062
|
+
clientId
|
|
6063
|
+
})
|
|
6064
|
+
});
|
|
6144
6065
|
const client = new WSClient({
|
|
6145
|
-
url:
|
|
6146
|
-
sessionId:
|
|
6147
|
-
token:
|
|
6066
|
+
url: session.wsUrl,
|
|
6067
|
+
sessionId: session.sessionId,
|
|
6068
|
+
token: session.token,
|
|
6148
6069
|
tokenProvider: this.tokenProvider,
|
|
6149
6070
|
WebSocketCtor: this.WebSocketCtor,
|
|
6150
6071
|
onUnexpectedClose: this.onUnexpectedClose,
|
|
@@ -6153,47 +6074,165 @@ var Granular = class {
|
|
|
6153
6074
|
await client.connect();
|
|
6154
6075
|
const graphqlEndpoint = `${this.httpUrl}/orchestrator/graphql`;
|
|
6155
6076
|
const environment = new Environment(client, envData, clientId, this.apiKey, graphqlEndpoint);
|
|
6156
|
-
if (!this.activeEnvironments.has(sandbox.sandboxId)) {
|
|
6157
|
-
this.activeEnvironments.set(sandbox.sandboxId, []);
|
|
6158
|
-
}
|
|
6159
|
-
this.activeEnvironments.get(sandbox.sandboxId).push(environment);
|
|
6160
|
-
environment.on("disconnect", () => {
|
|
6161
|
-
const list = this.activeEnvironments.get(sandbox.sandboxId);
|
|
6162
|
-
if (list) {
|
|
6163
|
-
this.activeEnvironments.set(sandbox.sandboxId, list.filter((e) => e !== environment));
|
|
6164
|
-
}
|
|
6165
|
-
});
|
|
6166
6077
|
await environment.hello();
|
|
6167
|
-
const effects = this.sandboxEffects.get(sandbox.sandboxId);
|
|
6168
|
-
if (effects && effects.size > 0) {
|
|
6169
|
-
const effectsList = Array.from(effects.values());
|
|
6170
|
-
console.log(`[Granular] Auto-publishing ${effectsList.length} effects for sandbox ${sandbox.sandboxId}`);
|
|
6171
|
-
await environment.publishEffects(effectsList);
|
|
6172
|
-
}
|
|
6173
6078
|
return environment;
|
|
6174
6079
|
}
|
|
6080
|
+
async activateEnvironment(environmentId) {
|
|
6081
|
+
await this.request(`/orchestrator/runtime/environments/${environmentId}/activate`, {
|
|
6082
|
+
method: "POST",
|
|
6083
|
+
body: JSON.stringify({})
|
|
6084
|
+
});
|
|
6085
|
+
}
|
|
6175
6086
|
// ── Sandbox-Level Effects ──
|
|
6087
|
+
getSandboxEffectMap(sandboxId) {
|
|
6088
|
+
let effects = this.sandboxEffects.get(sandboxId);
|
|
6089
|
+
if (!effects) {
|
|
6090
|
+
effects = /* @__PURE__ */ new Map();
|
|
6091
|
+
this.sandboxEffects.set(sandboxId, effects);
|
|
6092
|
+
}
|
|
6093
|
+
return effects;
|
|
6094
|
+
}
|
|
6095
|
+
serializeEffect(effect) {
|
|
6096
|
+
return {
|
|
6097
|
+
effectKey: computeEffectKey(effect),
|
|
6098
|
+
name: effect.name,
|
|
6099
|
+
description: effect.description,
|
|
6100
|
+
inputSchema: effect.inputSchema,
|
|
6101
|
+
outputSchema: effect.outputSchema,
|
|
6102
|
+
stability: effect.stability || "stable",
|
|
6103
|
+
provenance: effect.provenance || { source: "custom" },
|
|
6104
|
+
tags: effect.tags,
|
|
6105
|
+
className: effect.className,
|
|
6106
|
+
static: effect.static
|
|
6107
|
+
};
|
|
6108
|
+
}
|
|
6109
|
+
async publishSandboxEffectCatalog(host) {
|
|
6110
|
+
const effects = Array.from(this.getSandboxEffectMap(host.sandboxId).values()).map(
|
|
6111
|
+
(effect) => this.serializeEffect(effect)
|
|
6112
|
+
);
|
|
6113
|
+
await host.wsClient.call("effects.publishCatalog", { effects });
|
|
6114
|
+
}
|
|
6115
|
+
async syncSandboxEffectCatalog(sandboxId) {
|
|
6116
|
+
const host = await this.ensureSandboxEffectHost(sandboxId);
|
|
6117
|
+
await this.publishSandboxEffectCatalog(host);
|
|
6118
|
+
}
|
|
6119
|
+
startEffectHostHeartbeat(host) {
|
|
6120
|
+
if (host.heartbeatTimer) {
|
|
6121
|
+
clearInterval(host.heartbeatTimer);
|
|
6122
|
+
}
|
|
6123
|
+
host.wsClient.call("client.heartbeat", {}).catch((error) => {
|
|
6124
|
+
console.warn(
|
|
6125
|
+
`[Granular] Initial effect host heartbeat failed for sandbox ${host.sandboxId}:`,
|
|
6126
|
+
error
|
|
6127
|
+
);
|
|
6128
|
+
});
|
|
6129
|
+
host.heartbeatTimer = setInterval(() => {
|
|
6130
|
+
host.wsClient.call("client.heartbeat", {}).catch((error) => {
|
|
6131
|
+
console.warn(
|
|
6132
|
+
`[Granular] Effect host heartbeat failed for sandbox ${host.sandboxId}:`,
|
|
6133
|
+
error
|
|
6134
|
+
);
|
|
6135
|
+
});
|
|
6136
|
+
}, 1e4);
|
|
6137
|
+
}
|
|
6138
|
+
stopEffectHostHeartbeat(host) {
|
|
6139
|
+
if (!host?.heartbeatTimer) {
|
|
6140
|
+
return;
|
|
6141
|
+
}
|
|
6142
|
+
clearInterval(host.heartbeatTimer);
|
|
6143
|
+
host.heartbeatTimer = null;
|
|
6144
|
+
}
|
|
6145
|
+
async synchronizeEffectHost(host) {
|
|
6146
|
+
await host.wsClient.call("client.hello", {
|
|
6147
|
+
clientId: host.clientId,
|
|
6148
|
+
protocolVersion: "2.0"
|
|
6149
|
+
});
|
|
6150
|
+
this.startEffectHostHeartbeat(host);
|
|
6151
|
+
await this.publishSandboxEffectCatalog(host);
|
|
6152
|
+
}
|
|
6153
|
+
async ensureSandboxEffectHost(sandboxId) {
|
|
6154
|
+
const existing = this.sandboxEffectHosts.get(sandboxId);
|
|
6155
|
+
if (existing) {
|
|
6156
|
+
return existing;
|
|
6157
|
+
}
|
|
6158
|
+
const inflight = this.sandboxEffectHostPromises.get(sandboxId);
|
|
6159
|
+
if (inflight) {
|
|
6160
|
+
return inflight;
|
|
6161
|
+
}
|
|
6162
|
+
const connectPromise = (async () => {
|
|
6163
|
+
const effectClientId = crypto.randomUUID();
|
|
6164
|
+
const clientId = `effect-host:${sandboxId}:${effectClientId}`;
|
|
6165
|
+
const wsClient = new WSClient({
|
|
6166
|
+
url: buildEffectHostUrl(this.apiUrl, sandboxId, effectClientId, clientId),
|
|
6167
|
+
sessionId: `effect-host:${effectClientId}`,
|
|
6168
|
+
token: this.apiKey,
|
|
6169
|
+
tokenProvider: this.tokenProvider,
|
|
6170
|
+
WebSocketCtor: this.WebSocketCtor,
|
|
6171
|
+
onUnexpectedClose: this.onUnexpectedClose,
|
|
6172
|
+
onReconnectError: this.onReconnectError
|
|
6173
|
+
});
|
|
6174
|
+
const host = {
|
|
6175
|
+
sandboxId,
|
|
6176
|
+
effectClientId,
|
|
6177
|
+
clientId,
|
|
6178
|
+
wsClient,
|
|
6179
|
+
heartbeatTimer: null
|
|
6180
|
+
};
|
|
6181
|
+
wsClient.registerRpcHandler("effect.invoke", async (params) => {
|
|
6182
|
+
const request = params;
|
|
6183
|
+
const effect = this.getSandboxEffectMap(sandboxId).get(request.effectKey);
|
|
6184
|
+
if (!effect) {
|
|
6185
|
+
throw new Error(`Effect handler not found: ${request.effectKey}`);
|
|
6186
|
+
}
|
|
6187
|
+
if (effect.className && !effect.static && request.input && typeof request.input === "object" && "_objectId" in request.input) {
|
|
6188
|
+
const { _objectId, ...rest } = request.input;
|
|
6189
|
+
return effect.handler(_objectId, rest, request.context);
|
|
6190
|
+
}
|
|
6191
|
+
return effect.handler(request.input, request.context);
|
|
6192
|
+
});
|
|
6193
|
+
wsClient.on("open", () => {
|
|
6194
|
+
void this.synchronizeEffectHost(host).catch((error) => {
|
|
6195
|
+
console.error(
|
|
6196
|
+
`[Granular] Failed to re-sync effect host for sandbox ${sandboxId}:`,
|
|
6197
|
+
error
|
|
6198
|
+
);
|
|
6199
|
+
});
|
|
6200
|
+
});
|
|
6201
|
+
wsClient.on("disconnect", () => {
|
|
6202
|
+
this.stopEffectHostHeartbeat(host);
|
|
6203
|
+
});
|
|
6204
|
+
await wsClient.connect();
|
|
6205
|
+
await this.synchronizeEffectHost(host);
|
|
6206
|
+
this.sandboxEffectHosts.set(sandboxId, host);
|
|
6207
|
+
return host;
|
|
6208
|
+
})();
|
|
6209
|
+
this.sandboxEffectHostPromises.set(sandboxId, connectPromise);
|
|
6210
|
+
try {
|
|
6211
|
+
return await connectPromise;
|
|
6212
|
+
} finally {
|
|
6213
|
+
this.sandboxEffectHostPromises.delete(sandboxId);
|
|
6214
|
+
}
|
|
6215
|
+
}
|
|
6216
|
+
disconnectSandboxEffectHost(sandboxId) {
|
|
6217
|
+
const host = this.sandboxEffectHosts.get(sandboxId);
|
|
6218
|
+
if (!host) {
|
|
6219
|
+
return;
|
|
6220
|
+
}
|
|
6221
|
+
this.stopEffectHostHeartbeat(host);
|
|
6222
|
+
host.wsClient.disconnect();
|
|
6223
|
+
this.sandboxEffectHosts.delete(sandboxId);
|
|
6224
|
+
}
|
|
6176
6225
|
/**
|
|
6177
6226
|
* Register an effect (tool) for a specific sandbox.
|
|
6178
|
-
*
|
|
6179
|
-
* The effect will be automatically published to:
|
|
6180
|
-
* 1. Any currently active environments for this sandbox
|
|
6181
|
-
* 2. Any new environments created/connected for this sandbox
|
|
6182
|
-
*
|
|
6227
|
+
*
|
|
6183
6228
|
* @param sandboxNameOrId - The name or ID of the sandbox
|
|
6184
6229
|
* @param effect - The tool definition and handler
|
|
6185
6230
|
*/
|
|
6186
6231
|
async registerEffect(sandboxNameOrId, effect) {
|
|
6187
6232
|
const sandbox = await this.findOrCreateSandbox(sandboxNameOrId);
|
|
6188
6233
|
const sandboxId = sandbox.sandboxId;
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
}
|
|
6192
|
-
this.sandboxEffects.get(sandboxId).set(effect.name, effect);
|
|
6193
|
-
const activeEnvs = this.activeEnvironments.get(sandboxId) || [];
|
|
6194
|
-
for (const env of activeEnvs) {
|
|
6195
|
-
await env.publishEffect(effect);
|
|
6196
|
-
}
|
|
6234
|
+
this.getSandboxEffectMap(sandboxId).set(computeEffectKey(effect), effect);
|
|
6235
|
+
await this.syncSandboxEffectCatalog(sandboxId);
|
|
6197
6236
|
}
|
|
6198
6237
|
/**
|
|
6199
6238
|
* Register multiple effects (tools) for a specific sandbox.
|
|
@@ -6203,35 +6242,39 @@ var Granular = class {
|
|
|
6203
6242
|
async registerEffects(sandboxNameOrId, effects) {
|
|
6204
6243
|
const sandbox = await this.findOrCreateSandbox(sandboxNameOrId);
|
|
6205
6244
|
const sandboxId = sandbox.sandboxId;
|
|
6206
|
-
|
|
6207
|
-
this.sandboxEffects.set(sandboxId, /* @__PURE__ */ new Map());
|
|
6208
|
-
}
|
|
6209
|
-
const map = this.sandboxEffects.get(sandboxId);
|
|
6245
|
+
const map = this.getSandboxEffectMap(sandboxId);
|
|
6210
6246
|
for (const effect of effects) {
|
|
6211
|
-
map.set(effect
|
|
6212
|
-
}
|
|
6213
|
-
const activeEnvs = this.activeEnvironments.get(sandboxId) || [];
|
|
6214
|
-
for (const env of activeEnvs) {
|
|
6215
|
-
await env.publishEffects(effects);
|
|
6247
|
+
map.set(computeEffectKey(effect), effect);
|
|
6216
6248
|
}
|
|
6249
|
+
await this.syncSandboxEffectCatalog(sandboxId);
|
|
6217
6250
|
}
|
|
6218
6251
|
/**
|
|
6219
6252
|
* Unregister an effect from a sandbox.
|
|
6220
6253
|
*
|
|
6221
|
-
* Removes it from the local registry and
|
|
6222
|
-
*
|
|
6254
|
+
* Removes it from the local sandbox registry and updates the
|
|
6255
|
+
* sandbox-scoped live catalog.
|
|
6223
6256
|
*/
|
|
6224
6257
|
async unregisterEffect(sandboxNameOrId, name) {
|
|
6225
6258
|
const sandbox = await this.findOrCreateSandbox(sandboxNameOrId);
|
|
6226
6259
|
const sandboxId = sandbox.sandboxId;
|
|
6227
|
-
const
|
|
6228
|
-
if (
|
|
6229
|
-
|
|
6260
|
+
const currentMap = this.sandboxEffects.get(sandboxId);
|
|
6261
|
+
if (!currentMap) {
|
|
6262
|
+
return;
|
|
6230
6263
|
}
|
|
6231
|
-
const
|
|
6232
|
-
|
|
6233
|
-
|
|
6264
|
+
const nextEntries = Array.from(currentMap.entries()).filter(
|
|
6265
|
+
([effectKey, effect]) => effectKey !== name && effect.name !== name
|
|
6266
|
+
);
|
|
6267
|
+
if (nextEntries.length === currentMap.size) {
|
|
6268
|
+
return;
|
|
6269
|
+
}
|
|
6270
|
+
const nextMap = new Map(nextEntries);
|
|
6271
|
+
this.sandboxEffects.set(sandboxId, nextMap);
|
|
6272
|
+
if (nextMap.size === 0) {
|
|
6273
|
+
this.disconnectSandboxEffectHost(sandboxId);
|
|
6274
|
+
this.sandboxEffects.delete(sandboxId);
|
|
6275
|
+
return;
|
|
6234
6276
|
}
|
|
6277
|
+
await this.syncSandboxEffectCatalog(sandboxId);
|
|
6235
6278
|
}
|
|
6236
6279
|
/**
|
|
6237
6280
|
* Unregister all effects for a sandbox.
|
|
@@ -6240,10 +6283,7 @@ var Granular = class {
|
|
|
6240
6283
|
const sandbox = await this.findOrCreateSandbox(sandboxNameOrId);
|
|
6241
6284
|
const sandboxId = sandbox.sandboxId;
|
|
6242
6285
|
this.sandboxEffects.delete(sandboxId);
|
|
6243
|
-
|
|
6244
|
-
for (const env of activeEnvs) {
|
|
6245
|
-
await env.unpublishAllEffects();
|
|
6246
|
-
}
|
|
6286
|
+
this.disconnectSandboxEffectHost(sandboxId);
|
|
6247
6287
|
}
|
|
6248
6288
|
/**
|
|
6249
6289
|
* Find a sandbox by name or create it if it doesn't exist
|
|
@@ -6279,7 +6319,7 @@ var Granular = class {
|
|
|
6279
6319
|
const created = await this.permissionProfiles.create(sandboxId, {
|
|
6280
6320
|
name: profileName,
|
|
6281
6321
|
rules: {
|
|
6282
|
-
|
|
6322
|
+
effects: { allow: ["*"] },
|
|
6283
6323
|
resources: { allow: ["*"] }
|
|
6284
6324
|
}
|
|
6285
6325
|
});
|