@granular-software/sdk 0.4.29 → 0.4.31
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 +25 -20
- package/dist/agent-evals.d.mts +7 -6
- package/dist/agent-evals.d.ts +7 -6
- package/dist/agent-evals.js +1002 -525
- package/dist/agent-evals.js.map +1 -1
- package/dist/agent-evals.mjs +1002 -525
- package/dist/agent-evals.mjs.map +1 -1
- package/dist/agent-harness.d.mts +11 -2
- package/dist/agent-harness.d.ts +11 -2
- package/dist/agent-harness.js +248 -125
- package/dist/agent-harness.js.map +1 -1
- package/dist/agent-harness.mjs +246 -126
- package/dist/agent-harness.mjs.map +1 -1
- package/dist/cli/index.js +773 -394
- package/dist/{client-CigjaeO8.d.mts → client-CTH1hfwe.d.mts} +255 -117
- package/dist/{client-CigjaeO8.d.ts → client-CTH1hfwe.d.ts} +255 -117
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +938 -435
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +934 -436
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/agent-evals.js
CHANGED
|
@@ -4674,27 +4674,27 @@ var Session = class {
|
|
|
4674
4674
|
}
|
|
4675
4675
|
async publishTools(tools, revision = "1.0.0") {
|
|
4676
4676
|
throw new Error(
|
|
4677
|
-
"Environment-scoped effect publication was removed. Declare effects in the manifest and register live handlers with granular.
|
|
4677
|
+
"Environment-scoped effect publication was removed. Declare effects in the manifest and register live handlers with granular.ontology(environment.sandboxId).effects.registerMany(effects)."
|
|
4678
4678
|
);
|
|
4679
4679
|
}
|
|
4680
4680
|
async publishEffect(effect) {
|
|
4681
4681
|
throw new Error(
|
|
4682
|
-
"Environment-scoped effect publication was removed. Use granular.
|
|
4682
|
+
"Environment-scoped effect publication was removed. Use granular.ontology(environment.sandboxId).effects.register(effect)."
|
|
4683
4683
|
);
|
|
4684
4684
|
}
|
|
4685
4685
|
async publishEffects(effects) {
|
|
4686
4686
|
throw new Error(
|
|
4687
|
-
"Environment-scoped effect publication was removed. Use granular.
|
|
4687
|
+
"Environment-scoped effect publication was removed. Use granular.ontology(environment.sandboxId).effects.registerMany(effects)."
|
|
4688
4688
|
);
|
|
4689
4689
|
}
|
|
4690
4690
|
async unpublishEffect(name) {
|
|
4691
4691
|
throw new Error(
|
|
4692
|
-
"Environment-scoped effect publication was removed. Use granular.
|
|
4692
|
+
"Environment-scoped effect publication was removed. Use granular.ontology(environment.sandboxId).effects.unregister(effectName)."
|
|
4693
4693
|
);
|
|
4694
4694
|
}
|
|
4695
4695
|
async unpublishAllEffects() {
|
|
4696
4696
|
throw new Error(
|
|
4697
|
-
"Environment-scoped effect publication was removed. Use granular.
|
|
4697
|
+
"Environment-scoped effect publication was removed. Use granular.ontology(environment.sandboxId).effects.clear()."
|
|
4698
4698
|
);
|
|
4699
4699
|
}
|
|
4700
4700
|
/**
|
|
@@ -10980,17 +10980,8 @@ var searchableMetamodelPackage = defineMetamodelPackage({
|
|
|
10980
10980
|
}
|
|
10981
10981
|
},
|
|
10982
10982
|
domain: {
|
|
10983
|
-
applyToPropertyIR(propertyIR,
|
|
10984
|
-
|
|
10985
|
-
return propertyIR;
|
|
10986
|
-
}
|
|
10987
|
-
return {
|
|
10988
|
-
...propertyIR,
|
|
10989
|
-
docs: [
|
|
10990
|
-
...propertyIR.docs,
|
|
10991
|
-
propertySummary.searchablePhonetic ? "Searchable via `search` using FalkorDB full-text query syntax with phonetic matching enabled." : "Searchable via `search` using FalkorDB full-text query syntax."
|
|
10992
|
-
]
|
|
10993
|
-
};
|
|
10983
|
+
applyToPropertyIR(propertyIR, _propertySummary) {
|
|
10984
|
+
return propertyIR;
|
|
10994
10985
|
}
|
|
10995
10986
|
}
|
|
10996
10987
|
});
|
|
@@ -11604,17 +11595,40 @@ function buildEffectMetamodelMutations(toolPath, spec) {
|
|
|
11604
11595
|
|
|
11605
11596
|
// src/client.ts
|
|
11606
11597
|
var STANDARD_MODULES_OPERATIONS = [
|
|
11607
|
-
{
|
|
11598
|
+
{
|
|
11599
|
+
create: "entity",
|
|
11600
|
+
has: { id: { value: "auto-generated" }, createdAt: { value: void 0 } }
|
|
11601
|
+
},
|
|
11608
11602
|
{ create: "class", extends: "entity", has: {} },
|
|
11609
|
-
{
|
|
11610
|
-
|
|
11603
|
+
{
|
|
11604
|
+
create: "user",
|
|
11605
|
+
extends: "entity",
|
|
11606
|
+
has: {
|
|
11607
|
+
email: { value: void 0 },
|
|
11608
|
+
firstName: { value: void 0 },
|
|
11609
|
+
lastName: { value: void 0 }
|
|
11610
|
+
}
|
|
11611
|
+
},
|
|
11612
|
+
{
|
|
11613
|
+
create: "company",
|
|
11614
|
+
extends: "entity",
|
|
11615
|
+
has: { name: { value: void 0 }, website: { value: void 0 } }
|
|
11616
|
+
},
|
|
11611
11617
|
{ create: "string", has: {} },
|
|
11612
11618
|
{ create: "number", has: {} },
|
|
11613
11619
|
{ create: "boolean", has: {} },
|
|
11614
|
-
{
|
|
11620
|
+
{
|
|
11621
|
+
create: "tool_parameter",
|
|
11622
|
+
has: {
|
|
11623
|
+
name: { value: void 0 },
|
|
11624
|
+
type: { value: "string" },
|
|
11625
|
+
description: { value: void 0 },
|
|
11626
|
+
required: { value: false }
|
|
11627
|
+
}
|
|
11628
|
+
}
|
|
11615
11629
|
];
|
|
11616
11630
|
var BUILTIN_MODULES = {
|
|
11617
|
-
|
|
11631
|
+
standard_modules: STANDARD_MODULES_OPERATIONS
|
|
11618
11632
|
};
|
|
11619
11633
|
var DEFAULT_DIRECT_RECORD_OBJECTS_REQUEST_BATCH_SIZE = 100;
|
|
11620
11634
|
var MAX_RECORD_OBJECTS_CONCURRENCY = 16;
|
|
@@ -11649,7 +11663,9 @@ function isRetryableLocalWorkerRestart(status, body, url) {
|
|
|
11649
11663
|
}
|
|
11650
11664
|
function isRetryableRecordObjectsError(error) {
|
|
11651
11665
|
const message = error instanceof Error ? error.message : String(error);
|
|
11652
|
-
return /worker restarted mid-request|econnreset|network|socket connection was closed unexpectedly|timed out/i.test(
|
|
11666
|
+
return /worker restarted mid-request|econnreset|network|socket connection was closed unexpectedly|timed out/i.test(
|
|
11667
|
+
message
|
|
11668
|
+
);
|
|
11653
11669
|
}
|
|
11654
11670
|
function computeEffectKey2(effect) {
|
|
11655
11671
|
const attachedClass = effect.className?.trim();
|
|
@@ -11702,6 +11718,22 @@ function normalizeHeapSnapshot(raw) {
|
|
|
11702
11718
|
updatedAt: typeof heap.updatedAt === "number" ? heap.updatedAt : Date.now()
|
|
11703
11719
|
};
|
|
11704
11720
|
}
|
|
11721
|
+
function deriveRuntimeBaseUrl(apiEndpoint) {
|
|
11722
|
+
try {
|
|
11723
|
+
const endpoint = new URL(apiEndpoint);
|
|
11724
|
+
const graphqlSuffix = "/orchestrator/graphql";
|
|
11725
|
+
if (endpoint.pathname.endsWith(graphqlSuffix)) {
|
|
11726
|
+
endpoint.pathname = endpoint.pathname.slice(0, -graphqlSuffix.length);
|
|
11727
|
+
} else if (endpoint.pathname.endsWith("/graphql")) {
|
|
11728
|
+
endpoint.pathname = endpoint.pathname.slice(0, -"/graphql".length);
|
|
11729
|
+
}
|
|
11730
|
+
endpoint.search = "";
|
|
11731
|
+
endpoint.hash = "";
|
|
11732
|
+
return endpoint.toString().replace(/\/$/, "");
|
|
11733
|
+
} catch {
|
|
11734
|
+
return apiEndpoint.replace(/\/orchestrator\/graphql$/, "").replace(/\/$/, "");
|
|
11735
|
+
}
|
|
11736
|
+
}
|
|
11705
11737
|
function normalizeSubject(subject) {
|
|
11706
11738
|
const granularId = subject.granularId || subject.subjectId;
|
|
11707
11739
|
const userId = subject.userId || subject.identityId || granularId;
|
|
@@ -11726,7 +11758,10 @@ function normalizeUser(user) {
|
|
|
11726
11758
|
};
|
|
11727
11759
|
}
|
|
11728
11760
|
function normalizeEnvironmentData(environment) {
|
|
11729
|
-
const buildPolicy = environment.buildPolicy || environment.tracking || (environment.tagId ? { mode: "tag", tagId: environment.tagId } : {
|
|
11761
|
+
const buildPolicy = environment.buildPolicy || environment.tracking || (environment.tagId ? { mode: "tag", tagId: environment.tagId } : {
|
|
11762
|
+
mode: "pinned",
|
|
11763
|
+
versionId: environment.versionId || environment.buildId
|
|
11764
|
+
});
|
|
11730
11765
|
const environmentName = environment.environment || environment.envName || "prod";
|
|
11731
11766
|
return {
|
|
11732
11767
|
...environment,
|
|
@@ -11738,12 +11773,13 @@ function normalizeEnvironmentData(environment) {
|
|
|
11738
11773
|
tracking: environment.tracking || buildPolicy
|
|
11739
11774
|
};
|
|
11740
11775
|
}
|
|
11741
|
-
var Environment = class
|
|
11776
|
+
var Environment = class {
|
|
11777
|
+
granular;
|
|
11742
11778
|
envData;
|
|
11743
11779
|
_apiKey;
|
|
11744
11780
|
_apiEndpoint;
|
|
11745
|
-
constructor(
|
|
11746
|
-
|
|
11781
|
+
constructor(granular, envData, apiKey, apiEndpoint) {
|
|
11782
|
+
this.granular = granular;
|
|
11747
11783
|
this.envData = envData;
|
|
11748
11784
|
this._apiKey = apiKey;
|
|
11749
11785
|
this._apiEndpoint = apiEndpoint;
|
|
@@ -11784,35 +11820,126 @@ var Environment = class extends Session {
|
|
|
11784
11820
|
get permissionProfileId() {
|
|
11785
11821
|
return this.envData.permissionProfileId;
|
|
11786
11822
|
}
|
|
11823
|
+
/** The current build policy backing this environment */
|
|
11824
|
+
get buildPolicy() {
|
|
11825
|
+
return this.envData.buildPolicy;
|
|
11826
|
+
}
|
|
11827
|
+
/** The current update state relative to the followed tag */
|
|
11828
|
+
get updateState() {
|
|
11829
|
+
return this.envData.updateState;
|
|
11830
|
+
}
|
|
11831
|
+
/** Convenience flag for whether this environment trails the current tag target */
|
|
11832
|
+
get isOutdated() {
|
|
11833
|
+
return this.envData.updateState === "update_available";
|
|
11834
|
+
}
|
|
11835
|
+
/** The followed tag name when this environment is tag-tracked */
|
|
11836
|
+
get tag() {
|
|
11837
|
+
return this.envData.tag?.name || this.envData.buildPolicy.tagName || null;
|
|
11838
|
+
}
|
|
11787
11839
|
/** The GraphQL API endpoint URL */
|
|
11788
11840
|
get apiEndpoint() {
|
|
11789
11841
|
return this._apiEndpoint;
|
|
11790
11842
|
}
|
|
11843
|
+
/** Internal auth token used for control-plane and runtime fallback requests */
|
|
11844
|
+
get authToken() {
|
|
11845
|
+
return this._apiKey;
|
|
11846
|
+
}
|
|
11847
|
+
/** Base runtime URL derived from the GraphQL endpoint */
|
|
11848
|
+
get runtimeBaseUrl() {
|
|
11849
|
+
return this.getRuntimeBaseUrl();
|
|
11850
|
+
}
|
|
11851
|
+
get sessions() {
|
|
11852
|
+
return {
|
|
11853
|
+
list: async (options) => this.listSessions(options?.status || "active"),
|
|
11854
|
+
create: async (options) => this.createSession(options),
|
|
11855
|
+
connect: async (sessionId, options) => this.connectSession(sessionId, options),
|
|
11856
|
+
reopen: async (sessionId, options) => this.reopenSession(sessionId, options),
|
|
11857
|
+
close: async (sessionId, session) => this.closeSession(sessionId, session)
|
|
11858
|
+
};
|
|
11859
|
+
}
|
|
11860
|
+
get data() {
|
|
11861
|
+
return {
|
|
11862
|
+
record: async (record) => this.recordObject(record),
|
|
11863
|
+
recordMany: async (records, options) => this.recordObjects(records, options),
|
|
11864
|
+
import: async (records, options) => this.enqueueRecordImport(records, options),
|
|
11865
|
+
listImports: async (status) => this.listRecordImports(status),
|
|
11866
|
+
getImport: async (importId) => this.getRecordImport(importId),
|
|
11867
|
+
getImportSummary: async () => this.getRecordImportSummary(),
|
|
11868
|
+
cancelImport: async (importId) => this.cancelRecordImport(importId),
|
|
11869
|
+
getAwaitingCount: async () => this.getAwaitingRecordCount()
|
|
11870
|
+
};
|
|
11871
|
+
}
|
|
11872
|
+
get feedback() {
|
|
11873
|
+
return {
|
|
11874
|
+
list: async () => this.listFeedback()
|
|
11875
|
+
};
|
|
11876
|
+
}
|
|
11791
11877
|
/**
|
|
11792
|
-
*
|
|
11793
|
-
*
|
|
11794
|
-
*
|
|
11795
|
-
*
|
|
11878
|
+
* Sessionless environments do not own a live transport, so disconnecting the
|
|
11879
|
+
* environment handle itself is a no-op. This keeps the public surface
|
|
11880
|
+
* symmetric with `EnvironmentSession.disconnect()` and lets callers always
|
|
11881
|
+
* clean up safely without tracking whether they currently hold an environment
|
|
11882
|
+
* or a session.
|
|
11796
11883
|
*/
|
|
11797
|
-
|
|
11798
|
-
const doc = this.document;
|
|
11799
|
-
return normalizeHeapSnapshot(doc?.heap);
|
|
11884
|
+
async disconnect() {
|
|
11800
11885
|
}
|
|
11801
|
-
|
|
11802
|
-
|
|
11803
|
-
const
|
|
11804
|
-
|
|
11805
|
-
|
|
11806
|
-
|
|
11807
|
-
|
|
11808
|
-
|
|
11809
|
-
|
|
11810
|
-
|
|
11811
|
-
|
|
11812
|
-
|
|
11813
|
-
|
|
11814
|
-
|
|
11886
|
+
async listSessions(status = "active") {
|
|
11887
|
+
if (status === "all") {
|
|
11888
|
+
const [active, closed] = await Promise.all([
|
|
11889
|
+
this.granular.listOpenSessions({ environmentId: this.environmentId }),
|
|
11890
|
+
this.granular.listClosedSessions({ environmentId: this.environmentId })
|
|
11891
|
+
]);
|
|
11892
|
+
return [...active, ...closed].sort(
|
|
11893
|
+
(left, right) => Date.parse(right.lastSeenAt) - Date.parse(left.lastSeenAt)
|
|
11894
|
+
);
|
|
11895
|
+
}
|
|
11896
|
+
return status === "closed" ? this.granular.listClosedSessions({ environmentId: this.environmentId }) : this.granular.listOpenSessions({ environmentId: this.environmentId });
|
|
11897
|
+
}
|
|
11898
|
+
async createSession(options) {
|
|
11899
|
+
return this.granular.createSession({
|
|
11900
|
+
environmentId: this.environmentId,
|
|
11901
|
+
clientId: options?.clientId,
|
|
11902
|
+
initialHeap: options?.initialHeap
|
|
11903
|
+
});
|
|
11904
|
+
}
|
|
11905
|
+
async connectSession(sessionId, options) {
|
|
11906
|
+
const session = await this.granular["connectSession"]({
|
|
11907
|
+
sessionId,
|
|
11908
|
+
clientId: options?.clientId
|
|
11909
|
+
});
|
|
11910
|
+
if (session.environmentId !== this.environmentId) {
|
|
11911
|
+
await session.disconnect().catch(() => {
|
|
11912
|
+
session.disconnectTransport();
|
|
11913
|
+
});
|
|
11914
|
+
throw new Error(
|
|
11915
|
+
`Session ${sessionId} belongs to environment ${session.environmentId}, not ${this.environmentId}.`
|
|
11916
|
+
);
|
|
11815
11917
|
}
|
|
11918
|
+
return session;
|
|
11919
|
+
}
|
|
11920
|
+
async reopenSession(sessionId, options) {
|
|
11921
|
+
const session = await this.granular.reopenSession(sessionId, {
|
|
11922
|
+
clientId: options?.clientId
|
|
11923
|
+
});
|
|
11924
|
+
if (session.environmentId !== this.environmentId) {
|
|
11925
|
+
await session.disconnect().catch(() => {
|
|
11926
|
+
session.disconnectTransport();
|
|
11927
|
+
});
|
|
11928
|
+
throw new Error(
|
|
11929
|
+
`Session ${sessionId} belongs to environment ${session.environmentId}, not ${this.environmentId}.`
|
|
11930
|
+
);
|
|
11931
|
+
}
|
|
11932
|
+
return session;
|
|
11933
|
+
}
|
|
11934
|
+
async closeSession(sessionId, session) {
|
|
11935
|
+
await this.granular.closeSession(sessionId, session);
|
|
11936
|
+
}
|
|
11937
|
+
async listFeedback() {
|
|
11938
|
+
const response = await this.controlPlaneRequest(`/control/environments/${this.environmentId}/feedback`);
|
|
11939
|
+
return Array.isArray(response.items) ? response.items : [];
|
|
11940
|
+
}
|
|
11941
|
+
getRuntimeBaseUrl() {
|
|
11942
|
+
return deriveRuntimeBaseUrl(this._apiEndpoint);
|
|
11816
11943
|
}
|
|
11817
11944
|
async controlPlaneRequest(path2, options = {}) {
|
|
11818
11945
|
const runtimeBase = this.getRuntimeBaseUrl();
|
|
@@ -11820,94 +11947,19 @@ var Environment = class extends Session {
|
|
|
11820
11947
|
const response = await fetch(url, {
|
|
11821
11948
|
...options,
|
|
11822
11949
|
headers: {
|
|
11823
|
-
|
|
11950
|
+
Authorization: `Bearer ${this._apiKey}`,
|
|
11824
11951
|
"Content-Type": "application/json",
|
|
11825
|
-
|
|
11952
|
+
Connection: "close",
|
|
11826
11953
|
...options.headers
|
|
11827
11954
|
}
|
|
11828
11955
|
});
|
|
11829
11956
|
if (!response.ok) {
|
|
11830
|
-
throw new Error(
|
|
11957
|
+
throw new Error(
|
|
11958
|
+
`Control Plane API Error (${response.status}): ${await response.text()}`
|
|
11959
|
+
);
|
|
11831
11960
|
}
|
|
11832
11961
|
return response.json();
|
|
11833
11962
|
}
|
|
11834
|
-
/**
|
|
11835
|
-
* Close the session and disconnect from the sandbox.
|
|
11836
|
-
*
|
|
11837
|
-
* Sends `client.goodbye` over WebSocket first, then issues an HTTP fallback
|
|
11838
|
-
* to the runtime goodbye endpoint if no definitive WS-side runtime notify
|
|
11839
|
-
* acknowledgement was observed.
|
|
11840
|
-
*/
|
|
11841
|
-
async disconnect() {
|
|
11842
|
-
let wsNotifiedRuntime = false;
|
|
11843
|
-
try {
|
|
11844
|
-
const goodbye = await this.rpc("client.goodbye", {
|
|
11845
|
-
timestamp: Date.now()
|
|
11846
|
-
});
|
|
11847
|
-
wsNotifiedRuntime = Boolean(goodbye?.ok && goodbye?.via);
|
|
11848
|
-
} catch {
|
|
11849
|
-
wsNotifiedRuntime = false;
|
|
11850
|
-
}
|
|
11851
|
-
if (!wsNotifiedRuntime) {
|
|
11852
|
-
try {
|
|
11853
|
-
const runtimeBase = this.getRuntimeBaseUrl();
|
|
11854
|
-
await fetch(
|
|
11855
|
-
`${runtimeBase}/orchestrator/runtime/environments/${this.environmentId}/session-goodbye`,
|
|
11856
|
-
{
|
|
11857
|
-
method: "POST",
|
|
11858
|
-
headers: {
|
|
11859
|
-
"Content-Type": "application/json",
|
|
11860
|
-
"Authorization": `Bearer ${this._apiKey}`,
|
|
11861
|
-
"Connection": "close"
|
|
11862
|
-
},
|
|
11863
|
-
body: JSON.stringify({
|
|
11864
|
-
reason: "sdk_disconnect_http_fallback",
|
|
11865
|
-
sessionId: this.client.currentSessionId
|
|
11866
|
-
})
|
|
11867
|
-
}
|
|
11868
|
-
);
|
|
11869
|
-
} catch {
|
|
11870
|
-
}
|
|
11871
|
-
}
|
|
11872
|
-
this.client.disconnect();
|
|
11873
|
-
}
|
|
11874
|
-
// ==================== GRAPH CONTAINER READINESS ====================
|
|
11875
|
-
/** The last known graph container status, updated by checkReadiness() or on heartbeat */
|
|
11876
|
-
graphContainerStatus = null;
|
|
11877
|
-
/**
|
|
11878
|
-
* Check if the graph container is ready and warm.
|
|
11879
|
-
*
|
|
11880
|
-
* Sends a lightweight heartbeat RPC to the Session DO which internally
|
|
11881
|
-
* pings the FalkorDB container. The response includes `graphContainerStatus`,
|
|
11882
|
-
* which is stored locally and emitted as a `readiness` event.
|
|
11883
|
-
*
|
|
11884
|
-
* Use this method to proactively warm the graph container before any
|
|
11885
|
-
* GraphQL query that requires it, or to poll the container's state in
|
|
11886
|
-
* the background.
|
|
11887
|
-
*
|
|
11888
|
-
* @returns The current graph container status object
|
|
11889
|
-
*
|
|
11890
|
-
* @example
|
|
11891
|
-
* ```typescript
|
|
11892
|
-
* const status = await env.checkReadiness();
|
|
11893
|
-
* console.log(status.status); // 'hot' | 'warming' | 'unknown'
|
|
11894
|
-
*
|
|
11895
|
-
* // Or listen for live updates
|
|
11896
|
-
* env.on('readiness', (status) => {
|
|
11897
|
-
* console.log('Graph is now:', status.status);
|
|
11898
|
-
* });
|
|
11899
|
-
* ```
|
|
11900
|
-
*/
|
|
11901
|
-
async checkReadiness() {
|
|
11902
|
-
const result = await this.client.call("client.heartbeat", {});
|
|
11903
|
-
const containerStatus = result?.graphContainerStatus ?? {
|
|
11904
|
-
lastKeepAliveAt: Date.now(),
|
|
11905
|
-
status: "unknown"
|
|
11906
|
-
};
|
|
11907
|
-
this.graphContainerStatus = containerStatus;
|
|
11908
|
-
this.emit("readiness", containerStatus);
|
|
11909
|
-
return containerStatus;
|
|
11910
|
-
}
|
|
11911
11963
|
// ==================== ID ↔ GRAPH PATH MAPPING ====================
|
|
11912
11964
|
/**
|
|
11913
11965
|
* Convert a class name + real-world ID into a unique graph path.
|
|
@@ -11936,14 +11988,14 @@ var Environment = class extends Session {
|
|
|
11936
11988
|
}
|
|
11937
11989
|
/**
|
|
11938
11990
|
* Execute a GraphQL query against the environment's graph.
|
|
11939
|
-
*
|
|
11991
|
+
*
|
|
11940
11992
|
* The query uses the Granular graph query language (based on Cypher/GraphQL).
|
|
11941
11993
|
* Authentication is handled automatically using the SDK's API key.
|
|
11942
|
-
*
|
|
11994
|
+
*
|
|
11943
11995
|
* @param query - The GraphQL query string
|
|
11944
11996
|
* @param variables - Optional variables for the query
|
|
11945
11997
|
* @returns The query result data
|
|
11946
|
-
*
|
|
11998
|
+
*
|
|
11947
11999
|
* @example
|
|
11948
12000
|
* ```typescript
|
|
11949
12001
|
* // Read the workspace
|
|
@@ -11951,7 +12003,7 @@ var Environment = class extends Session {
|
|
|
11951
12003
|
* `query { model(path: "workspace") { path label submodels { path label } } }`
|
|
11952
12004
|
* );
|
|
11953
12005
|
* console.log(result.data);
|
|
11954
|
-
*
|
|
12006
|
+
*
|
|
11955
12007
|
* // Create a model
|
|
11956
12008
|
* const created = await env.graphql(
|
|
11957
12009
|
* `mutation { at(path: "workspace") { create_submodel(subpath: "my_node", label: "My Node", prototype: "Model") { model { path label } } } }`
|
|
@@ -11963,7 +12015,7 @@ var Environment = class extends Session {
|
|
|
11963
12015
|
method: "POST",
|
|
11964
12016
|
headers: {
|
|
11965
12017
|
"Content-Type": "application/json",
|
|
11966
|
-
|
|
12018
|
+
Authorization: `Bearer ${this._apiKey}`
|
|
11967
12019
|
},
|
|
11968
12020
|
body: JSON.stringify({
|
|
11969
12021
|
environmentId: this.environmentId,
|
|
@@ -11980,10 +12032,10 @@ var Environment = class extends Session {
|
|
|
11980
12032
|
// ==================== RELATIONSHIP METHODS ====================
|
|
11981
12033
|
/**
|
|
11982
12034
|
* Define a relationship between two model types.
|
|
11983
|
-
*
|
|
12035
|
+
*
|
|
11984
12036
|
* Creates both submodels (if they don't exist) and links them with
|
|
11985
12037
|
* a RelationshipDef node that encodes cardinality.
|
|
11986
|
-
*
|
|
12038
|
+
*
|
|
11987
12039
|
* @example
|
|
11988
12040
|
* ```typescript
|
|
11989
12041
|
* // Author has many Books, Book has one Author
|
|
@@ -12043,10 +12095,10 @@ var Environment = class extends Session {
|
|
|
12043
12095
|
}
|
|
12044
12096
|
/**
|
|
12045
12097
|
* Get all relationships for a model type.
|
|
12046
|
-
*
|
|
12098
|
+
*
|
|
12047
12099
|
* @param modelPath - The model type path (e.g., "author")
|
|
12048
12100
|
* @returns Array of relationships from this model's perspective
|
|
12049
|
-
*
|
|
12101
|
+
*
|
|
12050
12102
|
* @example
|
|
12051
12103
|
* ```typescript
|
|
12052
12104
|
* const rels = await env.getRelationships('author');
|
|
@@ -12079,18 +12131,18 @@ var Environment = class extends Session {
|
|
|
12079
12131
|
}
|
|
12080
12132
|
/**
|
|
12081
12133
|
* Attach a target model to a relationship submodel.
|
|
12082
|
-
*
|
|
12134
|
+
*
|
|
12083
12135
|
* Handles cardinality automatically:
|
|
12084
12136
|
* - "One" side: sets/replaces the reference
|
|
12085
12137
|
* - "Many" side: adds the target to the collection
|
|
12086
|
-
*
|
|
12138
|
+
*
|
|
12087
12139
|
* If the target model doesn't exist, it's created as an instance of the foreign type.
|
|
12088
12140
|
* Bidirectional sync is automatic.
|
|
12089
|
-
*
|
|
12141
|
+
*
|
|
12090
12142
|
* @param modelPath - The model instance path (e.g., "tolkien")
|
|
12091
12143
|
* @param submodelPath - The relationship submodel (e.g., "books")
|
|
12092
12144
|
* @param targetPath - The target model to attach (e.g., "lord_of_the_rings")
|
|
12093
|
-
*
|
|
12145
|
+
*
|
|
12094
12146
|
* @example
|
|
12095
12147
|
* ```typescript
|
|
12096
12148
|
* // Attach a book to an author (many side)
|
|
@@ -12117,18 +12169,18 @@ var Environment = class extends Session {
|
|
|
12117
12169
|
}
|
|
12118
12170
|
/**
|
|
12119
12171
|
* Detach a target model from a relationship submodel.
|
|
12120
|
-
*
|
|
12172
|
+
*
|
|
12121
12173
|
* Handles bidirectional cleanup automatically.
|
|
12122
|
-
*
|
|
12174
|
+
*
|
|
12123
12175
|
* @param modelPath - The model instance path
|
|
12124
12176
|
* @param submodelPath - The relationship submodel
|
|
12125
12177
|
* @param targetPath - The target to detach (optional for "one" side; omit on "many" side to detach all)
|
|
12126
|
-
*
|
|
12178
|
+
*
|
|
12127
12179
|
* @example
|
|
12128
12180
|
* ```typescript
|
|
12129
12181
|
* // Detach a specific book
|
|
12130
12182
|
* await env.detach('tolkien', 'books', 'lord_of_the_rings');
|
|
12131
|
-
*
|
|
12183
|
+
*
|
|
12132
12184
|
* // Detach all books
|
|
12133
12185
|
* await env.detach('tolkien', 'books');
|
|
12134
12186
|
* ```
|
|
@@ -12152,11 +12204,11 @@ var Environment = class extends Session {
|
|
|
12152
12204
|
}
|
|
12153
12205
|
/**
|
|
12154
12206
|
* List all related models through a relationship submodel.
|
|
12155
|
-
*
|
|
12207
|
+
*
|
|
12156
12208
|
* @param modelPath - The model instance path
|
|
12157
12209
|
* @param submodelPath - The relationship submodel
|
|
12158
12210
|
* @returns Array of related model references
|
|
12159
|
-
*
|
|
12211
|
+
*
|
|
12160
12212
|
* @example
|
|
12161
12213
|
* ```typescript
|
|
12162
12214
|
* const books = await env.listRelated('tolkien', 'books');
|
|
@@ -12180,14 +12232,14 @@ var Environment = class extends Session {
|
|
|
12180
12232
|
}
|
|
12181
12233
|
/**
|
|
12182
12234
|
* Apply a manifest to the current environment's graph.
|
|
12183
|
-
*
|
|
12235
|
+
*
|
|
12184
12236
|
* Translates each manifest operation into GraphQL mutations and executes them
|
|
12185
12237
|
* in order. This is the core mechanism for creating classes, fields, and
|
|
12186
12238
|
* relationships from a declarative manifest.
|
|
12187
|
-
*
|
|
12239
|
+
*
|
|
12188
12240
|
* @param manifest - The manifest content to apply
|
|
12189
12241
|
* @returns Summary of applied operations
|
|
12190
|
-
*
|
|
12242
|
+
*
|
|
12191
12243
|
* @example
|
|
12192
12244
|
* ```typescript
|
|
12193
12245
|
* await environment.applyManifest({
|
|
@@ -12225,12 +12277,16 @@ var Environment = class extends Session {
|
|
|
12225
12277
|
applied++;
|
|
12226
12278
|
} catch (err) {
|
|
12227
12279
|
if (!err.message?.includes("already exists")) {
|
|
12228
|
-
errors.push(
|
|
12280
|
+
errors.push(
|
|
12281
|
+
`Import ${imp.name} operation failed: ${err.message}`
|
|
12282
|
+
);
|
|
12229
12283
|
}
|
|
12230
12284
|
}
|
|
12231
12285
|
}
|
|
12232
12286
|
} else {
|
|
12233
|
-
errors.push(
|
|
12287
|
+
errors.push(
|
|
12288
|
+
`Unknown module: "${imp.name}" (only built-in modules are supported)`
|
|
12289
|
+
);
|
|
12234
12290
|
}
|
|
12235
12291
|
}
|
|
12236
12292
|
}
|
|
@@ -12314,7 +12370,10 @@ var Environment = class extends Session {
|
|
|
12314
12370
|
}
|
|
12315
12371
|
}
|
|
12316
12372
|
async _applyEffectMetamodels(toolPath, metamodels) {
|
|
12317
|
-
for (const mutation of buildEffectMetamodelMutations(
|
|
12373
|
+
for (const mutation of buildEffectMetamodelMutations(
|
|
12374
|
+
toolPath,
|
|
12375
|
+
metamodels
|
|
12376
|
+
)) {
|
|
12318
12377
|
await this._runGraphql(mutation.query, mutation.label);
|
|
12319
12378
|
}
|
|
12320
12379
|
}
|
|
@@ -12363,7 +12422,9 @@ var Environment = class extends Session {
|
|
|
12363
12422
|
}
|
|
12364
12423
|
if (eventType.payloadSchema?.properties) {
|
|
12365
12424
|
const fieldSpecs = {};
|
|
12366
|
-
for (const [propName, propSchema] of Object.entries(
|
|
12425
|
+
for (const [propName, propSchema] of Object.entries(
|
|
12426
|
+
eventType.payloadSchema.properties
|
|
12427
|
+
)) {
|
|
12367
12428
|
const schema = propSchema;
|
|
12368
12429
|
fieldSpecs[propName] = {
|
|
12369
12430
|
type: schema.type ?? "string",
|
|
@@ -12646,7 +12707,9 @@ var Environment = class extends Session {
|
|
|
12646
12707
|
const wave = plans.slice(waveStart, waveStart + concurrency);
|
|
12647
12708
|
await Promise.all(
|
|
12648
12709
|
wave.map(async (plan) => {
|
|
12649
|
-
const { items, durationMs } = await this.executeRecordObjectsChunk(
|
|
12710
|
+
const { items, durationMs } = await this.executeRecordObjectsChunk(
|
|
12711
|
+
plan.slice
|
|
12712
|
+
);
|
|
12650
12713
|
if (items.length !== plan.slice.length) {
|
|
12651
12714
|
throw new Error(
|
|
12652
12715
|
`recordObjects: chunk ${plan.chunkIndex + 1} returned ${items.length} results, expected ${plan.slice.length}`
|
|
@@ -12676,13 +12739,10 @@ var Environment = class extends Session {
|
|
|
12676
12739
|
let lastError;
|
|
12677
12740
|
for (let attempt = 1; attempt <= DEFAULT_DIRECT_RECORD_OBJECTS_RETRY_COUNT; attempt += 1) {
|
|
12678
12741
|
try {
|
|
12679
|
-
const response = await this.controlPlaneRequest(
|
|
12680
|
-
|
|
12681
|
-
{
|
|
12682
|
-
|
|
12683
|
-
body: JSON.stringify({ records: chunk })
|
|
12684
|
-
}
|
|
12685
|
-
);
|
|
12742
|
+
const response = await this.controlPlaneRequest(`/control/environments/${this.environmentId}/records/batch`, {
|
|
12743
|
+
method: "POST",
|
|
12744
|
+
body: JSON.stringify({ records: chunk })
|
|
12745
|
+
});
|
|
12686
12746
|
const items = Array.isArray(response.items) ? response.items : [];
|
|
12687
12747
|
return { items, durationMs: Date.now() - wallStart };
|
|
12688
12748
|
} catch (error) {
|
|
@@ -12745,7 +12805,9 @@ var Environment = class extends Session {
|
|
|
12745
12805
|
* Fetch a single record import by id.
|
|
12746
12806
|
*/
|
|
12747
12807
|
async getRecordImport(importId) {
|
|
12748
|
-
return this.controlPlaneRequest(
|
|
12808
|
+
return this.controlPlaneRequest(
|
|
12809
|
+
`/control/record-imports/${importId}`
|
|
12810
|
+
);
|
|
12749
12811
|
}
|
|
12750
12812
|
/**
|
|
12751
12813
|
* Cancel a queued/background record import.
|
|
@@ -12758,36 +12820,186 @@ var Environment = class extends Session {
|
|
|
12758
12820
|
}
|
|
12759
12821
|
);
|
|
12760
12822
|
}
|
|
12761
|
-
|
|
12823
|
+
};
|
|
12824
|
+
var EnvironmentSession = class extends Session {
|
|
12825
|
+
environment;
|
|
12826
|
+
/** The last known graph container status, updated by checkReadiness() or on heartbeat */
|
|
12827
|
+
graphContainerStatus = null;
|
|
12828
|
+
constructor(client, environment, clientId) {
|
|
12829
|
+
super(client, clientId);
|
|
12830
|
+
this.environment = environment;
|
|
12831
|
+
}
|
|
12832
|
+
get environmentId() {
|
|
12833
|
+
return this.environment.environmentId;
|
|
12834
|
+
}
|
|
12835
|
+
get sandboxId() {
|
|
12836
|
+
return this.environment.sandboxId;
|
|
12837
|
+
}
|
|
12838
|
+
get ontologyId() {
|
|
12839
|
+
return this.environment.ontologyId;
|
|
12840
|
+
}
|
|
12841
|
+
get subjectId() {
|
|
12842
|
+
return this.environment.subjectId;
|
|
12843
|
+
}
|
|
12844
|
+
get envName() {
|
|
12845
|
+
return this.environment.envName;
|
|
12846
|
+
}
|
|
12847
|
+
get versionId() {
|
|
12848
|
+
return this.environment.versionId;
|
|
12849
|
+
}
|
|
12850
|
+
get granularId() {
|
|
12851
|
+
return this.environment.granularId;
|
|
12852
|
+
}
|
|
12853
|
+
get permissionProfileId() {
|
|
12854
|
+
return this.environment.permissionProfileId;
|
|
12855
|
+
}
|
|
12856
|
+
get apiEndpoint() {
|
|
12857
|
+
return this.environment.apiEndpoint;
|
|
12858
|
+
}
|
|
12859
|
+
get data() {
|
|
12860
|
+
return this.environment.data;
|
|
12861
|
+
}
|
|
12862
|
+
get feedback() {
|
|
12863
|
+
return this.environment.feedback;
|
|
12864
|
+
}
|
|
12762
12865
|
/**
|
|
12763
|
-
*
|
|
12866
|
+
* Return a plain JS snapshot of the synced session heap.
|
|
12764
12867
|
*/
|
|
12765
|
-
|
|
12766
|
-
|
|
12868
|
+
getHeap() {
|
|
12869
|
+
const doc = this.document;
|
|
12870
|
+
return normalizeHeapSnapshot(doc?.heap);
|
|
12871
|
+
}
|
|
12872
|
+
async graphql(query, variables) {
|
|
12873
|
+
return this.environment.graphql(query, variables);
|
|
12874
|
+
}
|
|
12875
|
+
async defineRelationship(options) {
|
|
12876
|
+
return this.environment.defineRelationship(options);
|
|
12877
|
+
}
|
|
12878
|
+
async getRelationships(modelPath) {
|
|
12879
|
+
return this.environment.getRelationships(modelPath);
|
|
12880
|
+
}
|
|
12881
|
+
async attach(modelPath, submodelPath, targetPath) {
|
|
12882
|
+
return this.environment.attach(modelPath, submodelPath, targetPath);
|
|
12883
|
+
}
|
|
12884
|
+
async detach(modelPath, submodelPath, targetPath) {
|
|
12885
|
+
return this.environment.detach(modelPath, submodelPath, targetPath);
|
|
12886
|
+
}
|
|
12887
|
+
async listRelated(modelPath, submodelPath) {
|
|
12888
|
+
return this.environment.listRelated(modelPath, submodelPath);
|
|
12889
|
+
}
|
|
12890
|
+
async applyManifest(manifest) {
|
|
12891
|
+
return this.environment.applyManifest(manifest);
|
|
12892
|
+
}
|
|
12893
|
+
async recordObject(options) {
|
|
12894
|
+
return this.environment.recordObject(options);
|
|
12895
|
+
}
|
|
12896
|
+
async recordObjects(records, options) {
|
|
12897
|
+
return this.environment.recordObjects(records, options);
|
|
12898
|
+
}
|
|
12899
|
+
async enqueueRecordImport(records, options = {}) {
|
|
12900
|
+
return this.environment.enqueueRecordImport(records, options);
|
|
12901
|
+
}
|
|
12902
|
+
async listRecordImports(status) {
|
|
12903
|
+
return this.environment.listRecordImports(status);
|
|
12904
|
+
}
|
|
12905
|
+
async getRecordImportSummary() {
|
|
12906
|
+
return this.environment.getRecordImportSummary();
|
|
12907
|
+
}
|
|
12908
|
+
async getAwaitingRecordCount() {
|
|
12909
|
+
return this.environment.getAwaitingRecordCount();
|
|
12910
|
+
}
|
|
12911
|
+
async getRecordImport(importId) {
|
|
12912
|
+
return this.environment.getRecordImport(importId);
|
|
12913
|
+
}
|
|
12914
|
+
async cancelRecordImport(importId) {
|
|
12915
|
+
return this.environment.cancelRecordImport(importId);
|
|
12916
|
+
}
|
|
12917
|
+
async listFeedback() {
|
|
12918
|
+
return this.environment.listFeedback();
|
|
12767
12919
|
}
|
|
12768
12920
|
/**
|
|
12769
|
-
*
|
|
12921
|
+
* Close the session and disconnect from the sandbox.
|
|
12922
|
+
*
|
|
12923
|
+
* Sends `client.goodbye` over WebSocket first, then issues an HTTP fallback
|
|
12924
|
+
* to the runtime goodbye endpoint if no definitive WS-side runtime notify
|
|
12925
|
+
* acknowledgement was observed.
|
|
12770
12926
|
*/
|
|
12771
|
-
async
|
|
12772
|
-
|
|
12927
|
+
async disconnect() {
|
|
12928
|
+
let wsNotifiedRuntime = false;
|
|
12929
|
+
try {
|
|
12930
|
+
const goodbye = await this.rpc(
|
|
12931
|
+
"client.goodbye",
|
|
12932
|
+
{
|
|
12933
|
+
timestamp: Date.now()
|
|
12934
|
+
}
|
|
12935
|
+
);
|
|
12936
|
+
wsNotifiedRuntime = Boolean(goodbye?.ok && goodbye?.via);
|
|
12937
|
+
} catch {
|
|
12938
|
+
wsNotifiedRuntime = false;
|
|
12939
|
+
}
|
|
12940
|
+
if (!wsNotifiedRuntime) {
|
|
12941
|
+
try {
|
|
12942
|
+
await fetch(
|
|
12943
|
+
`${this.environment.runtimeBaseUrl}/orchestrator/runtime/environments/${this.environmentId}/session-goodbye`,
|
|
12944
|
+
{
|
|
12945
|
+
method: "POST",
|
|
12946
|
+
headers: {
|
|
12947
|
+
"Content-Type": "application/json",
|
|
12948
|
+
Authorization: `Bearer ${this.environment.authToken}`,
|
|
12949
|
+
Connection: "close"
|
|
12950
|
+
},
|
|
12951
|
+
body: JSON.stringify({
|
|
12952
|
+
reason: "sdk_disconnect_http_fallback",
|
|
12953
|
+
sessionId: this.client.currentSessionId
|
|
12954
|
+
})
|
|
12955
|
+
}
|
|
12956
|
+
);
|
|
12957
|
+
} catch {
|
|
12958
|
+
}
|
|
12959
|
+
}
|
|
12960
|
+
this.client.disconnect();
|
|
12773
12961
|
}
|
|
12774
12962
|
/**
|
|
12775
|
-
*
|
|
12963
|
+
* Close only the socket transport without sending `client.goodbye`.
|
|
12776
12964
|
*/
|
|
12777
|
-
|
|
12778
|
-
|
|
12965
|
+
disconnectTransport() {
|
|
12966
|
+
this.client.disconnect();
|
|
12779
12967
|
}
|
|
12780
12968
|
/**
|
|
12781
|
-
*
|
|
12969
|
+
* Backwards-compatible alias for `disconnect()`.
|
|
12782
12970
|
*/
|
|
12783
|
-
async
|
|
12784
|
-
|
|
12971
|
+
async close() {
|
|
12972
|
+
await this.disconnect();
|
|
12785
12973
|
}
|
|
12786
12974
|
/**
|
|
12787
|
-
*
|
|
12975
|
+
* Check if the graph container is ready and warm.
|
|
12788
12976
|
*/
|
|
12789
|
-
async
|
|
12790
|
-
|
|
12977
|
+
async checkReadiness() {
|
|
12978
|
+
const result = await this.client.call("client.heartbeat", {});
|
|
12979
|
+
const containerStatus = result?.graphContainerStatus ?? {
|
|
12980
|
+
lastKeepAliveAt: Date.now(),
|
|
12981
|
+
status: "unknown"
|
|
12982
|
+
};
|
|
12983
|
+
this.graphContainerStatus = containerStatus;
|
|
12984
|
+
this.emit("readiness", containerStatus);
|
|
12985
|
+
return containerStatus;
|
|
12986
|
+
}
|
|
12987
|
+
};
|
|
12988
|
+
var OntologyHandle = class {
|
|
12989
|
+
granular;
|
|
12990
|
+
ontologyNameOrId;
|
|
12991
|
+
constructor(granular, ontologyNameOrId) {
|
|
12992
|
+
this.granular = granular;
|
|
12993
|
+
this.ontologyNameOrId = ontologyNameOrId;
|
|
12994
|
+
}
|
|
12995
|
+
get effects() {
|
|
12996
|
+
return {
|
|
12997
|
+
register: async (effect) => this.granular.registerEffect(this.ontologyNameOrId, effect),
|
|
12998
|
+
registerMany: async (effects) => this.granular.registerEffects(this.ontologyNameOrId, effects),
|
|
12999
|
+
unregister: async (name) => this.granular.unregisterEffect(this.ontologyNameOrId, name),
|
|
13000
|
+
clear: async () => this.granular.unregisterAllEffects(this.ontologyNameOrId),
|
|
13001
|
+
disconnect: async () => this.granular.disconnectEffects(this.ontologyNameOrId)
|
|
13002
|
+
};
|
|
12791
13003
|
}
|
|
12792
13004
|
};
|
|
12793
13005
|
var Granular = class _Granular {
|
|
@@ -12812,7 +13024,9 @@ var Granular = class _Granular {
|
|
|
12812
13024
|
constructor(options) {
|
|
12813
13025
|
const auth = options.token ?? options.apiKey;
|
|
12814
13026
|
if (!auth) {
|
|
12815
|
-
throw new Error(
|
|
13027
|
+
throw new Error(
|
|
13028
|
+
"Granular client requires either apiKey or token. Set GRANULAR_API_KEY or GRANULAR_TOKEN, or pass one in options."
|
|
13029
|
+
);
|
|
12816
13030
|
}
|
|
12817
13031
|
this.apiUrl = resolveApiUrl(options.apiUrl, options.endpointMode);
|
|
12818
13032
|
this.apiKey = resolveAuthTokenForApiUrl(auth, this.apiUrl);
|
|
@@ -12822,12 +13036,18 @@ var Granular = class _Granular {
|
|
|
12822
13036
|
this.onReconnectError = options.onReconnectError;
|
|
12823
13037
|
this.httpUrl = this.apiUrl.replace(/^wss:\/\//, "https://").replace(/^ws:\/\//, "http://").replace(/\/ws$/, "");
|
|
12824
13038
|
}
|
|
13039
|
+
/**
|
|
13040
|
+
* Return an ontology-scoped handle for effects and other ontology-level APIs.
|
|
13041
|
+
*/
|
|
13042
|
+
ontology(ontologyNameOrId) {
|
|
13043
|
+
return new OntologyHandle(this, ontologyNameOrId);
|
|
13044
|
+
}
|
|
12825
13045
|
/**
|
|
12826
13046
|
* Records/upserts a user and prepares them for sandbox connections
|
|
12827
|
-
*
|
|
13047
|
+
*
|
|
12828
13048
|
* @param options - User options
|
|
12829
13049
|
* @returns The recorded user with both `userId` and `granularId`
|
|
12830
|
-
*
|
|
13050
|
+
*
|
|
12831
13051
|
* @example
|
|
12832
13052
|
* ```typescript
|
|
12833
13053
|
* const user = await granular.recordUser({
|
|
@@ -12838,14 +13058,16 @@ var Granular = class _Granular {
|
|
|
12838
13058
|
* ```
|
|
12839
13059
|
*/
|
|
12840
13060
|
async recordUser(options) {
|
|
12841
|
-
const subject = normalizeSubject(
|
|
12842
|
-
|
|
12843
|
-
|
|
12844
|
-
|
|
12845
|
-
|
|
12846
|
-
|
|
13061
|
+
const subject = normalizeSubject(
|
|
13062
|
+
await this.request("/control/subjects", {
|
|
13063
|
+
method: "POST",
|
|
13064
|
+
body: JSON.stringify({
|
|
13065
|
+
identityId: options.userId,
|
|
13066
|
+
name: options.name,
|
|
13067
|
+
email: options.email
|
|
13068
|
+
})
|
|
12847
13069
|
})
|
|
12848
|
-
|
|
13070
|
+
);
|
|
12849
13071
|
return normalizeUser({
|
|
12850
13072
|
granularId: subject.granularId,
|
|
12851
13073
|
userId: options.userId,
|
|
@@ -12856,7 +13078,23 @@ var Granular = class _Granular {
|
|
|
12856
13078
|
permissions: options.permissions || []
|
|
12857
13079
|
});
|
|
12858
13080
|
}
|
|
13081
|
+
/**
|
|
13082
|
+
* Alias for `recordUser()` with user-facing naming that matches upsert semantics.
|
|
13083
|
+
*/
|
|
13084
|
+
async upsertUser(options) {
|
|
13085
|
+
return this.recordUser(options);
|
|
13086
|
+
}
|
|
12859
13087
|
async resolveConnectUser(options) {
|
|
13088
|
+
const providedIdentityCount = [
|
|
13089
|
+
Boolean(options.user),
|
|
13090
|
+
Boolean(options.userId),
|
|
13091
|
+
Boolean(options.granularId)
|
|
13092
|
+
].filter(Boolean).length;
|
|
13093
|
+
if (providedIdentityCount !== 1) {
|
|
13094
|
+
throw new Error(
|
|
13095
|
+
"openEnvironment() requires exactly one of userId, granularId, or a user object returned by recordUser()."
|
|
13096
|
+
);
|
|
13097
|
+
}
|
|
12860
13098
|
if (options.user) {
|
|
12861
13099
|
const user = normalizeUser(options.user);
|
|
12862
13100
|
return {
|
|
@@ -12892,76 +13130,141 @@ var Granular = class _Granular {
|
|
|
12892
13130
|
permissions: options.permissions || []
|
|
12893
13131
|
};
|
|
12894
13132
|
}
|
|
12895
|
-
throw new Error(
|
|
13133
|
+
throw new Error(
|
|
13134
|
+
"openEnvironment() requires either userId, granularId, or a user object returned by recordUser()."
|
|
13135
|
+
);
|
|
12896
13136
|
}
|
|
12897
13137
|
/**
|
|
12898
|
-
*
|
|
12899
|
-
*
|
|
12900
|
-
* Effects are registered at the sandbox level via `granular.registerEffect()`
|
|
12901
|
-
* or `granular.registerEffects()`. Sessions pick up live availability from
|
|
12902
|
-
* the sandbox registry automatically.
|
|
12903
|
-
*
|
|
12904
|
-
* @param options - Connection options
|
|
12905
|
-
* @returns An active environment session
|
|
12906
|
-
*
|
|
13138
|
+
* Open or resolve an ontology environment for one user without opening a session.
|
|
13139
|
+
*
|
|
12907
13140
|
* @example
|
|
12908
13141
|
* ```typescript
|
|
12909
|
-
* const environment = await granular.
|
|
13142
|
+
* const environment = await granular.openEnvironment({
|
|
12910
13143
|
* ontology: 'my-ontology',
|
|
12911
|
-
*
|
|
13144
|
+
* tag: 'dev',
|
|
12912
13145
|
* userId: 'user_123',
|
|
12913
13146
|
* permissions: ['agent'],
|
|
12914
13147
|
* });
|
|
12915
|
-
*
|
|
12916
|
-
* await
|
|
12917
|
-
*
|
|
12918
|
-
*
|
|
12919
|
-
*
|
|
12920
|
-
* handler: async () => 'Hello!',
|
|
13148
|
+
*
|
|
13149
|
+
* await environment.data.record({
|
|
13150
|
+
* className: 'customer',
|
|
13151
|
+
* id: 'acme',
|
|
13152
|
+
* fields: { name: 'Acme' },
|
|
12921
13153
|
* });
|
|
12922
|
-
*
|
|
12923
|
-
*
|
|
12924
|
-
* const job = await
|
|
12925
|
-
*
|
|
12926
|
-
* return await tools.greet({});
|
|
12927
|
-
* `);
|
|
12928
|
-
*
|
|
12929
|
-
* console.log(await job.result); // 'Hello!'
|
|
13154
|
+
*
|
|
13155
|
+
* const session = await environment.sessions.create();
|
|
13156
|
+
* const job = await session.submitJob(`return "hello";`);
|
|
13157
|
+
* console.log(await job.result);
|
|
12930
13158
|
* ```
|
|
12931
|
-
|
|
13159
|
+
*/
|
|
13160
|
+
async openEnvironment(options) {
|
|
13161
|
+
const envData = await this.resolveOpenEnvironmentData(
|
|
13162
|
+
options,
|
|
13163
|
+
"openEnvironment"
|
|
13164
|
+
);
|
|
13165
|
+
return this.bindEnvironmentHandle(envData);
|
|
13166
|
+
}
|
|
13167
|
+
/**
|
|
13168
|
+
* Deprecated compatibility alias for `openEnvironment()`.
|
|
13169
|
+
*
|
|
13170
|
+
* `connect()` no longer opens a runtime session automatically.
|
|
13171
|
+
*/
|
|
12932
13172
|
async connect(options) {
|
|
12933
|
-
|
|
13173
|
+
return this.openEnvironment({
|
|
13174
|
+
...options,
|
|
13175
|
+
tag: this.resolveRequestedTag(options, "connect"),
|
|
13176
|
+
permissions: options.permissions || options.user?.permissions || []
|
|
13177
|
+
});
|
|
13178
|
+
}
|
|
13179
|
+
resolveRequestedTag(options, methodName) {
|
|
13180
|
+
const tag = options.tag?.trim() || options.tagName?.trim() || options.environment?.trim();
|
|
13181
|
+
if (!tag) {
|
|
13182
|
+
throw new Error(`${methodName}() requires \`tag\`.`);
|
|
13183
|
+
}
|
|
13184
|
+
return tag;
|
|
13185
|
+
}
|
|
13186
|
+
buildManagedEnvironmentName(tag, versionId) {
|
|
13187
|
+
return `__sdk__${tag}__${versionId}`;
|
|
13188
|
+
}
|
|
13189
|
+
matchesTagTrackedEnvironment(environment, tagName, tagId) {
|
|
13190
|
+
const environmentTagName = environment.tag?.name || environment.buildPolicy.tagName || null;
|
|
13191
|
+
return environment.tagId === tagId || environmentTagName === tagName || environment.environment === tagName || environment.envName === tagName || environment.environment === this.buildManagedEnvironmentName(tagName, environment.versionId) || environment.envName === this.buildManagedEnvironmentName(tagName, environment.versionId);
|
|
13192
|
+
}
|
|
13193
|
+
sortEnvironmentsByRecency(environments) {
|
|
13194
|
+
return [...environments].sort(
|
|
13195
|
+
(left, right) => right.updatedAt - left.updatedAt
|
|
13196
|
+
);
|
|
13197
|
+
}
|
|
13198
|
+
async resolveOpenEnvironmentData(options, methodName) {
|
|
12934
13199
|
const ontology = options.ontology;
|
|
12935
13200
|
if (!ontology) {
|
|
12936
|
-
throw new Error(
|
|
13201
|
+
throw new Error(`${methodName}() requires \`ontology\`.`);
|
|
12937
13202
|
}
|
|
12938
|
-
const
|
|
12939
|
-
if (!
|
|
12940
|
-
throw new Error(
|
|
13203
|
+
const tagName = options.tag?.trim();
|
|
13204
|
+
if (!tagName) {
|
|
13205
|
+
throw new Error(`${methodName}() requires \`tag\`.`);
|
|
12941
13206
|
}
|
|
12942
|
-
const tagName = options.tagName?.trim() || void 0;
|
|
12943
13207
|
const user = await this.resolveConnectUser(options);
|
|
13208
|
+
if (!Array.isArray(user.permissions) || user.permissions.length === 0) {
|
|
13209
|
+
throw new Error(
|
|
13210
|
+
`${methodName}() requires at least one permission so the SDK can ensure assignments for new users.`
|
|
13211
|
+
);
|
|
13212
|
+
}
|
|
12944
13213
|
const sandbox = await this.findOrCreateSandbox(ontology);
|
|
12945
13214
|
for (const profileName of user.permissions) {
|
|
12946
|
-
const profileId = await this.ensurePermissionProfile(
|
|
12947
|
-
|
|
13215
|
+
const profileId = await this.ensurePermissionProfile(
|
|
13216
|
+
sandbox.sandboxId,
|
|
13217
|
+
profileName
|
|
13218
|
+
);
|
|
13219
|
+
await this.ensureAssignment(
|
|
13220
|
+
user.granularId,
|
|
13221
|
+
sandbox.sandboxId,
|
|
13222
|
+
profileId
|
|
13223
|
+
);
|
|
13224
|
+
}
|
|
13225
|
+
const tags = await this.request(
|
|
13226
|
+
`/control/sandboxes/${sandbox.sandboxId}/tags`
|
|
13227
|
+
);
|
|
13228
|
+
const tag = (tags.items || []).find(
|
|
13229
|
+
(candidate) => Boolean(candidate?.name === tagName)
|
|
13230
|
+
);
|
|
13231
|
+
if (!tag) {
|
|
13232
|
+
throw new Error(
|
|
13233
|
+
`Tag "${tagName}" was not found for ontology ${sandbox.sandboxId}.`
|
|
13234
|
+
);
|
|
13235
|
+
}
|
|
13236
|
+
const targetVersionId = tag.targetVersionId || tag.targetBuildId;
|
|
13237
|
+
if (!targetVersionId) {
|
|
13238
|
+
throw new Error(
|
|
13239
|
+
`Tag "${tagName}" does not currently point to a build/version.`
|
|
13240
|
+
);
|
|
13241
|
+
}
|
|
13242
|
+
const allEnvironments = await this.environments.list(sandbox.sandboxId);
|
|
13243
|
+
const userEnvironments = allEnvironments.filter(
|
|
13244
|
+
(environment) => environment.subjectId === user.granularId
|
|
13245
|
+
);
|
|
13246
|
+
const currentMatches = this.sortEnvironmentsByRecency(
|
|
13247
|
+
userEnvironments.filter(
|
|
13248
|
+
(environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag.tagId) && environment.versionId === targetVersionId
|
|
13249
|
+
)
|
|
13250
|
+
);
|
|
13251
|
+
if (currentMatches.length > 0) {
|
|
13252
|
+
return currentMatches[0];
|
|
13253
|
+
}
|
|
13254
|
+
const outdatedMatches = this.sortEnvironmentsByRecency(
|
|
13255
|
+
userEnvironments.filter(
|
|
13256
|
+
(environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag.tagId)
|
|
13257
|
+
)
|
|
13258
|
+
);
|
|
13259
|
+
if (outdatedMatches.length > 0 && options.createFreshIfOutdated !== true) {
|
|
13260
|
+
return outdatedMatches[0];
|
|
12948
13261
|
}
|
|
12949
|
-
|
|
13262
|
+
return this.environments.create(sandbox.sandboxId, {
|
|
12950
13263
|
subjectId: user.granularId,
|
|
12951
|
-
environment:
|
|
12952
|
-
|
|
13264
|
+
environment: this.buildManagedEnvironmentName(tagName, targetVersionId),
|
|
13265
|
+
tagId: tag.tagId,
|
|
12953
13266
|
permissionProfileId: null
|
|
12954
13267
|
});
|
|
12955
|
-
await this.activateEnvironment(envData.environmentId);
|
|
12956
|
-
const session = await this.request("/ws/sessions", {
|
|
12957
|
-
method: "POST",
|
|
12958
|
-
body: JSON.stringify({
|
|
12959
|
-
environmentId: envData.environmentId,
|
|
12960
|
-
clientId,
|
|
12961
|
-
initialHeap: options.initialHeap
|
|
12962
|
-
})
|
|
12963
|
-
});
|
|
12964
|
-
return this.bindWebSocketEnvironment(envData, clientId, session);
|
|
12965
13268
|
}
|
|
12966
13269
|
/**
|
|
12967
13270
|
* List active (open) sessions for an environment — each session is one agent conversation thread.
|
|
@@ -12996,7 +13299,9 @@ var Granular = class _Granular {
|
|
|
12996
13299
|
createdAt: _Granular.coerceIsoDate(row.createdAt ?? row.created_at),
|
|
12997
13300
|
lastSeenAt: _Granular.coerceIsoDate(row.lastSeenAt ?? row.last_seen_at),
|
|
12998
13301
|
summary: row.summary != null ? String(row.summary) : null,
|
|
12999
|
-
summaryUpdatedAt: row.summaryUpdatedAt != null || row.summary_updated_at != null ? _Granular.coerceIsoDate(
|
|
13302
|
+
summaryUpdatedAt: row.summaryUpdatedAt != null || row.summary_updated_at != null ? _Granular.coerceIsoDate(
|
|
13303
|
+
row.summaryUpdatedAt ?? row.summary_updated_at
|
|
13304
|
+
) : null,
|
|
13000
13305
|
subjectId: row.subjectId != null ? String(row.subjectId) : null,
|
|
13001
13306
|
jobCount: typeof row.jobCount === "number" ? row.jobCount : void 0,
|
|
13002
13307
|
toolCallCount: typeof row.toolCallCount === "number" ? row.toolCallCount : void 0
|
|
@@ -13022,6 +13327,7 @@ var Granular = class _Granular {
|
|
|
13022
13327
|
const clientId = options.clientId || `client_${Date.now()}`;
|
|
13023
13328
|
await this.activateEnvironment(options.environmentId);
|
|
13024
13329
|
const envData = await this.environments.get(options.environmentId);
|
|
13330
|
+
const environment = this.bindEnvironmentHandle(envData);
|
|
13025
13331
|
const session = await this.request("/ws/sessions", {
|
|
13026
13332
|
method: "POST",
|
|
13027
13333
|
body: JSON.stringify({
|
|
@@ -13030,7 +13336,7 @@ var Granular = class _Granular {
|
|
|
13030
13336
|
initialHeap: options.initialHeap
|
|
13031
13337
|
})
|
|
13032
13338
|
});
|
|
13033
|
-
return this.
|
|
13339
|
+
return this.bindWebSocketEnvironmentSession(environment, clientId, session);
|
|
13034
13340
|
}
|
|
13035
13341
|
/**
|
|
13036
13342
|
* Connect to an existing open session (same conversation thread) using a freshly minted WebSocket token.
|
|
@@ -13042,7 +13348,8 @@ var Granular = class _Granular {
|
|
|
13042
13348
|
body: JSON.stringify({})
|
|
13043
13349
|
});
|
|
13044
13350
|
const envData = await this.environments.get(minted.environmentId);
|
|
13045
|
-
|
|
13351
|
+
const environment = this.bindEnvironmentHandle(envData);
|
|
13352
|
+
return this.bindWebSocketEnvironmentSession(environment, clientId, minted);
|
|
13046
13353
|
}
|
|
13047
13354
|
/**
|
|
13048
13355
|
* Mark a session closed in the control plane. If `environment` is the connected handle for that
|
|
@@ -13074,7 +13381,11 @@ var Granular = class _Granular {
|
|
|
13074
13381
|
});
|
|
13075
13382
|
return this.connectSession({ sessionId, clientId: options?.clientId });
|
|
13076
13383
|
}
|
|
13077
|
-
|
|
13384
|
+
bindEnvironmentHandle(envData) {
|
|
13385
|
+
const graphqlEndpoint = `${this.httpUrl}/orchestrator/graphql`;
|
|
13386
|
+
return new Environment(this, envData, this.apiKey, graphqlEndpoint);
|
|
13387
|
+
}
|
|
13388
|
+
async bindWebSocketEnvironmentSession(environment, clientId, session) {
|
|
13078
13389
|
const client = new WSClient({
|
|
13079
13390
|
url: session.wsUrl,
|
|
13080
13391
|
sessionId: session.sessionId,
|
|
@@ -13085,10 +13396,13 @@ var Granular = class _Granular {
|
|
|
13085
13396
|
onReconnectError: this.onReconnectError
|
|
13086
13397
|
});
|
|
13087
13398
|
await client.connect();
|
|
13088
|
-
const
|
|
13089
|
-
|
|
13090
|
-
|
|
13091
|
-
|
|
13399
|
+
const environmentSession = new EnvironmentSession(
|
|
13400
|
+
client,
|
|
13401
|
+
environment,
|
|
13402
|
+
clientId
|
|
13403
|
+
);
|
|
13404
|
+
await environmentSession.hello();
|
|
13405
|
+
return environmentSession;
|
|
13092
13406
|
}
|
|
13093
13407
|
async activateEnvironment(environmentId) {
|
|
13094
13408
|
await this.request(`/orchestrator/runtime/environments/${environmentId}/activate`, {
|
|
@@ -13120,14 +13434,18 @@ var Granular = class _Granular {
|
|
|
13120
13434
|
};
|
|
13121
13435
|
}
|
|
13122
13436
|
async publishSandboxEffectCatalog(host) {
|
|
13123
|
-
const effects = Array.from(
|
|
13124
|
-
(
|
|
13125
|
-
);
|
|
13126
|
-
const result = await host.wsClient.call("effects.publishCatalog", {
|
|
13437
|
+
const effects = Array.from(
|
|
13438
|
+
this.getSandboxEffectMap(host.sandboxId).values()
|
|
13439
|
+
).map((effect) => this.serializeEffect(effect));
|
|
13440
|
+
const result = await host.wsClient.call("effects.publishCatalog", {
|
|
13441
|
+
effects
|
|
13442
|
+
});
|
|
13127
13443
|
const acceptedCount = typeof result?.acceptedCount === "number" ? result.acceptedCount : 0;
|
|
13128
13444
|
const rejected = Array.isArray(result?.rejected) ? result.rejected : [];
|
|
13129
13445
|
if (acceptedCount === 0 && rejected.length > 0) {
|
|
13130
|
-
const detail = rejected.map(
|
|
13446
|
+
const detail = rejected.map(
|
|
13447
|
+
(entry) => `${entry.name || "unknown"}: ${entry.reason || "rejected"}`
|
|
13448
|
+
).join("; ");
|
|
13131
13449
|
throw new Error(
|
|
13132
13450
|
`Failed to publish live effects for sandbox ${host.sandboxId}: ${detail}`
|
|
13133
13451
|
);
|
|
@@ -13161,13 +13479,15 @@ var Granular = class _Granular {
|
|
|
13161
13479
|
disconnectError
|
|
13162
13480
|
);
|
|
13163
13481
|
}
|
|
13164
|
-
void this.ensureSandboxEffectHost(host.sandboxId).catch(
|
|
13165
|
-
|
|
13166
|
-
|
|
13167
|
-
|
|
13168
|
-
|
|
13169
|
-
|
|
13170
|
-
|
|
13482
|
+
void this.ensureSandboxEffectHost(host.sandboxId).catch(
|
|
13483
|
+
(reconnectError) => {
|
|
13484
|
+
console.error(
|
|
13485
|
+
`[Granular] Failed to recover effect host for sandbox ${host.sandboxId} after heartbeat failure:`,
|
|
13486
|
+
reconnectError
|
|
13487
|
+
);
|
|
13488
|
+
console.error("[Granular] Original heartbeat failure:", error);
|
|
13489
|
+
}
|
|
13490
|
+
);
|
|
13171
13491
|
}
|
|
13172
13492
|
startEffectHostHeartbeat(host) {
|
|
13173
13493
|
if (host.heartbeatTimer) {
|
|
@@ -13188,9 +13508,15 @@ var Granular = class _Granular {
|
|
|
13188
13508
|
host.heartbeatInFlight = false;
|
|
13189
13509
|
});
|
|
13190
13510
|
};
|
|
13191
|
-
sendHeartbeat(
|
|
13511
|
+
sendHeartbeat(
|
|
13512
|
+
"[Granular] Initial effect host heartbeat failed for sandbox",
|
|
13513
|
+
false
|
|
13514
|
+
);
|
|
13192
13515
|
host.heartbeatTimer = setInterval(() => {
|
|
13193
|
-
sendHeartbeat(
|
|
13516
|
+
sendHeartbeat(
|
|
13517
|
+
"[Granular] Effect host heartbeat failed for sandbox",
|
|
13518
|
+
true
|
|
13519
|
+
);
|
|
13194
13520
|
}, 1e4);
|
|
13195
13521
|
}
|
|
13196
13522
|
stopEffectHostHeartbeat(host) {
|
|
@@ -13222,7 +13548,12 @@ var Granular = class _Granular {
|
|
|
13222
13548
|
const effectClientId = crypto.randomUUID();
|
|
13223
13549
|
const clientId = `effect-host:${sandboxId}:${effectClientId}`;
|
|
13224
13550
|
const wsClient = new WSClient({
|
|
13225
|
-
url: buildEffectHostUrl(
|
|
13551
|
+
url: buildEffectHostUrl(
|
|
13552
|
+
this.apiUrl,
|
|
13553
|
+
sandboxId,
|
|
13554
|
+
effectClientId,
|
|
13555
|
+
clientId
|
|
13556
|
+
),
|
|
13226
13557
|
sessionId: `effect-host:${effectClientId}`,
|
|
13227
13558
|
token: this.apiKey,
|
|
13228
13559
|
tokenProvider: this.tokenProvider,
|
|
@@ -13241,7 +13572,10 @@ var Granular = class _Granular {
|
|
|
13241
13572
|
};
|
|
13242
13573
|
wsClient.registerRpcHandler("effect.invoke", async (params) => {
|
|
13243
13574
|
const request = params;
|
|
13244
|
-
return invokeRegisteredEffect(
|
|
13575
|
+
return invokeRegisteredEffect(
|
|
13576
|
+
this.getSandboxEffectMap(sandboxId),
|
|
13577
|
+
request
|
|
13578
|
+
);
|
|
13245
13579
|
});
|
|
13246
13580
|
wsClient.on("open", () => {
|
|
13247
13581
|
void this.synchronizeEffectHost(host).catch((error) => {
|
|
@@ -13289,7 +13623,7 @@ var Granular = class _Granular {
|
|
|
13289
13623
|
}
|
|
13290
13624
|
/**
|
|
13291
13625
|
* Register multiple effects (tools) for a specific sandbox.
|
|
13292
|
-
*
|
|
13626
|
+
*
|
|
13293
13627
|
* batch version of `registerEffect`.
|
|
13294
13628
|
*/
|
|
13295
13629
|
async registerEffects(sandboxNameOrId, effects) {
|
|
@@ -13303,7 +13637,7 @@ var Granular = class _Granular {
|
|
|
13303
13637
|
}
|
|
13304
13638
|
/**
|
|
13305
13639
|
* Unregister an effect from a sandbox.
|
|
13306
|
-
*
|
|
13640
|
+
*
|
|
13307
13641
|
* Removes it from the local sandbox registry and updates the
|
|
13308
13642
|
* sandbox-scoped live catalog.
|
|
13309
13643
|
*/
|
|
@@ -13402,27 +13736,31 @@ var Granular = class _Granular {
|
|
|
13402
13736
|
const assignments = await this.request(
|
|
13403
13737
|
`/control/subjects/${subjectId}/assignments`
|
|
13404
13738
|
);
|
|
13405
|
-
const existing = assignments.items.find(
|
|
13406
|
-
(a) => a.sandboxId === sandboxId
|
|
13407
|
-
);
|
|
13739
|
+
const existing = assignments.items.find((a) => a.sandboxId === sandboxId);
|
|
13408
13740
|
if (existing) {
|
|
13409
13741
|
if (existing.permissionProfileId === permissionProfileId) {
|
|
13410
13742
|
return;
|
|
13411
13743
|
}
|
|
13412
|
-
await this.request(
|
|
13413
|
-
|
|
13414
|
-
|
|
13744
|
+
await this.request(
|
|
13745
|
+
`/control/assignments/${existing.assignmentId}`,
|
|
13746
|
+
{
|
|
13747
|
+
method: "DELETE"
|
|
13748
|
+
}
|
|
13749
|
+
);
|
|
13415
13750
|
}
|
|
13416
13751
|
} catch {
|
|
13417
13752
|
}
|
|
13418
|
-
await this.request(
|
|
13419
|
-
|
|
13420
|
-
|
|
13421
|
-
|
|
13422
|
-
|
|
13423
|
-
|
|
13424
|
-
|
|
13425
|
-
|
|
13753
|
+
await this.request(
|
|
13754
|
+
`/control/subjects/${subjectId}/assignments`,
|
|
13755
|
+
{
|
|
13756
|
+
method: "POST",
|
|
13757
|
+
body: JSON.stringify({
|
|
13758
|
+
sandboxId,
|
|
13759
|
+
subjectId,
|
|
13760
|
+
permissionProfileId
|
|
13761
|
+
})
|
|
13762
|
+
}
|
|
13763
|
+
);
|
|
13426
13764
|
}
|
|
13427
13765
|
/**
|
|
13428
13766
|
* Sandbox management API
|
|
@@ -13502,23 +13840,33 @@ var Granular = class _Granular {
|
|
|
13502
13840
|
},
|
|
13503
13841
|
get: async (environmentId) => {
|
|
13504
13842
|
return normalizeEnvironmentData(
|
|
13505
|
-
await this.request(
|
|
13843
|
+
await this.request(
|
|
13844
|
+
`/control/environments/${environmentId}`
|
|
13845
|
+
)
|
|
13506
13846
|
);
|
|
13507
13847
|
},
|
|
13508
13848
|
create: async (sandboxId, data) => {
|
|
13509
13849
|
const environmentName = data.environment || data.envName;
|
|
13510
|
-
return normalizeEnvironmentData(
|
|
13511
|
-
|
|
13512
|
-
|
|
13513
|
-
|
|
13514
|
-
|
|
13515
|
-
|
|
13516
|
-
|
|
13850
|
+
return normalizeEnvironmentData(
|
|
13851
|
+
await this.request(
|
|
13852
|
+
`/control/sandboxes/${sandboxId}/environments`,
|
|
13853
|
+
{
|
|
13854
|
+
method: "POST",
|
|
13855
|
+
body: JSON.stringify({
|
|
13856
|
+
...data,
|
|
13857
|
+
envName: environmentName
|
|
13858
|
+
})
|
|
13859
|
+
}
|
|
13860
|
+
)
|
|
13861
|
+
);
|
|
13517
13862
|
},
|
|
13518
13863
|
delete: async (environmentId) => {
|
|
13519
|
-
return this.request(
|
|
13520
|
-
|
|
13521
|
-
|
|
13864
|
+
return this.request(
|
|
13865
|
+
`/control/environments/${environmentId}`,
|
|
13866
|
+
{
|
|
13867
|
+
method: "DELETE"
|
|
13868
|
+
}
|
|
13869
|
+
);
|
|
13522
13870
|
}
|
|
13523
13871
|
};
|
|
13524
13872
|
}
|
|
@@ -13538,10 +13886,13 @@ var Granular = class _Granular {
|
|
|
13538
13886
|
}
|
|
13539
13887
|
if (params.since) query.set("since", params.since.toISOString());
|
|
13540
13888
|
if (params.until) query.set("until", params.until.toISOString());
|
|
13541
|
-
if (params.isAcked !== void 0)
|
|
13889
|
+
if (params.isAcked !== void 0)
|
|
13890
|
+
query.set("isAcked", params.isAcked ? "1" : "0");
|
|
13542
13891
|
if (params.limit) query.set("limit", String(params.limit));
|
|
13543
13892
|
if (params.offset) query.set("offset", String(params.offset));
|
|
13544
|
-
const result = await this.request(
|
|
13893
|
+
const result = await this.request(
|
|
13894
|
+
`/control/stream-events?${query.toString()}`
|
|
13895
|
+
);
|
|
13545
13896
|
return (result.items || []).map((row) => ({
|
|
13546
13897
|
eventId: row.event_id,
|
|
13547
13898
|
streamName: row.stream_name,
|
|
@@ -13572,7 +13923,9 @@ var Granular = class _Granular {
|
|
|
13572
13923
|
since: cursor,
|
|
13573
13924
|
limit: 100
|
|
13574
13925
|
});
|
|
13575
|
-
const orderedEvents = [...events].sort(
|
|
13926
|
+
const orderedEvents = [...events].sort(
|
|
13927
|
+
(a, b) => a.createdAt - b.createdAt
|
|
13928
|
+
);
|
|
13576
13929
|
for (const event of orderedEvents) {
|
|
13577
13930
|
if (seenEventIds.has(event.eventId)) {
|
|
13578
13931
|
continue;
|
|
@@ -13585,15 +13938,19 @@ var Granular = class _Granular {
|
|
|
13585
13938
|
params.onEvent(event);
|
|
13586
13939
|
}
|
|
13587
13940
|
} catch (err) {
|
|
13588
|
-
params.onError?.(
|
|
13941
|
+
params.onError?.(
|
|
13942
|
+
err instanceof Error ? err : new Error(String(err))
|
|
13943
|
+
);
|
|
13589
13944
|
}
|
|
13590
13945
|
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
13591
13946
|
}
|
|
13592
13947
|
};
|
|
13593
13948
|
poll();
|
|
13594
|
-
return {
|
|
13595
|
-
|
|
13596
|
-
|
|
13949
|
+
return {
|
|
13950
|
+
unsubscribe: () => {
|
|
13951
|
+
running = false;
|
|
13952
|
+
}
|
|
13953
|
+
};
|
|
13597
13954
|
},
|
|
13598
13955
|
ack: async (eventId) => {
|
|
13599
13956
|
await this.request("/control/stream-events/ack", {
|
|
@@ -13611,7 +13968,9 @@ var Granular = class _Granular {
|
|
|
13611
13968
|
const sandbox = await this._resolveSandboxId(params.ontology);
|
|
13612
13969
|
const query = new URLSearchParams({ sandboxId: sandbox });
|
|
13613
13970
|
if (params.environment) query.set("environmentId", params.environment);
|
|
13614
|
-
const result = await this.request(
|
|
13971
|
+
const result = await this.request(
|
|
13972
|
+
`/control/stream-events/stats?${query.toString()}`
|
|
13973
|
+
);
|
|
13615
13974
|
return (result.items || []).map((row) => ({
|
|
13616
13975
|
streamName: row.stream_name,
|
|
13617
13976
|
eventType: row.event_type,
|
|
@@ -13629,10 +13988,14 @@ var Granular = class _Granular {
|
|
|
13629
13988
|
get subjects() {
|
|
13630
13989
|
return {
|
|
13631
13990
|
get: async (subjectId) => {
|
|
13632
|
-
return normalizeSubject(
|
|
13991
|
+
return normalizeSubject(
|
|
13992
|
+
await this.request(`/control/subjects/${subjectId}`)
|
|
13993
|
+
);
|
|
13633
13994
|
},
|
|
13634
13995
|
listAssignments: async (subjectId) => {
|
|
13635
|
-
return this.request(
|
|
13996
|
+
return this.request(
|
|
13997
|
+
`/control/subjects/${subjectId}/assignments`
|
|
13998
|
+
);
|
|
13636
13999
|
}
|
|
13637
14000
|
};
|
|
13638
14001
|
}
|
|
@@ -13642,24 +14005,31 @@ var Granular = class _Granular {
|
|
|
13642
14005
|
get users() {
|
|
13643
14006
|
return {
|
|
13644
14007
|
create: async (data) => {
|
|
13645
|
-
return normalizeSubject(
|
|
13646
|
-
|
|
13647
|
-
|
|
13648
|
-
|
|
13649
|
-
|
|
13650
|
-
|
|
14008
|
+
return normalizeSubject(
|
|
14009
|
+
await this.request("/control/subjects", {
|
|
14010
|
+
method: "POST",
|
|
14011
|
+
body: JSON.stringify({
|
|
14012
|
+
identityId: data.id,
|
|
14013
|
+
name: data.name,
|
|
14014
|
+
email: data.email
|
|
14015
|
+
})
|
|
13651
14016
|
})
|
|
13652
|
-
|
|
14017
|
+
);
|
|
13653
14018
|
},
|
|
13654
14019
|
get: async (id) => {
|
|
13655
|
-
return normalizeSubject(
|
|
14020
|
+
return normalizeSubject(
|
|
14021
|
+
await this.request(`/control/subjects/${id}`)
|
|
14022
|
+
);
|
|
13656
14023
|
}
|
|
13657
14024
|
};
|
|
13658
14025
|
}
|
|
13659
14026
|
async _resolveSandboxId(ontologyNameOrId) {
|
|
13660
14027
|
if (ontologyNameOrId.startsWith("sbx_")) return ontologyNameOrId;
|
|
13661
|
-
const result = await this.request(
|
|
13662
|
-
|
|
14028
|
+
const result = await this.request(
|
|
14029
|
+
`/control/sandboxes?name=${encodeURIComponent(ontologyNameOrId)}`
|
|
14030
|
+
);
|
|
14031
|
+
if (result.items.length === 0)
|
|
14032
|
+
throw new Error(`Ontology not found: ${ontologyNameOrId}`);
|
|
13663
14033
|
return result.items[0].sandboxId;
|
|
13664
14034
|
}
|
|
13665
14035
|
/**
|
|
@@ -13674,9 +14044,9 @@ var Granular = class _Granular {
|
|
|
13674
14044
|
const response = await fetch(url, {
|
|
13675
14045
|
...options,
|
|
13676
14046
|
headers: {
|
|
13677
|
-
|
|
14047
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
13678
14048
|
"Content-Type": "application/json",
|
|
13679
|
-
|
|
14049
|
+
Connection: "close",
|
|
13680
14050
|
...options.headers
|
|
13681
14051
|
}
|
|
13682
14052
|
});
|
|
@@ -13687,7 +14057,11 @@ var Granular = class _Granular {
|
|
|
13687
14057
|
return response.json();
|
|
13688
14058
|
}
|
|
13689
14059
|
const errorText = await response.text();
|
|
13690
|
-
const retryable = isRetryableLocalWorkerRestart(
|
|
14060
|
+
const retryable = isRetryableLocalWorkerRestart(
|
|
14061
|
+
response.status,
|
|
14062
|
+
errorText,
|
|
14063
|
+
url
|
|
14064
|
+
);
|
|
13691
14065
|
if (retryable && attempt < LOCAL_CONTROL_REQUEST_RETRY_COUNT) {
|
|
13692
14066
|
if (this.debugHttp) {
|
|
13693
14067
|
console.warn(
|
|
@@ -13826,21 +14200,6 @@ function reviewGeneratedJobCode(code) {
|
|
|
13826
14200
|
message: "After await loop.ask_user(...) returns a usable answer, continue the workflow in the same resumed run instead of stopping with placeholder text about doing the work later."
|
|
13827
14201
|
});
|
|
13828
14202
|
}
|
|
13829
|
-
const askUserCalls = normalized.match(/await\s+loop\.ask_user\s*\(\s*\{[\s\S]*?\}\s*\)/g) || [];
|
|
13830
|
-
for (const call of askUserCalls) {
|
|
13831
|
-
const usesChoiceType = /type\s*:\s*['"]choice['"]/.test(call);
|
|
13832
|
-
const usesInputType = /type\s*:\s*['"]input['"]/.test(call);
|
|
13833
|
-
const hasDisambiguationLanguage = /(which|choose|pick|select)/i.test(call) && /(invoice|order|shipment|request|case|work[\s_-]?order)/i.test(call);
|
|
13834
|
-
const includesShortlistOptions = /options\s*:\s*\[/.test(call);
|
|
13835
|
-
if (!usesChoiceType && (usesInputType || hasDisambiguationLanguage || includesShortlistOptions)) {
|
|
13836
|
-
issues.push({
|
|
13837
|
-
code: "disambiguation_requires_choice",
|
|
13838
|
-
severity: "error",
|
|
13839
|
-
message: "When asking the user to choose between known concrete records such as invoices, orders, shipments, requests, cases, or work orders, use loop.ask_user({ type: 'choice', options: [...] }) with a short explicit shortlist instead of a free-text input."
|
|
13840
|
-
});
|
|
13841
|
-
break;
|
|
13842
|
-
}
|
|
13843
|
-
}
|
|
13844
14203
|
}
|
|
13845
14204
|
const hasConversationalReturn = /return\s+[`'"]/.test(normalized) || /\breply\s*:/.test(normalized) || /\bagent_message\s*\(/.test(normalized) || /\bagent_text_message\s*\(/.test(normalized);
|
|
13846
14205
|
const returnsObjectLiteral = /return\s+\{[\s\S]*?\}/.test(normalized);
|
|
@@ -13886,6 +14245,9 @@ function extractFocusHintsFromActionSummary(actionSummaryLines) {
|
|
|
13886
14245
|
entryPaths: uniqueStrings(entryPaths, 8)
|
|
13887
14246
|
};
|
|
13888
14247
|
}
|
|
14248
|
+
function normalizeActionSummaryForPrompt(line) {
|
|
14249
|
+
return line.replace(/\blimit=/g, "perPage=").replace(/\blimit:/g, "perPage:");
|
|
14250
|
+
}
|
|
13889
14251
|
function getCurrentClosureId(liveDoc) {
|
|
13890
14252
|
const loop = asRecord2(liveDoc?.loop);
|
|
13891
14253
|
return typeof loop?.currentClosureId === "string" ? loop.currentClosureId : null;
|
|
@@ -14098,7 +14460,9 @@ function projectWorkflowFocus(liveDoc, pendingPrompts = [], options) {
|
|
|
14098
14460
|
variableNames: uniqueStrings(variableNames, 4),
|
|
14099
14461
|
listNames: uniqueStrings(listNames, 4),
|
|
14100
14462
|
entryPaths: uniqueStrings(entryPaths, 6),
|
|
14101
|
-
recentActionSummary: uniqueStrings(actionSummaryLines, 8)
|
|
14463
|
+
recentActionSummary: uniqueStrings(actionSummaryLines, 8).map(
|
|
14464
|
+
normalizeActionSummaryForPrompt
|
|
14465
|
+
)
|
|
14102
14466
|
};
|
|
14103
14467
|
}
|
|
14104
14468
|
function projectWorkflowSummary(liveDoc, pendingPrompts = [], options) {
|
|
@@ -14490,17 +14854,14 @@ ${resultPreview}` : null
|
|
|
14490
14854
|
].filter(Boolean).join("\n\n");
|
|
14491
14855
|
}
|
|
14492
14856
|
function buildGranularAgentDomainBlock(domainDocumentation) {
|
|
14493
|
-
return domainDocumentation?.trim() || "No domain
|
|
14857
|
+
return domainDocumentation?.trim() || "No domain reference available. The graph may not be ready yet.";
|
|
14494
14858
|
}
|
|
14495
14859
|
function buildGranularAgentSessionBlock(sessionContext) {
|
|
14496
14860
|
if (!sessionContext) return "No session metadata available.";
|
|
14497
14861
|
const rows = [
|
|
14498
14862
|
["sandboxId", sessionContext.sandboxId],
|
|
14499
14863
|
["environmentId", sessionContext.environmentId],
|
|
14500
|
-
["
|
|
14501
|
-
["granularId", sessionContext.granularId],
|
|
14502
|
-
["userName", sessionContext.userName],
|
|
14503
|
-
["domainRevision", sessionContext.domainRevision]
|
|
14864
|
+
["userName", sessionContext.userName]
|
|
14504
14865
|
];
|
|
14505
14866
|
const activeRows = rows.filter(([, value]) => Boolean(value));
|
|
14506
14867
|
if (activeRows.length === 0) return "No session metadata available.";
|
|
@@ -14509,6 +14870,9 @@ function buildGranularAgentSessionBlock(sessionContext) {
|
|
|
14509
14870
|
function buildGranularAgentHeapBlock(heapSummary) {
|
|
14510
14871
|
return heapSummary?.trim() || "Heap is empty for this session.";
|
|
14511
14872
|
}
|
|
14873
|
+
function buildGranularAgentReferentBlock(referentSummary) {
|
|
14874
|
+
return referentSummary?.trim() || "No recent referents recorded from prior assistant replies.";
|
|
14875
|
+
}
|
|
14512
14876
|
function buildGranularAgentLoopBlock(loopSummary) {
|
|
14513
14877
|
return loopSummary?.trim() || "No active loop state recorded for this session.";
|
|
14514
14878
|
}
|
|
@@ -14532,7 +14896,7 @@ function buildGranularAgentToolBlock(tools) {
|
|
|
14532
14896
|
(tool) => Boolean(tool.className && !tool.static)
|
|
14533
14897
|
);
|
|
14534
14898
|
const lines = [
|
|
14535
|
-
"Treat this block as the planning map. Use DOMAIN
|
|
14899
|
+
"Treat this block as the planning map. Use DOMAIN REFERENCE below for exact signatures and query examples."
|
|
14536
14900
|
];
|
|
14537
14901
|
const appendGroup = (title, group) => {
|
|
14538
14902
|
lines.push(`- ${title}:`);
|
|
@@ -14580,7 +14944,10 @@ function buildGranularAgentCheckpointBlock(checkpoint) {
|
|
|
14580
14944
|
if (Array.isArray(checkpoint.latestActionSummary) && checkpoint.latestActionSummary.length > 0) {
|
|
14581
14945
|
lines.push("latestActionSummary:");
|
|
14582
14946
|
for (const line of checkpoint.latestActionSummary.slice(0, 8)) {
|
|
14583
|
-
|
|
14947
|
+
const normalizedLine = normalizeActionSummaryForPrompt(line);
|
|
14948
|
+
lines.push(
|
|
14949
|
+
normalizedLine.startsWith("- ") ? normalizedLine : `- ${normalizedLine}`
|
|
14950
|
+
);
|
|
14584
14951
|
}
|
|
14585
14952
|
}
|
|
14586
14953
|
if (checkpoint.latestJobResult?.trim()) {
|
|
@@ -14596,9 +14963,10 @@ function buildGranularAgentSystemPrompt(input) {
|
|
|
14596
14963
|
const workflowBlock = buildGranularAgentWorkflowBlock(input.workflowSummary);
|
|
14597
14964
|
const checkpointBlock = buildGranularAgentCheckpointBlock(input.checkpoint);
|
|
14598
14965
|
const heapBlock = buildGranularAgentHeapBlock(input.heapSummary);
|
|
14966
|
+
const referentBlock = buildGranularAgentReferentBlock(input.referentSummary);
|
|
14599
14967
|
const loopBlock = buildGranularAgentLoopBlock(input.loopSummary);
|
|
14600
14968
|
return `You are an AI assistant for a live Granular session.
|
|
14601
|
-
You can help the user understand the domain, answer questions, or generate and execute
|
|
14969
|
+
You can help the user understand the domain, answer questions, or generate and execute code against the live session.
|
|
14602
14970
|
Your tone must be natural and human-like.
|
|
14603
14971
|
|
|
14604
14972
|
Call the \`execute_code\` effect ONLY when the user's intent matches the domain's capabilities and requires executing code against the live session. If the user is just asking a general question or if their request doesn't match the available effects or domain types, respond with text to explain.
|
|
@@ -14607,6 +14975,8 @@ When you call \`execute_code\`, additional assistant text must be either:
|
|
|
14607
14975
|
- a brief summary of the actions the generated code will perform.
|
|
14608
14976
|
Do not include any other kind of commentary when calling \`execute_code\`.
|
|
14609
14977
|
- If the next step needs to create or update workflow state in the live session, you must call \`execute_code\`. This includes \`loop.ask_user(...)\`, \`loop.confirm(...)\`, \`loop.open_decision(...)\`, \`loop.close_decision(...)\`, \`loop.create_task(...)\`, \`loop.update_task(...)\`, \`loop.complete_task(...)\`, and \`loop.close_loop(...)\`.
|
|
14978
|
+
- If the next step is an interactive clarification that should be resumable in the live workflow, you must call \`execute_code\`. A missing preference, rule, metric, target, or option selection is not a plain-text reply when the answer should drive the next live step.
|
|
14979
|
+
- If you can offer a short grounded shortlist, that clarification should usually be \`loop.ask_user({ type: 'choice', ... })\` instead of a plain-text question with bullet options.
|
|
14610
14980
|
- Never simulate a live prompt, confirmation, decision, task change, or loop closure in plain text. Plain-text replies are only for conversational answers that do not need to mutate session state.
|
|
14611
14981
|
|
|
14612
14982
|
\u2500\u2500\u2500 STREAMING COMMENT RULES \u2500\u2500\u2500
|
|
@@ -14626,6 +14996,7 @@ Do not include any other kind of commentary when calling \`execute_code\`.
|
|
|
14626
14996
|
- Do not say "sandbox" in user-facing text unless the user is explicitly asking about the runtime environment itself.
|
|
14627
14997
|
- If you need clarification, ask in everyday language.
|
|
14628
14998
|
- If the missing information should pause the live workflow for later continuation, ask through \`loop.ask_user(...)\` in generated code rather than with a plain-text question.
|
|
14999
|
+
- If you are asking the user to pick from explicit options, prefer a live \`loop.ask_user({ type: 'choice', ... })\` prompt over a direct reply that lists those options in text.
|
|
14629
15000
|
- Keep replies concise and clear.
|
|
14630
15001
|
- This is a conversation UI, not an API console. Favor human answers over machine-shaped payloads.
|
|
14631
15002
|
|
|
@@ -14635,9 +15006,9 @@ ${sessionBlock}
|
|
|
14635
15006
|
\u2500\u2500\u2500 CAPABILITY SNAPSHOT \u2500\u2500\u2500
|
|
14636
15007
|
${toolBlock}
|
|
14637
15008
|
|
|
14638
|
-
\u2500\u2500\u2500 DOMAIN
|
|
15009
|
+
\u2500\u2500\u2500 DOMAIN REFERENCE (from ./sandbox-tools) \u2500\u2500\u2500
|
|
14639
15010
|
Import classes and effect functions from \`./sandbox-tools\` in generated code.
|
|
14640
|
-
|
|
15011
|
+
Use the TypeScript declarations for exact signatures. When present, the generated usage notes below them show query patterns and examples.
|
|
14641
15012
|
|
|
14642
15013
|
${domainBlock}
|
|
14643
15014
|
|
|
@@ -14647,6 +15018,9 @@ ${checkpointBlock}
|
|
|
14647
15018
|
\u2500\u2500\u2500 WORKFLOW SNAPSHOT \u2500\u2500\u2500
|
|
14648
15019
|
${workflowBlock}
|
|
14649
15020
|
|
|
15021
|
+
\u2500\u2500\u2500 RECENT REFERENTS \u2500\u2500\u2500
|
|
15022
|
+
${referentBlock}
|
|
15023
|
+
|
|
14650
15024
|
\u2500\u2500\u2500 SESSION HEAP \u2500\u2500\u2500
|
|
14651
15025
|
${heapBlock}
|
|
14652
15026
|
|
|
@@ -14654,109 +15028,54 @@ ${heapBlock}
|
|
|
14654
15028
|
${loopBlock}
|
|
14655
15029
|
|
|
14656
15030
|
\u2500\u2500\u2500 LOOP PLAYBOOK \u2500\u2500\u2500
|
|
14657
|
-
-
|
|
14658
|
-
-
|
|
14659
|
-
-
|
|
14660
|
-
-
|
|
14661
|
-
-
|
|
14662
|
-
-
|
|
14663
|
-
- If
|
|
14664
|
-
-
|
|
14665
|
-
-
|
|
14666
|
-
-
|
|
14667
|
-
-
|
|
14668
|
-
-
|
|
14669
|
-
-
|
|
14670
|
-
-
|
|
14671
|
-
- Use \`loop.
|
|
14672
|
-
-
|
|
14673
|
-
-
|
|
14674
|
-
- If
|
|
14675
|
-
- For disambiguation between concrete known records, prefer \`type: 'choice'\` over \`type: 'input'\`. This is especially important for invoices, orders, shipments, requests, work orders, and cases.
|
|
14676
|
-
- For \`type: 'choice'\` prompts, make the options directly pickable by a human: use a stable value and a readable label that includes the identifier or title they are likely to recognize.
|
|
14677
|
-
- If a shortlist already exists, do not ask the user to type an exact database key or identifier manually. Present the shortlist as clickable choices instead.
|
|
14678
|
-
- When reasoning about free-form status strings, do not use brittle substring checks such as \`status.includes("paid")\` because values like \`"unpaid"\` would be misclassified. Prefer explicit positive matches such as \`unpaid\`, \`open\`, or \`overdue\`, or exact normalized comparisons.
|
|
14679
|
-
- When multiple concrete records match and \`loop.ask_user(...)\` is available, do not stop with a plain-text question like "Which invoice do you mean?". Persist the live workflow and ask through \`await loop.ask_user(...)\` instead.
|
|
14680
|
-
- If the user could reasonably answer with a partial identifier such as \`abcd\` for \`INV-abcd\`, that is another sign the question should be a \`type: 'choice'\` prompt with visible options rather than a free-text input.
|
|
14681
|
-
- Never ask for approval in plain text when \`loop.confirm(...)\` is available. Use \`loop.confirm(...)\` for consequential approval.
|
|
14682
|
-
- When the correct next step is a loop helper action, generate code and call that helper. Do not replace it with a conversational reply.
|
|
14683
|
-
- If you ask the user a new question in the current job, do not also call \`loop.close_loop(...)\` in that same job.
|
|
14684
|
-
- When you need user input or approval, await \`loop.ask_user(...)\` or \`loop.confirm(...)\`. The job will pause until the user answers, then resume from that awaited call.
|
|
14685
|
-
- It is valid to branch on the value returned by \`await loop.ask_user(...)\` or \`await loop.confirm(...)\` after the job resumes.
|
|
14686
|
-
- After \`await loop.ask_user(...)\` returns a concrete choice, continue the workflow in the same resumed job whenever that answer is enough to act. Do not stop with placeholder text such as "I can do that next" or "I'm ready to continue".
|
|
14687
|
-
- After \`await loop.confirm(...)\` returns \`true\`, execute the approved mutation in that same resumed job before returning. Do not end with placeholder text like "Approved, ready to make the change next."
|
|
14688
|
-
- Only stop immediately after a resumed prompt when the user declined, the workflow is now blocked, or you truly still need another missing piece of information.
|
|
14689
|
-
- If the user says stop, enough, or no further action, close the loop and end cleanly without asking another question.
|
|
14690
|
-
- If one clear item is already selected and the next step matters, prefer \`loop.confirm(...)\` over another exploratory question.
|
|
14691
|
-
- If one clear item is already selected and the only missing input is approval to proceed, use \`loop.confirm(...)\` rather than \`loop.ask_user(...)\`.
|
|
14692
|
-
- If the user already gave a usable scheduling window such as "Tuesday morning", treat that as enough to choose a reasonable concrete slot. Do not open another menu just to choose between nearby sub-slots unless a real conflict or hard business rule forces that follow-up.
|
|
14693
|
-
- For schedule changes, prefer one grounded recommendation plus one approval prompt. Avoid a second prompt for optional time-window micro-choices when you can pick a sensible default that still satisfies the user request.
|
|
14694
|
-
- If the user explicitly instructs you to perform a consequential action now, that instruction counts as approval. Do not add an extra confirmation step unless the user expressed hesitation, ambiguity, or asked you not to execute yet.
|
|
14695
|
-
- Direct imperatives such as "cancel this order", "send the reminder now", "approve this refund", or "charge it now" already authorize that exact step. Execute them directly instead of inserting \`loop.confirm(...)\`.
|
|
14696
|
-
- If the user says not to do anything irreversible yet, stop at recommendation, review, or approval. Do not collect checkout-only details like quantity, delivery notes, gift message, or optional preferences unless the user explicitly asks to move closer to purchase.
|
|
14697
|
-
- Once you have one solid recommendation, prefer summarizing it and asking for approval over gathering more optional preferences.
|
|
14698
|
-
- Prefer asking the user for the next missing input over fetching extra related data they did not ask for yet.
|
|
14699
|
-
- Avoid serial menus. After one clarifying choice, prefer acting on it, asking one short text question, or confirming rather than opening another menu.
|
|
14700
|
-
- When the user asks for a summary "including" concrete records such as unpaid invoices, open cases, orders, or shipments, include the actual identifiers or titles of those records in the reply, not just aggregate counts.
|
|
14701
|
-
- Call \`loop.close_loop(...)\` before stopping whenever the current workflow is completed, canceled, or clearly blocked.
|
|
15031
|
+
- Continue from the latest structured state. Treat WORKFLOW SNAPSHOT, EXECUTION CHECKPOINT, RECENT REFERENTS, SESSION HEAP, and AGENT LOOP STATE as the working memory for this request.
|
|
15032
|
+
- Use CAPABILITY SNAPSHOT to choose the next step, then use DOMAIN REFERENCE for exact signatures and query shapes.
|
|
15033
|
+
- Take the minimum next step that directly helps the user. Avoid duplicate work, speculative cleanup, or extra fetching that is not needed yet.
|
|
15034
|
+
- If the user names a record that is not already in the heap, fetch it from the graph instead of saying it is not in context.
|
|
15035
|
+
- Treat user-provided names as human references, not exact keys. If one strong partial match exists, use it. If several plausible matches exist, ask the user to choose.
|
|
15036
|
+
- Use RECENT REFERENTS to resolve follow-up references across turns, such as "that invoice", "that customer", "those products", or "the other one".
|
|
15037
|
+
- If the request has more than one reasonable interpretation, ask the user to clarify instead of guessing.
|
|
15038
|
+
- For comparisons, rankings, selections, or summaries, first identify the rule you are using. If that rule is not clear from the user request and DOMAIN REFERENCE, ask the user before choosing anything.
|
|
15039
|
+
- When the ranking, comparison, or selection rule is unclear, the minimum next step is the clarification itself. Do not run a placeholder query for a provisional winner before asking.
|
|
15040
|
+
- Reuse exact \`taskId\`, \`decisionId\`, and \`closureId\` values from AGENT LOOP STATE. Never invent or rewrite them.
|
|
15041
|
+
- If the request is ambiguous or clearly multi-step, create 2-4 short user-visible tasks and keep them updated as the workflow advances.
|
|
15042
|
+
- Use \`loop.ask_user({ type: 'choice', options: [...] })\` when you have a short, grounded shortlist the user can choose from. Otherwise use \`type: 'input'\`.
|
|
15043
|
+
- For an unclear ranking, comparison, or selection rule, prefer \`type: 'choice'\` when you can offer a short grounded list of plausible interpretations from the domain or nearby context.
|
|
15044
|
+
- When \`type: 'choice'\` fits, do not ask the same question as plain text with bullets such as "Common options:" or "Choose one of these:".
|
|
15045
|
+
- Use \`loop.confirm(...)\` for consequential approval unless the user already clearly instructed you to perform that exact action now.
|
|
15046
|
+
- Await \`loop.ask_user(...)\` and \`loop.confirm(...)\`. After the job resumes, continue in the same job whenever the answer is enough to act.
|
|
15047
|
+
- Use \`loop.open_decision(...)\` to persist grounded candidates, \`loop.close_decision(...)\` to resolve one, and \`loop.close_loop(...)\` when the workflow is completed, canceled, or blocked.
|
|
15048
|
+
- If you ask a new question in the current job, do not also close the loop in that same job.
|
|
14702
15049
|
|
|
14703
15050
|
\u2500\u2500\u2500 CODE RULES \u2500\u2500\u2500
|
|
14704
15051
|
- Import from \`./sandbox-tools\`.
|
|
14705
|
-
- If you use \`heap\`, \`loop\`, \`agent_text_message\`,
|
|
15052
|
+
- If you use \`heap\`, \`loop\`, \`agent_text_message\`, or \`agent_heap_objects\`, import them explicitly from \`./sandbox-tools\`.
|
|
14706
15053
|
- Write top-level executable code with \`await\` at top level.
|
|
14707
|
-
- The generated job body must be plain runnable JavaScript.
|
|
14708
|
-
-
|
|
14709
|
-
-
|
|
14710
|
-
-
|
|
14711
|
-
-
|
|
14712
|
-
-
|
|
14713
|
-
-
|
|
14714
|
-
-
|
|
14715
|
-
-
|
|
14716
|
-
-
|
|
14717
|
-
-
|
|
14718
|
-
-
|
|
14719
|
-
-
|
|
14720
|
-
-
|
|
14721
|
-
-
|
|
14722
|
-
-
|
|
14723
|
-
-
|
|
14724
|
-
-
|
|
14725
|
-
-
|
|
14726
|
-
-
|
|
14727
|
-
-
|
|
14728
|
-
-
|
|
14729
|
-
-
|
|
14730
|
-
-
|
|
14731
|
-
-
|
|
14732
|
-
- If a helper returns plain JSON candidates, keep them local, store only a scalar like the chosen id, or resolve the matching sandbox instance before writing it into the heap.
|
|
14733
|
-
- When reading heap values, prefer generated generic typings such as \`await heap.getVar<Book[]>("my_books")\` or \`await heap.getVar<Book>("selected_book")\`.
|
|
14734
|
-
- If a focused heap variable already points to a known class, read it with that exact generic type and act on it directly. Do not use \`heap.getVar<any>(...)\` or cast through \`any\` when the class is already clear from the prompt. For example, prefer \`await heap.getVar<Order>("selected_order")\` over \`await heap.getVar<any>("selected_order")\`.
|
|
14735
|
-
- Use the injected \`loop\` helpers when you need to manage the workflow itself:
|
|
14736
|
-
\`loop.ask_user(...)\`, \`loop.confirm(...)\`, \`loop.open_decision(...)\`, \`loop.close_decision(...)\`,
|
|
14737
|
-
\`loop.create_task(...)\`, \`loop.update_task(...)\`, \`loop.complete_task(...)\`, and \`loop.close_loop(...)\`.
|
|
14738
|
-
- Loop helper semantics:
|
|
14739
|
-
- \`loop.open_decision(...)\`: store explicit candidates from the current job so a later job can revisit the same decision. Keep and reuse the returned \`decisionId\`.
|
|
14740
|
-
- \`loop.close_decision(...)\`: resolve an open decision by choosing one stored candidate with \`selectedId\` and recording why. Candidates may be any JSON objects, but each one must have an \`id\`.
|
|
14741
|
-
- \`loop.ask_user(...)\`: pause the job and ask the user for missing input. Default to \`type: 'input'\`; use \`type: 'choice'\` only for a short explicit shortlist. Write \`const answer = await loop.ask_user(...)\`, then continue the same job once the user answers.
|
|
14742
|
-
- \`loop.confirm(...)\`: pause the job for approval before a consequential action. Do not simulate confirmation in plain text. Write \`const approved = await loop.confirm(...)\`, then branch on that approval once the job resumes.
|
|
14743
|
-
- \`loop.create_task(...)\`, \`loop.update_task(...)\`, \`loop.complete_task(...)\`: keep a short task list that later jobs can continue and finish.
|
|
14744
|
-
- \`loop.close_loop(...)\`: record the current workflow outcome with a short summary before stopping. Do not call it in the same job that opens a new user prompt unless the workflow is explicitly blocked. This does not end the session forever.
|
|
14745
|
-
- Avoid \`as any\` and other broad casts when the DOMAIN TYPES block already tells you the correct class or list type.
|
|
14746
|
-
- Prefer manipulating heap-backed instances and typed lists instead of returning raw JSON blobs or object IDs unless the user explicitly asks for them.
|
|
14747
|
-
- Every job that intends to answer the user must emit at least one explicit UI message with \`agent_text_message(...)\` and/or \`agent_heap_objects(...)\`.
|
|
14748
|
-
- Use \`agent_text_message("...")\` for all user-visible text shown in the UI.
|
|
14749
|
-
- Use \`agent_heap_objects(...)\` only when you want the UI to render heap-backed records or lists.
|
|
14750
|
-
- If you want to show both text and records, call \`agent_text_message(...)\` and \`agent_heap_objects(...)\` separately in whatever order fits the interaction.
|
|
14751
|
-
- \`agent_text_message(...)\` should be used with a plain text string in normal generated code.
|
|
14752
|
-
- \`agent_heap_objects(...)\` should point at heap-backed values: explicit \`entryPaths\` / \`listNames\` / \`variableNames\`, a named list saved with \`saveAs\`, or values read back from \`heap.getVar(...)\`.
|
|
14753
|
-
- If you just fetched records and want to show them in the UI, save or reference them through the heap first, then call \`agent_heap_objects(...)\`. Do not try to hand-build UI payloads in job code.
|
|
14754
|
-
- Do not assume heap changes will be displayed automatically. If records should appear in the UI, you must call \`agent_heap_objects(...)\`.
|
|
14755
|
-
- Never write \`return { reply, show }\` or \`return { show: ... }\` for UI. If you want the UI to render records or lists, call \`agent_heap_objects(...)\` instead.
|
|
14756
|
-
- Do not rely on the final return value for user-visible output. A plain return value is not considered a displayed UI answer.
|
|
14757
|
-
- \`agent_message(...)\` remains available as a legacy compatibility alias, but prefer \`agent_text_message(...)\` and \`agent_heap_objects(...)\` in new code.
|
|
14758
|
-
- Do not return bare structured JSON, low-level diagnostics, or database-shaped payloads as the final answer unless the user explicitly asks for them.
|
|
14759
|
-
- Prefer simple executable JavaScript over clever interpolation. Avoid nested template literals or unusually dense inline expressions when a small temporary variable or string concatenation would be clearer and safer.
|
|
15054
|
+
- The generated job body must be plain runnable JavaScript. Do not use TypeScript-only syntax.
|
|
15055
|
+
- Follow the exact classes, methods, and parameter shapes in DOMAIN REFERENCE. Do not invent helpers or unsupported arguments.
|
|
15056
|
+
- Use \`ClassName.get({ path })\` only for known graph paths when you want a direct graph fetch.
|
|
15057
|
+
- Use \`ClassName.count()\` for totals, \`ClassName.page({ page, perPage, saveAs })\` when you need \`items\` plus \`totalCount\` or \`hasMore\`, \`ClassName.list({ page, perPage, saveAs })\` for one page of records, and \`ClassName.iterate({ perPage, maxItems })\` for large scans.
|
|
15058
|
+
- \`perPage\` defaults to \`100\` and is capped at \`100\`.
|
|
15059
|
+
- Push \`filter\`, \`search\`, and \`sort\` into graph queries instead of fetching a page and processing it locally.
|
|
15060
|
+
- A property appearing on a record does not make it valid in \`filter\` or \`sort\`; only use fields and operators that are explicitly exposed in DOMAIN REFERENCE.
|
|
15061
|
+
- Choose \`sort.field\` verbatim from the sortable fields listed in DOMAIN REFERENCE. Do not sort by relationship names, related-record collections, counts, totals, or other derived metrics unless they are explicitly listed as sortable.
|
|
15062
|
+
- If ordering alone answers the request, use \`sort\` without inventing a \`filter\`.
|
|
15063
|
+
- Do not invent proxy metrics, fallback heuristics, or made-up tie-breakers to resolve ambiguity. If the rule is unclear, ask the user with \`loop.ask_user(...)\`.
|
|
15064
|
+
- Do not fetch, sort, or show a provisional record just to have something to display while the real ranking or selection rule is still ambiguous.
|
|
15065
|
+
- Call instance methods on instances, static methods on classes, and global effects by name.
|
|
15066
|
+
- Use \`heap.getEntry(path)\` for remembered heap entries, \`heap.getList(name)\` for remembered lists, and \`heap.getVar(name)\` only for named variables.
|
|
15067
|
+
- Use \`heap.setVar(...)\` and \`heap.deleteVar(...)\` only when they help the next step.
|
|
15068
|
+
- Prefer \`heap.setVar(...)\` for scalars or one selected instance. Prefer \`ClassName.list({ saveAs })\` for reusable typed lists. Empty arrays are allowed.
|
|
15069
|
+
- Only store sandbox instances, typed lists, or scalars in the heap. If a helper returns plain JSON, keep it local or store only the chosen scalar.
|
|
15070
|
+
- Use the \`loop\` helpers to manage workflow state: \`ask_user\`, \`confirm\`, \`open_decision\`, \`close_decision\`, \`create_task\`, \`update_task\`, \`complete_task\`, and \`close_loop\`.
|
|
15071
|
+
- Use \`type: 'choice'\` only for short grounded options. Use \`type: 'input'\` when the answer should stay open-ended.
|
|
15072
|
+
- \`loop.confirm(...)\` is for consequential approval. Do not ask for approval in plain text.
|
|
15073
|
+
- After \`await loop.ask_user(...)\` or \`await loop.confirm(...)\`, continue in the same resumed job when the answer is enough to act.
|
|
15074
|
+
- Every job that answers the user must emit \`agent_text_message(...)\` and/or \`agent_heap_objects(...)\`.
|
|
15075
|
+
- Use \`agent_text_message(...)\` for user-visible text.
|
|
15076
|
+
- Use \`agent_heap_objects(...)\` for user-visible records. You may pass sandbox instances directly, or heap-backed \`entryPaths\`, \`listNames\`, and \`variableNames\` when you already have them. Use \`saveAs\` or \`heap.setVar(...)\` when you need a reusable named selection.
|
|
15077
|
+
- Do not rely on the final return value for UI output. Do not return ad-hoc \`reply\` / \`show\` payloads instead of explicit agent message calls.
|
|
15078
|
+
- Keep the code small and direct. Avoid speculative branches, broad casts, and raw JSON dumps unless the user asked for them.
|
|
14760
15079
|
- Use \`console.log()\` only for intermediate diagnostics, not for the final user-facing answer.`;
|
|
14761
15080
|
}
|
|
14762
15081
|
|
|
@@ -14915,6 +15234,10 @@ function fallbackResponseText(entries, lists) {
|
|
|
14915
15234
|
return entries.length === 1 ? "I found one relevant record." : `I found ${entries.length} relevant records.`;
|
|
14916
15235
|
}
|
|
14917
15236
|
if (lists.length > 0) {
|
|
15237
|
+
const emptyOnly = lists.every((list) => (list.paths || []).length === 0);
|
|
15238
|
+
if (emptyOnly) {
|
|
15239
|
+
return lists.length === 1 ? "I saved one empty result set." : `I saved ${lists.length} empty result sets.`;
|
|
15240
|
+
}
|
|
14918
15241
|
return lists.length === 1 ? "I saved one result set." : `I saved ${lists.length} result sets.`;
|
|
14919
15242
|
}
|
|
14920
15243
|
return null;
|
|
@@ -15018,19 +15341,27 @@ function matchesPattern(text, matcher) {
|
|
|
15018
15341
|
function assertMatches(label, text, includes = [], excludes = []) {
|
|
15019
15342
|
for (const matcher of includes) {
|
|
15020
15343
|
if (!matchesPattern(text, matcher)) {
|
|
15021
|
-
throw new Error(
|
|
15022
|
-
${
|
|
15344
|
+
throw new Error(
|
|
15345
|
+
`${label} did not match ${matcherToString(matcher)}:
|
|
15346
|
+
${text}`
|
|
15347
|
+
);
|
|
15023
15348
|
}
|
|
15024
15349
|
}
|
|
15025
15350
|
for (const matcher of excludes) {
|
|
15026
15351
|
if (matchesPattern(text, matcher)) {
|
|
15027
|
-
throw new Error(
|
|
15028
|
-
${
|
|
15352
|
+
throw new Error(
|
|
15353
|
+
`${label} matched forbidden ${matcherToString(matcher)}:
|
|
15354
|
+
${text}`
|
|
15355
|
+
);
|
|
15029
15356
|
}
|
|
15030
15357
|
}
|
|
15031
15358
|
}
|
|
15032
15359
|
function buildArtifactDir(baseDir, suiteName = "granular-agent-evals") {
|
|
15033
|
-
return path__default.default.join(
|
|
15360
|
+
return path__default.default.join(
|
|
15361
|
+
baseDir || path__default.default.join(process.cwd(), "test-artifacts"),
|
|
15362
|
+
suiteName,
|
|
15363
|
+
timestampId()
|
|
15364
|
+
);
|
|
15034
15365
|
}
|
|
15035
15366
|
function createTimestampedArtifactDirectory(options) {
|
|
15036
15367
|
return buildArtifactDir(options?.baseDir, options?.suiteName);
|
|
@@ -15044,17 +15375,16 @@ function buildScenarioSteps(scenario) {
|
|
|
15044
15375
|
return scenario.steps;
|
|
15045
15376
|
}
|
|
15046
15377
|
if (!scenario.request) {
|
|
15047
|
-
throw new Error(
|
|
15378
|
+
throw new Error(
|
|
15379
|
+
`Scenario ${scenario.id} must provide either request or steps`
|
|
15380
|
+
);
|
|
15048
15381
|
}
|
|
15049
15382
|
const compatibilityStep = {
|
|
15050
15383
|
id: scenario.id,
|
|
15051
15384
|
request: scenario.request,
|
|
15052
15385
|
human: scenario.human,
|
|
15053
15386
|
expect: scenario.expect,
|
|
15054
|
-
inspect: [
|
|
15055
|
-
...asArray2(scenario.inspect),
|
|
15056
|
-
...asArray2(scenario.verify)
|
|
15057
|
-
],
|
|
15387
|
+
inspect: [...asArray2(scenario.inspect), ...asArray2(scenario.verify)],
|
|
15058
15388
|
check: scenario.check,
|
|
15059
15389
|
maxIterations: scenario.maxIterations,
|
|
15060
15390
|
setup: {
|
|
@@ -15072,22 +15402,27 @@ function buildAssistantHistoryContent(entry) {
|
|
|
15072
15402
|
${entry.content}`);
|
|
15073
15403
|
if (entry.jobStatus) parts.push(`[Job status]
|
|
15074
15404
|
${entry.jobStatus}`);
|
|
15075
|
-
if (entry.jobResultPreview)
|
|
15405
|
+
if (entry.jobResultPreview)
|
|
15406
|
+
parts.push(`[Job result]
|
|
15076
15407
|
${entry.jobResultPreview}`);
|
|
15077
15408
|
if (entry.error) parts.push(`[Job error]
|
|
15078
15409
|
${entry.error}`);
|
|
15079
15410
|
return parts.join("\n\n") || entry.content;
|
|
15080
15411
|
}
|
|
15081
15412
|
function buildHistory(entries) {
|
|
15082
|
-
return entries.reduce(
|
|
15083
|
-
|
|
15084
|
-
if (entry.
|
|
15413
|
+
return entries.reduce(
|
|
15414
|
+
(history, entry) => {
|
|
15415
|
+
if (entry.role === "user") {
|
|
15416
|
+
if (entry.content.trim())
|
|
15417
|
+
history.push({ role: "user", content: entry.content });
|
|
15418
|
+
return history;
|
|
15419
|
+
}
|
|
15420
|
+
const content = buildAssistantHistoryContent(entry).trim();
|
|
15421
|
+
if (content) history.push({ role: "assistant", content });
|
|
15085
15422
|
return history;
|
|
15086
|
-
}
|
|
15087
|
-
|
|
15088
|
-
|
|
15089
|
-
return history;
|
|
15090
|
-
}, []);
|
|
15423
|
+
},
|
|
15424
|
+
[]
|
|
15425
|
+
);
|
|
15091
15426
|
}
|
|
15092
15427
|
function getOpenPromptsFromDoc(liveDoc) {
|
|
15093
15428
|
const jobsById = asRecord4(asRecord4(liveDoc?.jobs)?.byId) || {};
|
|
@@ -15096,7 +15431,8 @@ function getOpenPromptsFromDoc(liveDoc) {
|
|
|
15096
15431
|
const promptRecords = asRecord4(asRecord4(job)?.prompts) || {};
|
|
15097
15432
|
for (const raw of Object.values(promptRecords)) {
|
|
15098
15433
|
const record = asRecord4(raw);
|
|
15099
|
-
if (!record || record.status !== "open" || typeof record.promptId !== "string")
|
|
15434
|
+
if (!record || record.status !== "open" || typeof record.promptId !== "string")
|
|
15435
|
+
continue;
|
|
15100
15436
|
const prompt = normalizePrompt({
|
|
15101
15437
|
promptId: record.promptId,
|
|
15102
15438
|
kind: record.kind,
|
|
@@ -15140,7 +15476,9 @@ ${prompt.message || ""}`;
|
|
|
15140
15476
|
return resolvePromptAnswer(prompt, rawAnswer);
|
|
15141
15477
|
}
|
|
15142
15478
|
if (fallback) return fallback({ prompt, history });
|
|
15143
|
-
throw new Error(
|
|
15479
|
+
throw new Error(
|
|
15480
|
+
`No scripted prompt responder matched prompt ${prompt.id}: ${promptText}`
|
|
15481
|
+
);
|
|
15144
15482
|
};
|
|
15145
15483
|
}
|
|
15146
15484
|
function extractJsonObject(text) {
|
|
@@ -15164,13 +15502,19 @@ function modelOutputInstruction() {
|
|
|
15164
15502
|
].join("\n");
|
|
15165
15503
|
}
|
|
15166
15504
|
function createOpenAIChatTurnGenerator(options) {
|
|
15167
|
-
const baseUrl = (options.baseUrl || "https://api.openai.com/v1").replace(
|
|
15505
|
+
const baseUrl = (options.baseUrl || "https://api.openai.com/v1").replace(
|
|
15506
|
+
/\/$/,
|
|
15507
|
+
""
|
|
15508
|
+
);
|
|
15168
15509
|
const model = options.model || "gpt-5-mini";
|
|
15169
15510
|
return async (input) => {
|
|
15170
15511
|
const messages = [
|
|
15171
|
-
{
|
|
15512
|
+
{
|
|
15513
|
+
role: "system",
|
|
15514
|
+
content: `${input.systemPrompt}
|
|
15172
15515
|
|
|
15173
|
-
${modelOutputInstruction()}`
|
|
15516
|
+
${modelOutputInstruction()}`
|
|
15517
|
+
},
|
|
15174
15518
|
...input.history,
|
|
15175
15519
|
{ role: "user", content: input.request }
|
|
15176
15520
|
];
|
|
@@ -15199,10 +15543,14 @@ ${modelOutputInstruction()}` },
|
|
|
15199
15543
|
await sleep2(500 * attempt);
|
|
15200
15544
|
continue;
|
|
15201
15545
|
}
|
|
15202
|
-
throw new Error(
|
|
15546
|
+
throw new Error(
|
|
15547
|
+
`OpenAI chat generation failed: ${response.status} ${errorText}`
|
|
15548
|
+
);
|
|
15203
15549
|
}
|
|
15204
15550
|
const raw = await response.json();
|
|
15205
|
-
const content = asRecord4(
|
|
15551
|
+
const content = asRecord4(
|
|
15552
|
+
asRecord4(raw.choices?.[0])?.message
|
|
15553
|
+
)?.content;
|
|
15206
15554
|
const text = typeof content === "string" ? content : Array.isArray(content) ? content.map((part) => asRecord4(part)?.text || "").join("") : "";
|
|
15207
15555
|
const parsed = extractJsonObject(text);
|
|
15208
15556
|
if (!parsed) {
|
|
@@ -15220,7 +15568,9 @@ ${text}`);
|
|
|
15220
15568
|
};
|
|
15221
15569
|
} catch (error) {
|
|
15222
15570
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
15223
|
-
if (attempt < 3 && /socket connection was closed unexpectedly|ECONNRESET|network|timed out/i.test(
|
|
15571
|
+
if (attempt < 3 && /socket connection was closed unexpectedly|ECONNRESET|network|timed out/i.test(
|
|
15572
|
+
lastError.message
|
|
15573
|
+
)) {
|
|
15224
15574
|
await sleep2(500 * attempt);
|
|
15225
15575
|
continue;
|
|
15226
15576
|
}
|
|
@@ -15242,7 +15592,10 @@ async function withTimeout(promise, ms, label) {
|
|
|
15242
15592
|
return await Promise.race([
|
|
15243
15593
|
promise,
|
|
15244
15594
|
new Promise((_, reject) => {
|
|
15245
|
-
timeoutId = setTimeout(
|
|
15595
|
+
timeoutId = setTimeout(
|
|
15596
|
+
() => reject(new Error(`${label} timed out after ${ms}ms`)),
|
|
15597
|
+
ms
|
|
15598
|
+
);
|
|
15246
15599
|
})
|
|
15247
15600
|
]);
|
|
15248
15601
|
} finally {
|
|
@@ -15252,7 +15605,9 @@ async function withTimeout(promise, ms, label) {
|
|
|
15252
15605
|
function getActionSummary(liveDoc, jobId) {
|
|
15253
15606
|
const jobsById = asRecord4(asRecord4(liveDoc?.jobs)?.byId) || {};
|
|
15254
15607
|
const job = asRecord4(jobsById[jobId]);
|
|
15255
|
-
return Array.isArray(job?.actionSummary) ? job.actionSummary.filter(
|
|
15608
|
+
return Array.isArray(job?.actionSummary) ? job.actionSummary.filter(
|
|
15609
|
+
(line) => typeof line === "string"
|
|
15610
|
+
) : [];
|
|
15256
15611
|
}
|
|
15257
15612
|
function normalizeHeapSnapshot2(heap) {
|
|
15258
15613
|
return {
|
|
@@ -15282,12 +15637,20 @@ async function waitForJobOutcome(input) {
|
|
|
15282
15637
|
const startedAt = Date.now();
|
|
15283
15638
|
while (Date.now() - startedAt < input.timeoutMs) {
|
|
15284
15639
|
const liveDoc = cloneJson(input.environment.document);
|
|
15285
|
-
const prompts = filterPromptsByBoundary(
|
|
15640
|
+
const prompts = filterPromptsByBoundary(
|
|
15641
|
+
liveDoc,
|
|
15642
|
+
getOpenPromptsFromDoc(liveDoc),
|
|
15643
|
+
input.boundaryTimestamp
|
|
15644
|
+
);
|
|
15286
15645
|
if (prompts.length > 0) {
|
|
15287
15646
|
return { kind: "prompt", prompts, liveDoc, stdout, stderr };
|
|
15288
15647
|
}
|
|
15289
15648
|
try {
|
|
15290
|
-
const result = await withTimeout(
|
|
15649
|
+
const result = await withTimeout(
|
|
15650
|
+
input.job.result,
|
|
15651
|
+
input.pollIntervalMs,
|
|
15652
|
+
`job ${input.job.id} tick`
|
|
15653
|
+
);
|
|
15291
15654
|
return { kind: "completed", result, liveDoc, stdout, stderr };
|
|
15292
15655
|
} catch (error) {
|
|
15293
15656
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -15353,7 +15716,9 @@ function buildResultReport(result) {
|
|
|
15353
15716
|
...result.actionSummary.length ? result.actionSummary.map((line) => `- ${line}`) : ["- None"],
|
|
15354
15717
|
"",
|
|
15355
15718
|
"## Prompt Interactions",
|
|
15356
|
-
...result.promptInteractions.length ? result.promptInteractions.map(
|
|
15719
|
+
...result.promptInteractions.length ? result.promptInteractions.map(
|
|
15720
|
+
(interaction) => `- [${interaction.type}] ${interaction.message || interaction.title} -> ${JSON.stringify(interaction.answer)}`
|
|
15721
|
+
) : ["- None"],
|
|
15357
15722
|
"",
|
|
15358
15723
|
...stepSection,
|
|
15359
15724
|
"## Raw Files",
|
|
@@ -15368,7 +15733,9 @@ function buildSuiteIndex(results) {
|
|
|
15368
15733
|
const lines = [
|
|
15369
15734
|
"# Agent Eval Report Index",
|
|
15370
15735
|
"",
|
|
15371
|
-
...results.map(
|
|
15736
|
+
...results.map(
|
|
15737
|
+
(result) => `- [${result.scenario.id}](./${result.scenario.id}/REPORT.md) - ${result.status}`
|
|
15738
|
+
)
|
|
15372
15739
|
];
|
|
15373
15740
|
return `${lines.join("\n")}
|
|
15374
15741
|
`;
|
|
@@ -15382,7 +15749,7 @@ async function applySetup(setup, context) {
|
|
|
15382
15749
|
await context.environment.recordObjects(setup.records);
|
|
15383
15750
|
}
|
|
15384
15751
|
if (setup.effects?.length) {
|
|
15385
|
-
await context.environment.
|
|
15752
|
+
await context.granular.ontology(context.environment.sandboxId).effects.registerMany(setup.effects);
|
|
15386
15753
|
}
|
|
15387
15754
|
if (setup.run) {
|
|
15388
15755
|
await setup.run(context);
|
|
@@ -15404,6 +15771,7 @@ async function runAgentEvalSuite(options) {
|
|
|
15404
15771
|
);
|
|
15405
15772
|
try {
|
|
15406
15773
|
await applySetup(scenario.setup, {
|
|
15774
|
+
granular: options.harness.granular,
|
|
15407
15775
|
conversation,
|
|
15408
15776
|
environment: conversation.environment,
|
|
15409
15777
|
turnDir: conversation.artifactDir
|
|
@@ -15416,14 +15784,19 @@ async function runAgentEvalSuite(options) {
|
|
|
15416
15784
|
conversation,
|
|
15417
15785
|
request: step.request,
|
|
15418
15786
|
prepare: async (ctx) => {
|
|
15419
|
-
await applySetup(step.setup,
|
|
15787
|
+
await applySetup(step.setup, {
|
|
15788
|
+
granular: options.harness.granular,
|
|
15789
|
+
...ctx
|
|
15790
|
+
});
|
|
15420
15791
|
},
|
|
15421
15792
|
human: step.human,
|
|
15422
15793
|
maxIterations: step.maxIterations,
|
|
15423
15794
|
autoAnswerPrompts: step.autoAnswerPrompts
|
|
15424
15795
|
});
|
|
15425
15796
|
if ("prompts" in completed) {
|
|
15426
|
-
throw new Error(
|
|
15797
|
+
throw new Error(
|
|
15798
|
+
`Scenario ${scenario.id} step ${index + 1} paused for human input instead of completing automatically`
|
|
15799
|
+
);
|
|
15427
15800
|
}
|
|
15428
15801
|
const inspectionResults = [];
|
|
15429
15802
|
const stepChecks = asArray2(step.check);
|
|
@@ -15439,12 +15812,23 @@ async function runAgentEvalSuite(options) {
|
|
|
15439
15812
|
actionSummary: completed.actionSummary,
|
|
15440
15813
|
promptInteractions: completed.promptInteractions,
|
|
15441
15814
|
result: completed.result,
|
|
15442
|
-
heap: normalizeHeapSnapshot2(
|
|
15443
|
-
|
|
15815
|
+
heap: normalizeHeapSnapshot2(
|
|
15816
|
+
asRecord4(
|
|
15817
|
+
cloneJson(conversation.environment.document)?.heap
|
|
15818
|
+
)
|
|
15819
|
+
),
|
|
15820
|
+
openPrompts: getOpenPromptsFromDoc(
|
|
15821
|
+
cloneJson(conversation.environment.document)
|
|
15822
|
+
),
|
|
15444
15823
|
liveDoc: cloneJson(conversation.environment.document),
|
|
15445
15824
|
inspect: async (code) => {
|
|
15446
|
-
const
|
|
15447
|
-
|
|
15825
|
+
const session = conversation.environment;
|
|
15826
|
+
const job = await session.submitJob(code);
|
|
15827
|
+
return withTimeout(
|
|
15828
|
+
job.result,
|
|
15829
|
+
9e4,
|
|
15830
|
+
`inspection job ${job.id}`
|
|
15831
|
+
);
|
|
15448
15832
|
},
|
|
15449
15833
|
assertMatches
|
|
15450
15834
|
};
|
|
@@ -15502,7 +15886,9 @@ async function runAgentEvalSuite(options) {
|
|
|
15502
15886
|
}
|
|
15503
15887
|
const lastStep = stepResults[stepResults.length - 1];
|
|
15504
15888
|
if (!lastStep) {
|
|
15505
|
-
throw new Error(
|
|
15889
|
+
throw new Error(
|
|
15890
|
+
`Scenario ${scenario.id} produced no completed steps`
|
|
15891
|
+
);
|
|
15506
15892
|
}
|
|
15507
15893
|
const result = {
|
|
15508
15894
|
scenario,
|
|
@@ -15516,9 +15902,18 @@ async function runAgentEvalSuite(options) {
|
|
|
15516
15902
|
steps: stepResults,
|
|
15517
15903
|
turnDir: conversation.artifactDir
|
|
15518
15904
|
};
|
|
15519
|
-
await writeJson(
|
|
15520
|
-
|
|
15521
|
-
|
|
15905
|
+
await writeJson(
|
|
15906
|
+
path__default.default.join(conversation.artifactDir, "result.json"),
|
|
15907
|
+
result
|
|
15908
|
+
);
|
|
15909
|
+
await writeJson(
|
|
15910
|
+
path__default.default.join(conversation.artifactDir, "report.json"),
|
|
15911
|
+
result
|
|
15912
|
+
);
|
|
15913
|
+
await promises.writeFile(
|
|
15914
|
+
path__default.default.join(conversation.artifactDir, "REPORT.md"),
|
|
15915
|
+
buildResultReport(result)
|
|
15916
|
+
);
|
|
15522
15917
|
finalResult = result;
|
|
15523
15918
|
} catch (error) {
|
|
15524
15919
|
const failureMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -15540,7 +15935,10 @@ async function runAgentEvalSuite(options) {
|
|
|
15540
15935
|
await ensureDir(failed.turnDir);
|
|
15541
15936
|
await writeJson(path__default.default.join(failed.turnDir, "result.json"), failed);
|
|
15542
15937
|
await writeJson(path__default.default.join(failed.turnDir, "report.json"), failed);
|
|
15543
|
-
await promises.writeFile(
|
|
15938
|
+
await promises.writeFile(
|
|
15939
|
+
path__default.default.join(failed.turnDir, "REPORT.md"),
|
|
15940
|
+
buildResultReport(failed)
|
|
15941
|
+
);
|
|
15544
15942
|
finalResult = failed;
|
|
15545
15943
|
} finally {
|
|
15546
15944
|
await options.harness.closeConversation(conversation);
|
|
@@ -15561,12 +15959,21 @@ async function runAgentEvalSuite(options) {
|
|
|
15561
15959
|
}
|
|
15562
15960
|
results.push(finalResult);
|
|
15563
15961
|
}
|
|
15564
|
-
await writeJson(
|
|
15565
|
-
|
|
15962
|
+
await writeJson(
|
|
15963
|
+
path__default.default.join(options.harness.artifactDir, "summary.json"),
|
|
15964
|
+
results
|
|
15965
|
+
);
|
|
15966
|
+
await promises.writeFile(
|
|
15967
|
+
path__default.default.join(options.harness.artifactDir, "REPORT_INDEX.md"),
|
|
15968
|
+
buildSuiteIndex(results)
|
|
15969
|
+
);
|
|
15566
15970
|
return { artifactDir: options.harness.artifactDir, results };
|
|
15567
15971
|
}
|
|
15568
15972
|
function createAgentEvalHarness(options) {
|
|
15569
|
-
const artifactDir = buildArtifactDir(
|
|
15973
|
+
const artifactDir = buildArtifactDir(
|
|
15974
|
+
options.artifactBaseDir,
|
|
15975
|
+
options.suiteName
|
|
15976
|
+
);
|
|
15570
15977
|
const controllerBudgets = {
|
|
15571
15978
|
...DEFAULT_CONTROLLER_BUDGETS,
|
|
15572
15979
|
...options.controllerBudgets || {}
|
|
@@ -15578,7 +15985,9 @@ function createAgentEvalHarness(options) {
|
|
|
15578
15985
|
await ensureDir(artifactDir);
|
|
15579
15986
|
const clientId = `${slugify(label)}-${Date.now()}`;
|
|
15580
15987
|
if (!options.openEnvironment && !options.environmentId) {
|
|
15581
|
-
throw new Error(
|
|
15988
|
+
throw new Error(
|
|
15989
|
+
"createAgentEvalHarness requires either environmentId or openEnvironment"
|
|
15990
|
+
);
|
|
15582
15991
|
}
|
|
15583
15992
|
const environment = options.openEnvironment ? await options.openEnvironment({ label, clientId }) : await options.granular.createSession({
|
|
15584
15993
|
environmentId: options.environmentId,
|
|
@@ -15602,12 +16011,15 @@ function createAgentEvalHarness(options) {
|
|
|
15602
16011
|
}
|
|
15603
16012
|
async function closeConversation(conversation) {
|
|
15604
16013
|
try {
|
|
15605
|
-
await options.granular.closeSession(
|
|
16014
|
+
await options.granular.closeSession(
|
|
16015
|
+
conversation.environment.sessionId,
|
|
16016
|
+
conversation.environment
|
|
16017
|
+
);
|
|
15606
16018
|
} catch {
|
|
15607
16019
|
}
|
|
15608
16020
|
}
|
|
15609
|
-
async function runCheckJob(code,
|
|
15610
|
-
const job = await
|
|
16021
|
+
async function runCheckJob(code, session) {
|
|
16022
|
+
const job = await session.submitJob(code);
|
|
15611
16023
|
return withTimeout(job.result, jobTimeoutMs, `check job ${job.id}`);
|
|
15612
16024
|
}
|
|
15613
16025
|
function buildCheckContext(conversation, completed, turnDir) {
|
|
@@ -15652,8 +16064,12 @@ function createAgentEvalHarness(options) {
|
|
|
15652
16064
|
async function resumePendingTurn(pending, responder) {
|
|
15653
16065
|
const prompt = pending.prompts[0];
|
|
15654
16066
|
if (!prompt) throw new Error("Pending turn has no prompts to answer");
|
|
15655
|
-
const answer = await responder({
|
|
15656
|
-
|
|
16067
|
+
const answer = await responder({
|
|
16068
|
+
prompt,
|
|
16069
|
+
history: pending.promptInteractions
|
|
16070
|
+
});
|
|
16071
|
+
const session = pending.conversation.environment;
|
|
16072
|
+
await session.answerPrompt(prompt.id, answer);
|
|
15657
16073
|
pending.promptInteractions.push({
|
|
15658
16074
|
promptId: prompt.id,
|
|
15659
16075
|
type: prompt.type,
|
|
@@ -15677,7 +16093,9 @@ function createAgentEvalHarness(options) {
|
|
|
15677
16093
|
};
|
|
15678
16094
|
}
|
|
15679
16095
|
await sleep2(350);
|
|
15680
|
-
const liveDoc = cloneJson(
|
|
16096
|
+
const liveDoc = cloneJson(
|
|
16097
|
+
pending.conversation.environment.document
|
|
16098
|
+
);
|
|
15681
16099
|
const presentation = resolveJobPresentation({
|
|
15682
16100
|
jobId: pending.job.id,
|
|
15683
16101
|
result: resumed.result,
|
|
@@ -15722,25 +16140,40 @@ function createAgentEvalHarness(options) {
|
|
|
15722
16140
|
await conversation.environment.recordObjects(input.prepareRecords);
|
|
15723
16141
|
}
|
|
15724
16142
|
if (input.prepareTools?.length) {
|
|
15725
|
-
await conversation.environment.
|
|
16143
|
+
await options.granular.ontology(conversation.environment.sandboxId).effects.registerMany(input.prepareTools);
|
|
15726
16144
|
}
|
|
15727
16145
|
if (input.prepare) {
|
|
15728
|
-
await input.prepare({
|
|
16146
|
+
await input.prepare({
|
|
16147
|
+
conversation,
|
|
16148
|
+
environment: conversation.environment,
|
|
16149
|
+
turnDir
|
|
16150
|
+
});
|
|
15729
16151
|
}
|
|
15730
16152
|
const boundaryTimestamp = Date.now();
|
|
15731
16153
|
conversation.history.push({ role: "user", content: input.request });
|
|
15732
|
-
await writeJson(path__default.default.join(turnDir, "request.json"), {
|
|
16154
|
+
await writeJson(path__default.default.join(turnDir, "request.json"), {
|
|
16155
|
+
request: input.request,
|
|
16156
|
+
boundaryTimestamp
|
|
16157
|
+
});
|
|
15733
16158
|
let iteration = 0;
|
|
15734
16159
|
let noProgressCount = 0;
|
|
15735
16160
|
let previousSnapshot = null;
|
|
15736
16161
|
let latestCheckpoint = null;
|
|
15737
16162
|
const maxIterations = input.maxIterations || controllerBudgets.maxIterations;
|
|
15738
16163
|
const autoAnswerPrompts = input.autoAnswerPrompts ?? true;
|
|
15739
|
-
const baselineClosureId = getCurrentClosureId(
|
|
16164
|
+
const baselineClosureId = getCurrentClosureId(
|
|
16165
|
+
cloneJson(conversation.environment.document)
|
|
16166
|
+
);
|
|
15740
16167
|
while (iteration < maxIterations) {
|
|
15741
16168
|
const liveDoc = cloneJson(conversation.environment.document);
|
|
15742
|
-
const pendingPrompts = filterPromptsByBoundary(
|
|
15743
|
-
|
|
16169
|
+
const pendingPrompts = filterPromptsByBoundary(
|
|
16170
|
+
liveDoc,
|
|
16171
|
+
getOpenPromptsFromDoc(liveDoc),
|
|
16172
|
+
boundaryTimestamp
|
|
16173
|
+
);
|
|
16174
|
+
const workflowFocus = projectWorkflowFocus(liveDoc, pendingPrompts, {
|
|
16175
|
+
boundaryTimestamp
|
|
16176
|
+
});
|
|
15744
16177
|
const systemPrompt = buildGranularAgentSystemPrompt({
|
|
15745
16178
|
domainDocumentation: await conversation.environment.getDomainDocumentation(),
|
|
15746
16179
|
sessionContext: {
|
|
@@ -15751,8 +16184,12 @@ function createAgentEvalHarness(options) {
|
|
|
15751
16184
|
heapSummary: projectHeapSummary(liveDoc, {
|
|
15752
16185
|
focus: workflowFocus
|
|
15753
16186
|
}),
|
|
15754
|
-
loopSummary: projectLoopSummary(liveDoc, pendingPrompts, {
|
|
15755
|
-
|
|
16187
|
+
loopSummary: projectLoopSummary(liveDoc, pendingPrompts, {
|
|
16188
|
+
boundaryTimestamp
|
|
16189
|
+
}),
|
|
16190
|
+
workflowSummary: projectWorkflowSummary(liveDoc, pendingPrompts, {
|
|
16191
|
+
boundaryTimestamp
|
|
16192
|
+
}),
|
|
15756
16193
|
tools: conversation.environment.getEffects().map((tool) => ({
|
|
15757
16194
|
name: tool.name,
|
|
15758
16195
|
description: tool.description,
|
|
@@ -15762,14 +16199,23 @@ function createAgentEvalHarness(options) {
|
|
|
15762
16199
|
})),
|
|
15763
16200
|
checkpoint: latestCheckpoint
|
|
15764
16201
|
});
|
|
15765
|
-
const request = iteration === 0 ? input.request : buildContinuationInstruction(
|
|
15766
|
-
|
|
15767
|
-
|
|
15768
|
-
|
|
15769
|
-
|
|
15770
|
-
|
|
15771
|
-
|
|
15772
|
-
|
|
16202
|
+
const request = iteration === 0 ? input.request : buildContinuationInstruction(
|
|
16203
|
+
buildContinuationPreview(latestCheckpoint, noProgressCount)
|
|
16204
|
+
);
|
|
16205
|
+
const generation = await withTimeout(
|
|
16206
|
+
generateTurnWithRepair(options.generator, {
|
|
16207
|
+
systemPrompt,
|
|
16208
|
+
history: buildHistory(conversation.history),
|
|
16209
|
+
request,
|
|
16210
|
+
attempt: 1
|
|
16211
|
+
}),
|
|
16212
|
+
chatTimeoutMs,
|
|
16213
|
+
`chat generation for ${conversation.label} iteration ${iteration + 1}`
|
|
16214
|
+
);
|
|
16215
|
+
await writeJson(
|
|
16216
|
+
path__default.default.join(turnDir, `iteration-${iteration + 1}-generation.json`),
|
|
16217
|
+
generation
|
|
16218
|
+
);
|
|
15773
16219
|
if (!generation.code) {
|
|
15774
16220
|
const responseText2 = generation.reply?.trim() || "Done.";
|
|
15775
16221
|
conversation.history.push({ role: "assistant", content: responseText2 });
|
|
@@ -15785,12 +16231,18 @@ function createAgentEvalHarness(options) {
|
|
|
15785
16231
|
result: generation.reply?.trim() || responseText2
|
|
15786
16232
|
};
|
|
15787
16233
|
if (input.verification) {
|
|
15788
|
-
completed.verification = await runInspection(
|
|
16234
|
+
completed.verification = await runInspection(
|
|
16235
|
+
conversation,
|
|
16236
|
+
input.verification,
|
|
16237
|
+
completed,
|
|
16238
|
+
turnDir
|
|
16239
|
+
);
|
|
15789
16240
|
}
|
|
15790
16241
|
await writeJson(path__default.default.join(turnDir, "result.json"), completed);
|
|
15791
16242
|
return completed;
|
|
15792
16243
|
}
|
|
15793
|
-
const
|
|
16244
|
+
const session = conversation.environment;
|
|
16245
|
+
const job = await session.submitJob(generation.code);
|
|
15794
16246
|
const outcome = await waitForJobOutcome({
|
|
15795
16247
|
environment: conversation.environment,
|
|
15796
16248
|
job,
|
|
@@ -15815,7 +16267,9 @@ function createAgentEvalHarness(options) {
|
|
|
15815
16267
|
};
|
|
15816
16268
|
}
|
|
15817
16269
|
if (!input.human) {
|
|
15818
|
-
throw new Error(
|
|
16270
|
+
throw new Error(
|
|
16271
|
+
"This turn reached a human prompt but no responder was provided"
|
|
16272
|
+
);
|
|
15819
16273
|
}
|
|
15820
16274
|
let pending = {
|
|
15821
16275
|
conversation,
|
|
@@ -15837,16 +16291,25 @@ function createAgentEvalHarness(options) {
|
|
|
15837
16291
|
continue;
|
|
15838
16292
|
}
|
|
15839
16293
|
if (input.verification) {
|
|
15840
|
-
resumed.verification = await runInspection(
|
|
16294
|
+
resumed.verification = await runInspection(
|
|
16295
|
+
conversation,
|
|
16296
|
+
input.verification,
|
|
16297
|
+
resumed,
|
|
16298
|
+
turnDir
|
|
16299
|
+
);
|
|
15841
16300
|
}
|
|
15842
16301
|
return resumed;
|
|
15843
16302
|
}
|
|
15844
16303
|
}
|
|
15845
16304
|
if (outcome.kind !== "completed") {
|
|
15846
|
-
throw new Error(
|
|
16305
|
+
throw new Error(
|
|
16306
|
+
"Unexpected non-completed outcome after prompt handling"
|
|
16307
|
+
);
|
|
15847
16308
|
}
|
|
15848
16309
|
await sleep2(350);
|
|
15849
|
-
const settledLiveDoc = cloneJson(
|
|
16310
|
+
const settledLiveDoc = cloneJson(
|
|
16311
|
+
conversation.environment.document
|
|
16312
|
+
);
|
|
15850
16313
|
const sessionHeap = normalizeHeapSnapshot2(asRecord4(settledLiveDoc?.heap));
|
|
15851
16314
|
const presentation = resolveJobPresentation({
|
|
15852
16315
|
jobId: job.id,
|
|
@@ -15867,7 +16330,11 @@ function createAgentEvalHarness(options) {
|
|
|
15867
16330
|
baselineClosureId,
|
|
15868
16331
|
currentClosureId: getCurrentClosureId(settledLiveDoc),
|
|
15869
16332
|
liveDoc: settledLiveDoc,
|
|
15870
|
-
pendingPrompts: filterPromptsByBoundary(
|
|
16333
|
+
pendingPrompts: filterPromptsByBoundary(
|
|
16334
|
+
settledLiveDoc,
|
|
16335
|
+
getOpenPromptsFromDoc(settledLiveDoc),
|
|
16336
|
+
boundaryTimestamp
|
|
16337
|
+
),
|
|
15871
16338
|
projectionOptions: { boundaryTimestamp },
|
|
15872
16339
|
latestResponseText: responseText,
|
|
15873
16340
|
previousSnapshot,
|
|
@@ -15892,12 +16359,15 @@ function createAgentEvalHarness(options) {
|
|
|
15892
16359
|
jobStatus: "succeeded",
|
|
15893
16360
|
jobResultPreview: JSON.stringify(outcome.result, null, 2)
|
|
15894
16361
|
});
|
|
15895
|
-
await writeJson(
|
|
15896
|
-
|
|
15897
|
-
|
|
15898
|
-
|
|
15899
|
-
|
|
15900
|
-
|
|
16362
|
+
await writeJson(
|
|
16363
|
+
path__default.default.join(turnDir, `iteration-${iteration + 1}-result.json`),
|
|
16364
|
+
{
|
|
16365
|
+
responseText,
|
|
16366
|
+
continuation,
|
|
16367
|
+
actionSummary: latestCheckpoint.latestActionSummary,
|
|
16368
|
+
result: outcome.result
|
|
16369
|
+
}
|
|
16370
|
+
);
|
|
15901
16371
|
if (!continuation.shouldContinue) {
|
|
15902
16372
|
const completed = {
|
|
15903
16373
|
conversation,
|
|
@@ -15912,17 +16382,25 @@ function createAgentEvalHarness(options) {
|
|
|
15912
16382
|
result: outcome.result
|
|
15913
16383
|
};
|
|
15914
16384
|
if (input.verification) {
|
|
15915
|
-
completed.verification = await runInspection(
|
|
16385
|
+
completed.verification = await runInspection(
|
|
16386
|
+
conversation,
|
|
16387
|
+
input.verification,
|
|
16388
|
+
completed,
|
|
16389
|
+
turnDir
|
|
16390
|
+
);
|
|
15916
16391
|
}
|
|
15917
16392
|
await writeJson(path__default.default.join(turnDir, "result.json"), completed);
|
|
15918
16393
|
return completed;
|
|
15919
16394
|
}
|
|
15920
16395
|
iteration += 1;
|
|
15921
16396
|
}
|
|
15922
|
-
throw new Error(
|
|
16397
|
+
throw new Error(
|
|
16398
|
+
`Turn exceeded iteration budget (${maxIterations}) for ${conversation.label}`
|
|
16399
|
+
);
|
|
15923
16400
|
}
|
|
15924
16401
|
return {
|
|
15925
16402
|
artifactDir,
|
|
16403
|
+
granular: options.granular,
|
|
15926
16404
|
openConversation,
|
|
15927
16405
|
closeConversation,
|
|
15928
16406
|
runTurn,
|
|
@@ -15958,12 +16436,11 @@ function createAgentTester(options) {
|
|
|
15958
16436
|
}
|
|
15959
16437
|
if ("connect" in options.target && !connectSeeded) {
|
|
15960
16438
|
connectSeeded = true;
|
|
15961
|
-
const environment = await granular.
|
|
15962
|
-
...options.target.connect
|
|
15963
|
-
clientId
|
|
16439
|
+
const environment = await granular.openEnvironment({
|
|
16440
|
+
...options.target.connect
|
|
15964
16441
|
});
|
|
15965
16442
|
resolvedEnvironmentId = environment.environmentId;
|
|
15966
|
-
return environment;
|
|
16443
|
+
return environment.sessions.create({ clientId });
|
|
15967
16444
|
}
|
|
15968
16445
|
if ("createEnvironment" in options.target && !resolvedEnvironmentId) {
|
|
15969
16446
|
const envData = await granular.environments.create(
|