@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.mjs
CHANGED
|
@@ -4649,27 +4649,27 @@ var Session = class {
|
|
|
4649
4649
|
}
|
|
4650
4650
|
async publishTools(tools, revision = "1.0.0") {
|
|
4651
4651
|
throw new Error(
|
|
4652
|
-
"Environment-scoped effect publication was removed. Declare effects in the manifest and register live handlers with granular.
|
|
4652
|
+
"Environment-scoped effect publication was removed. Declare effects in the manifest and register live handlers with granular.ontology(environment.sandboxId).effects.registerMany(effects)."
|
|
4653
4653
|
);
|
|
4654
4654
|
}
|
|
4655
4655
|
async publishEffect(effect) {
|
|
4656
4656
|
throw new Error(
|
|
4657
|
-
"Environment-scoped effect publication was removed. Use granular.
|
|
4657
|
+
"Environment-scoped effect publication was removed. Use granular.ontology(environment.sandboxId).effects.register(effect)."
|
|
4658
4658
|
);
|
|
4659
4659
|
}
|
|
4660
4660
|
async publishEffects(effects) {
|
|
4661
4661
|
throw new Error(
|
|
4662
|
-
"Environment-scoped effect publication was removed. Use granular.
|
|
4662
|
+
"Environment-scoped effect publication was removed. Use granular.ontology(environment.sandboxId).effects.registerMany(effects)."
|
|
4663
4663
|
);
|
|
4664
4664
|
}
|
|
4665
4665
|
async unpublishEffect(name) {
|
|
4666
4666
|
throw new Error(
|
|
4667
|
-
"Environment-scoped effect publication was removed. Use granular.
|
|
4667
|
+
"Environment-scoped effect publication was removed. Use granular.ontology(environment.sandboxId).effects.unregister(effectName)."
|
|
4668
4668
|
);
|
|
4669
4669
|
}
|
|
4670
4670
|
async unpublishAllEffects() {
|
|
4671
4671
|
throw new Error(
|
|
4672
|
-
"Environment-scoped effect publication was removed. Use granular.
|
|
4672
|
+
"Environment-scoped effect publication was removed. Use granular.ontology(environment.sandboxId).effects.clear()."
|
|
4673
4673
|
);
|
|
4674
4674
|
}
|
|
4675
4675
|
/**
|
|
@@ -10955,17 +10955,8 @@ var searchableMetamodelPackage = defineMetamodelPackage({
|
|
|
10955
10955
|
}
|
|
10956
10956
|
},
|
|
10957
10957
|
domain: {
|
|
10958
|
-
applyToPropertyIR(propertyIR,
|
|
10959
|
-
|
|
10960
|
-
return propertyIR;
|
|
10961
|
-
}
|
|
10962
|
-
return {
|
|
10963
|
-
...propertyIR,
|
|
10964
|
-
docs: [
|
|
10965
|
-
...propertyIR.docs,
|
|
10966
|
-
propertySummary.searchablePhonetic ? "Searchable via `search` using FalkorDB full-text query syntax with phonetic matching enabled." : "Searchable via `search` using FalkorDB full-text query syntax."
|
|
10967
|
-
]
|
|
10968
|
-
};
|
|
10958
|
+
applyToPropertyIR(propertyIR, _propertySummary) {
|
|
10959
|
+
return propertyIR;
|
|
10969
10960
|
}
|
|
10970
10961
|
}
|
|
10971
10962
|
});
|
|
@@ -11579,17 +11570,40 @@ function buildEffectMetamodelMutations(toolPath, spec) {
|
|
|
11579
11570
|
|
|
11580
11571
|
// src/client.ts
|
|
11581
11572
|
var STANDARD_MODULES_OPERATIONS = [
|
|
11582
|
-
{
|
|
11573
|
+
{
|
|
11574
|
+
create: "entity",
|
|
11575
|
+
has: { id: { value: "auto-generated" }, createdAt: { value: void 0 } }
|
|
11576
|
+
},
|
|
11583
11577
|
{ create: "class", extends: "entity", has: {} },
|
|
11584
|
-
{
|
|
11585
|
-
|
|
11578
|
+
{
|
|
11579
|
+
create: "user",
|
|
11580
|
+
extends: "entity",
|
|
11581
|
+
has: {
|
|
11582
|
+
email: { value: void 0 },
|
|
11583
|
+
firstName: { value: void 0 },
|
|
11584
|
+
lastName: { value: void 0 }
|
|
11585
|
+
}
|
|
11586
|
+
},
|
|
11587
|
+
{
|
|
11588
|
+
create: "company",
|
|
11589
|
+
extends: "entity",
|
|
11590
|
+
has: { name: { value: void 0 }, website: { value: void 0 } }
|
|
11591
|
+
},
|
|
11586
11592
|
{ create: "string", has: {} },
|
|
11587
11593
|
{ create: "number", has: {} },
|
|
11588
11594
|
{ create: "boolean", has: {} },
|
|
11589
|
-
{
|
|
11595
|
+
{
|
|
11596
|
+
create: "tool_parameter",
|
|
11597
|
+
has: {
|
|
11598
|
+
name: { value: void 0 },
|
|
11599
|
+
type: { value: "string" },
|
|
11600
|
+
description: { value: void 0 },
|
|
11601
|
+
required: { value: false }
|
|
11602
|
+
}
|
|
11603
|
+
}
|
|
11590
11604
|
];
|
|
11591
11605
|
var BUILTIN_MODULES = {
|
|
11592
|
-
|
|
11606
|
+
standard_modules: STANDARD_MODULES_OPERATIONS
|
|
11593
11607
|
};
|
|
11594
11608
|
var DEFAULT_DIRECT_RECORD_OBJECTS_REQUEST_BATCH_SIZE = 100;
|
|
11595
11609
|
var MAX_RECORD_OBJECTS_CONCURRENCY = 16;
|
|
@@ -11624,7 +11638,9 @@ function isRetryableLocalWorkerRestart(status, body, url) {
|
|
|
11624
11638
|
}
|
|
11625
11639
|
function isRetryableRecordObjectsError(error) {
|
|
11626
11640
|
const message = error instanceof Error ? error.message : String(error);
|
|
11627
|
-
return /worker restarted mid-request|econnreset|network|socket connection was closed unexpectedly|timed out/i.test(
|
|
11641
|
+
return /worker restarted mid-request|econnreset|network|socket connection was closed unexpectedly|timed out/i.test(
|
|
11642
|
+
message
|
|
11643
|
+
);
|
|
11628
11644
|
}
|
|
11629
11645
|
function computeEffectKey2(effect) {
|
|
11630
11646
|
const attachedClass = effect.className?.trim();
|
|
@@ -11677,6 +11693,22 @@ function normalizeHeapSnapshot(raw) {
|
|
|
11677
11693
|
updatedAt: typeof heap.updatedAt === "number" ? heap.updatedAt : Date.now()
|
|
11678
11694
|
};
|
|
11679
11695
|
}
|
|
11696
|
+
function deriveRuntimeBaseUrl(apiEndpoint) {
|
|
11697
|
+
try {
|
|
11698
|
+
const endpoint = new URL(apiEndpoint);
|
|
11699
|
+
const graphqlSuffix = "/orchestrator/graphql";
|
|
11700
|
+
if (endpoint.pathname.endsWith(graphqlSuffix)) {
|
|
11701
|
+
endpoint.pathname = endpoint.pathname.slice(0, -graphqlSuffix.length);
|
|
11702
|
+
} else if (endpoint.pathname.endsWith("/graphql")) {
|
|
11703
|
+
endpoint.pathname = endpoint.pathname.slice(0, -"/graphql".length);
|
|
11704
|
+
}
|
|
11705
|
+
endpoint.search = "";
|
|
11706
|
+
endpoint.hash = "";
|
|
11707
|
+
return endpoint.toString().replace(/\/$/, "");
|
|
11708
|
+
} catch {
|
|
11709
|
+
return apiEndpoint.replace(/\/orchestrator\/graphql$/, "").replace(/\/$/, "");
|
|
11710
|
+
}
|
|
11711
|
+
}
|
|
11680
11712
|
function normalizeSubject(subject) {
|
|
11681
11713
|
const granularId = subject.granularId || subject.subjectId;
|
|
11682
11714
|
const userId = subject.userId || subject.identityId || granularId;
|
|
@@ -11701,7 +11733,10 @@ function normalizeUser(user) {
|
|
|
11701
11733
|
};
|
|
11702
11734
|
}
|
|
11703
11735
|
function normalizeEnvironmentData(environment) {
|
|
11704
|
-
const buildPolicy = environment.buildPolicy || environment.tracking || (environment.tagId ? { mode: "tag", tagId: environment.tagId } : {
|
|
11736
|
+
const buildPolicy = environment.buildPolicy || environment.tracking || (environment.tagId ? { mode: "tag", tagId: environment.tagId } : {
|
|
11737
|
+
mode: "pinned",
|
|
11738
|
+
versionId: environment.versionId || environment.buildId
|
|
11739
|
+
});
|
|
11705
11740
|
const environmentName = environment.environment || environment.envName || "prod";
|
|
11706
11741
|
return {
|
|
11707
11742
|
...environment,
|
|
@@ -11713,12 +11748,13 @@ function normalizeEnvironmentData(environment) {
|
|
|
11713
11748
|
tracking: environment.tracking || buildPolicy
|
|
11714
11749
|
};
|
|
11715
11750
|
}
|
|
11716
|
-
var Environment = class
|
|
11751
|
+
var Environment = class {
|
|
11752
|
+
granular;
|
|
11717
11753
|
envData;
|
|
11718
11754
|
_apiKey;
|
|
11719
11755
|
_apiEndpoint;
|
|
11720
|
-
constructor(
|
|
11721
|
-
|
|
11756
|
+
constructor(granular, envData, apiKey, apiEndpoint) {
|
|
11757
|
+
this.granular = granular;
|
|
11722
11758
|
this.envData = envData;
|
|
11723
11759
|
this._apiKey = apiKey;
|
|
11724
11760
|
this._apiEndpoint = apiEndpoint;
|
|
@@ -11759,35 +11795,126 @@ var Environment = class extends Session {
|
|
|
11759
11795
|
get permissionProfileId() {
|
|
11760
11796
|
return this.envData.permissionProfileId;
|
|
11761
11797
|
}
|
|
11798
|
+
/** The current build policy backing this environment */
|
|
11799
|
+
get buildPolicy() {
|
|
11800
|
+
return this.envData.buildPolicy;
|
|
11801
|
+
}
|
|
11802
|
+
/** The current update state relative to the followed tag */
|
|
11803
|
+
get updateState() {
|
|
11804
|
+
return this.envData.updateState;
|
|
11805
|
+
}
|
|
11806
|
+
/** Convenience flag for whether this environment trails the current tag target */
|
|
11807
|
+
get isOutdated() {
|
|
11808
|
+
return this.envData.updateState === "update_available";
|
|
11809
|
+
}
|
|
11810
|
+
/** The followed tag name when this environment is tag-tracked */
|
|
11811
|
+
get tag() {
|
|
11812
|
+
return this.envData.tag?.name || this.envData.buildPolicy.tagName || null;
|
|
11813
|
+
}
|
|
11762
11814
|
/** The GraphQL API endpoint URL */
|
|
11763
11815
|
get apiEndpoint() {
|
|
11764
11816
|
return this._apiEndpoint;
|
|
11765
11817
|
}
|
|
11818
|
+
/** Internal auth token used for control-plane and runtime fallback requests */
|
|
11819
|
+
get authToken() {
|
|
11820
|
+
return this._apiKey;
|
|
11821
|
+
}
|
|
11822
|
+
/** Base runtime URL derived from the GraphQL endpoint */
|
|
11823
|
+
get runtimeBaseUrl() {
|
|
11824
|
+
return this.getRuntimeBaseUrl();
|
|
11825
|
+
}
|
|
11826
|
+
get sessions() {
|
|
11827
|
+
return {
|
|
11828
|
+
list: async (options) => this.listSessions(options?.status || "active"),
|
|
11829
|
+
create: async (options) => this.createSession(options),
|
|
11830
|
+
connect: async (sessionId, options) => this.connectSession(sessionId, options),
|
|
11831
|
+
reopen: async (sessionId, options) => this.reopenSession(sessionId, options),
|
|
11832
|
+
close: async (sessionId, session) => this.closeSession(sessionId, session)
|
|
11833
|
+
};
|
|
11834
|
+
}
|
|
11835
|
+
get data() {
|
|
11836
|
+
return {
|
|
11837
|
+
record: async (record) => this.recordObject(record),
|
|
11838
|
+
recordMany: async (records, options) => this.recordObjects(records, options),
|
|
11839
|
+
import: async (records, options) => this.enqueueRecordImport(records, options),
|
|
11840
|
+
listImports: async (status) => this.listRecordImports(status),
|
|
11841
|
+
getImport: async (importId) => this.getRecordImport(importId),
|
|
11842
|
+
getImportSummary: async () => this.getRecordImportSummary(),
|
|
11843
|
+
cancelImport: async (importId) => this.cancelRecordImport(importId),
|
|
11844
|
+
getAwaitingCount: async () => this.getAwaitingRecordCount()
|
|
11845
|
+
};
|
|
11846
|
+
}
|
|
11847
|
+
get feedback() {
|
|
11848
|
+
return {
|
|
11849
|
+
list: async () => this.listFeedback()
|
|
11850
|
+
};
|
|
11851
|
+
}
|
|
11766
11852
|
/**
|
|
11767
|
-
*
|
|
11768
|
-
*
|
|
11769
|
-
*
|
|
11770
|
-
*
|
|
11853
|
+
* Sessionless environments do not own a live transport, so disconnecting the
|
|
11854
|
+
* environment handle itself is a no-op. This keeps the public surface
|
|
11855
|
+
* symmetric with `EnvironmentSession.disconnect()` and lets callers always
|
|
11856
|
+
* clean up safely without tracking whether they currently hold an environment
|
|
11857
|
+
* or a session.
|
|
11771
11858
|
*/
|
|
11772
|
-
|
|
11773
|
-
const doc = this.document;
|
|
11774
|
-
return normalizeHeapSnapshot(doc?.heap);
|
|
11859
|
+
async disconnect() {
|
|
11775
11860
|
}
|
|
11776
|
-
|
|
11777
|
-
|
|
11778
|
-
const
|
|
11779
|
-
|
|
11780
|
-
|
|
11781
|
-
|
|
11782
|
-
|
|
11783
|
-
|
|
11784
|
-
|
|
11785
|
-
|
|
11786
|
-
|
|
11787
|
-
|
|
11788
|
-
|
|
11789
|
-
|
|
11861
|
+
async listSessions(status = "active") {
|
|
11862
|
+
if (status === "all") {
|
|
11863
|
+
const [active, closed] = await Promise.all([
|
|
11864
|
+
this.granular.listOpenSessions({ environmentId: this.environmentId }),
|
|
11865
|
+
this.granular.listClosedSessions({ environmentId: this.environmentId })
|
|
11866
|
+
]);
|
|
11867
|
+
return [...active, ...closed].sort(
|
|
11868
|
+
(left, right) => Date.parse(right.lastSeenAt) - Date.parse(left.lastSeenAt)
|
|
11869
|
+
);
|
|
11870
|
+
}
|
|
11871
|
+
return status === "closed" ? this.granular.listClosedSessions({ environmentId: this.environmentId }) : this.granular.listOpenSessions({ environmentId: this.environmentId });
|
|
11872
|
+
}
|
|
11873
|
+
async createSession(options) {
|
|
11874
|
+
return this.granular.createSession({
|
|
11875
|
+
environmentId: this.environmentId,
|
|
11876
|
+
clientId: options?.clientId,
|
|
11877
|
+
initialHeap: options?.initialHeap
|
|
11878
|
+
});
|
|
11879
|
+
}
|
|
11880
|
+
async connectSession(sessionId, options) {
|
|
11881
|
+
const session = await this.granular["connectSession"]({
|
|
11882
|
+
sessionId,
|
|
11883
|
+
clientId: options?.clientId
|
|
11884
|
+
});
|
|
11885
|
+
if (session.environmentId !== this.environmentId) {
|
|
11886
|
+
await session.disconnect().catch(() => {
|
|
11887
|
+
session.disconnectTransport();
|
|
11888
|
+
});
|
|
11889
|
+
throw new Error(
|
|
11890
|
+
`Session ${sessionId} belongs to environment ${session.environmentId}, not ${this.environmentId}.`
|
|
11891
|
+
);
|
|
11790
11892
|
}
|
|
11893
|
+
return session;
|
|
11894
|
+
}
|
|
11895
|
+
async reopenSession(sessionId, options) {
|
|
11896
|
+
const session = await this.granular.reopenSession(sessionId, {
|
|
11897
|
+
clientId: options?.clientId
|
|
11898
|
+
});
|
|
11899
|
+
if (session.environmentId !== this.environmentId) {
|
|
11900
|
+
await session.disconnect().catch(() => {
|
|
11901
|
+
session.disconnectTransport();
|
|
11902
|
+
});
|
|
11903
|
+
throw new Error(
|
|
11904
|
+
`Session ${sessionId} belongs to environment ${session.environmentId}, not ${this.environmentId}.`
|
|
11905
|
+
);
|
|
11906
|
+
}
|
|
11907
|
+
return session;
|
|
11908
|
+
}
|
|
11909
|
+
async closeSession(sessionId, session) {
|
|
11910
|
+
await this.granular.closeSession(sessionId, session);
|
|
11911
|
+
}
|
|
11912
|
+
async listFeedback() {
|
|
11913
|
+
const response = await this.controlPlaneRequest(`/control/environments/${this.environmentId}/feedback`);
|
|
11914
|
+
return Array.isArray(response.items) ? response.items : [];
|
|
11915
|
+
}
|
|
11916
|
+
getRuntimeBaseUrl() {
|
|
11917
|
+
return deriveRuntimeBaseUrl(this._apiEndpoint);
|
|
11791
11918
|
}
|
|
11792
11919
|
async controlPlaneRequest(path2, options = {}) {
|
|
11793
11920
|
const runtimeBase = this.getRuntimeBaseUrl();
|
|
@@ -11795,94 +11922,19 @@ var Environment = class extends Session {
|
|
|
11795
11922
|
const response = await fetch(url, {
|
|
11796
11923
|
...options,
|
|
11797
11924
|
headers: {
|
|
11798
|
-
|
|
11925
|
+
Authorization: `Bearer ${this._apiKey}`,
|
|
11799
11926
|
"Content-Type": "application/json",
|
|
11800
|
-
|
|
11927
|
+
Connection: "close",
|
|
11801
11928
|
...options.headers
|
|
11802
11929
|
}
|
|
11803
11930
|
});
|
|
11804
11931
|
if (!response.ok) {
|
|
11805
|
-
throw new Error(
|
|
11932
|
+
throw new Error(
|
|
11933
|
+
`Control Plane API Error (${response.status}): ${await response.text()}`
|
|
11934
|
+
);
|
|
11806
11935
|
}
|
|
11807
11936
|
return response.json();
|
|
11808
11937
|
}
|
|
11809
|
-
/**
|
|
11810
|
-
* Close the session and disconnect from the sandbox.
|
|
11811
|
-
*
|
|
11812
|
-
* Sends `client.goodbye` over WebSocket first, then issues an HTTP fallback
|
|
11813
|
-
* to the runtime goodbye endpoint if no definitive WS-side runtime notify
|
|
11814
|
-
* acknowledgement was observed.
|
|
11815
|
-
*/
|
|
11816
|
-
async disconnect() {
|
|
11817
|
-
let wsNotifiedRuntime = false;
|
|
11818
|
-
try {
|
|
11819
|
-
const goodbye = await this.rpc("client.goodbye", {
|
|
11820
|
-
timestamp: Date.now()
|
|
11821
|
-
});
|
|
11822
|
-
wsNotifiedRuntime = Boolean(goodbye?.ok && goodbye?.via);
|
|
11823
|
-
} catch {
|
|
11824
|
-
wsNotifiedRuntime = false;
|
|
11825
|
-
}
|
|
11826
|
-
if (!wsNotifiedRuntime) {
|
|
11827
|
-
try {
|
|
11828
|
-
const runtimeBase = this.getRuntimeBaseUrl();
|
|
11829
|
-
await fetch(
|
|
11830
|
-
`${runtimeBase}/orchestrator/runtime/environments/${this.environmentId}/session-goodbye`,
|
|
11831
|
-
{
|
|
11832
|
-
method: "POST",
|
|
11833
|
-
headers: {
|
|
11834
|
-
"Content-Type": "application/json",
|
|
11835
|
-
"Authorization": `Bearer ${this._apiKey}`,
|
|
11836
|
-
"Connection": "close"
|
|
11837
|
-
},
|
|
11838
|
-
body: JSON.stringify({
|
|
11839
|
-
reason: "sdk_disconnect_http_fallback",
|
|
11840
|
-
sessionId: this.client.currentSessionId
|
|
11841
|
-
})
|
|
11842
|
-
}
|
|
11843
|
-
);
|
|
11844
|
-
} catch {
|
|
11845
|
-
}
|
|
11846
|
-
}
|
|
11847
|
-
this.client.disconnect();
|
|
11848
|
-
}
|
|
11849
|
-
// ==================== GRAPH CONTAINER READINESS ====================
|
|
11850
|
-
/** The last known graph container status, updated by checkReadiness() or on heartbeat */
|
|
11851
|
-
graphContainerStatus = null;
|
|
11852
|
-
/**
|
|
11853
|
-
* Check if the graph container is ready and warm.
|
|
11854
|
-
*
|
|
11855
|
-
* Sends a lightweight heartbeat RPC to the Session DO which internally
|
|
11856
|
-
* pings the FalkorDB container. The response includes `graphContainerStatus`,
|
|
11857
|
-
* which is stored locally and emitted as a `readiness` event.
|
|
11858
|
-
*
|
|
11859
|
-
* Use this method to proactively warm the graph container before any
|
|
11860
|
-
* GraphQL query that requires it, or to poll the container's state in
|
|
11861
|
-
* the background.
|
|
11862
|
-
*
|
|
11863
|
-
* @returns The current graph container status object
|
|
11864
|
-
*
|
|
11865
|
-
* @example
|
|
11866
|
-
* ```typescript
|
|
11867
|
-
* const status = await env.checkReadiness();
|
|
11868
|
-
* console.log(status.status); // 'hot' | 'warming' | 'unknown'
|
|
11869
|
-
*
|
|
11870
|
-
* // Or listen for live updates
|
|
11871
|
-
* env.on('readiness', (status) => {
|
|
11872
|
-
* console.log('Graph is now:', status.status);
|
|
11873
|
-
* });
|
|
11874
|
-
* ```
|
|
11875
|
-
*/
|
|
11876
|
-
async checkReadiness() {
|
|
11877
|
-
const result = await this.client.call("client.heartbeat", {});
|
|
11878
|
-
const containerStatus = result?.graphContainerStatus ?? {
|
|
11879
|
-
lastKeepAliveAt: Date.now(),
|
|
11880
|
-
status: "unknown"
|
|
11881
|
-
};
|
|
11882
|
-
this.graphContainerStatus = containerStatus;
|
|
11883
|
-
this.emit("readiness", containerStatus);
|
|
11884
|
-
return containerStatus;
|
|
11885
|
-
}
|
|
11886
11938
|
// ==================== ID ↔ GRAPH PATH MAPPING ====================
|
|
11887
11939
|
/**
|
|
11888
11940
|
* Convert a class name + real-world ID into a unique graph path.
|
|
@@ -11911,14 +11963,14 @@ var Environment = class extends Session {
|
|
|
11911
11963
|
}
|
|
11912
11964
|
/**
|
|
11913
11965
|
* Execute a GraphQL query against the environment's graph.
|
|
11914
|
-
*
|
|
11966
|
+
*
|
|
11915
11967
|
* The query uses the Granular graph query language (based on Cypher/GraphQL).
|
|
11916
11968
|
* Authentication is handled automatically using the SDK's API key.
|
|
11917
|
-
*
|
|
11969
|
+
*
|
|
11918
11970
|
* @param query - The GraphQL query string
|
|
11919
11971
|
* @param variables - Optional variables for the query
|
|
11920
11972
|
* @returns The query result data
|
|
11921
|
-
*
|
|
11973
|
+
*
|
|
11922
11974
|
* @example
|
|
11923
11975
|
* ```typescript
|
|
11924
11976
|
* // Read the workspace
|
|
@@ -11926,7 +11978,7 @@ var Environment = class extends Session {
|
|
|
11926
11978
|
* `query { model(path: "workspace") { path label submodels { path label } } }`
|
|
11927
11979
|
* );
|
|
11928
11980
|
* console.log(result.data);
|
|
11929
|
-
*
|
|
11981
|
+
*
|
|
11930
11982
|
* // Create a model
|
|
11931
11983
|
* const created = await env.graphql(
|
|
11932
11984
|
* `mutation { at(path: "workspace") { create_submodel(subpath: "my_node", label: "My Node", prototype: "Model") { model { path label } } } }`
|
|
@@ -11938,7 +11990,7 @@ var Environment = class extends Session {
|
|
|
11938
11990
|
method: "POST",
|
|
11939
11991
|
headers: {
|
|
11940
11992
|
"Content-Type": "application/json",
|
|
11941
|
-
|
|
11993
|
+
Authorization: `Bearer ${this._apiKey}`
|
|
11942
11994
|
},
|
|
11943
11995
|
body: JSON.stringify({
|
|
11944
11996
|
environmentId: this.environmentId,
|
|
@@ -11955,10 +12007,10 @@ var Environment = class extends Session {
|
|
|
11955
12007
|
// ==================== RELATIONSHIP METHODS ====================
|
|
11956
12008
|
/**
|
|
11957
12009
|
* Define a relationship between two model types.
|
|
11958
|
-
*
|
|
12010
|
+
*
|
|
11959
12011
|
* Creates both submodels (if they don't exist) and links them with
|
|
11960
12012
|
* a RelationshipDef node that encodes cardinality.
|
|
11961
|
-
*
|
|
12013
|
+
*
|
|
11962
12014
|
* @example
|
|
11963
12015
|
* ```typescript
|
|
11964
12016
|
* // Author has many Books, Book has one Author
|
|
@@ -12018,10 +12070,10 @@ var Environment = class extends Session {
|
|
|
12018
12070
|
}
|
|
12019
12071
|
/**
|
|
12020
12072
|
* Get all relationships for a model type.
|
|
12021
|
-
*
|
|
12073
|
+
*
|
|
12022
12074
|
* @param modelPath - The model type path (e.g., "author")
|
|
12023
12075
|
* @returns Array of relationships from this model's perspective
|
|
12024
|
-
*
|
|
12076
|
+
*
|
|
12025
12077
|
* @example
|
|
12026
12078
|
* ```typescript
|
|
12027
12079
|
* const rels = await env.getRelationships('author');
|
|
@@ -12054,18 +12106,18 @@ var Environment = class extends Session {
|
|
|
12054
12106
|
}
|
|
12055
12107
|
/**
|
|
12056
12108
|
* Attach a target model to a relationship submodel.
|
|
12057
|
-
*
|
|
12109
|
+
*
|
|
12058
12110
|
* Handles cardinality automatically:
|
|
12059
12111
|
* - "One" side: sets/replaces the reference
|
|
12060
12112
|
* - "Many" side: adds the target to the collection
|
|
12061
|
-
*
|
|
12113
|
+
*
|
|
12062
12114
|
* If the target model doesn't exist, it's created as an instance of the foreign type.
|
|
12063
12115
|
* Bidirectional sync is automatic.
|
|
12064
|
-
*
|
|
12116
|
+
*
|
|
12065
12117
|
* @param modelPath - The model instance path (e.g., "tolkien")
|
|
12066
12118
|
* @param submodelPath - The relationship submodel (e.g., "books")
|
|
12067
12119
|
* @param targetPath - The target model to attach (e.g., "lord_of_the_rings")
|
|
12068
|
-
*
|
|
12120
|
+
*
|
|
12069
12121
|
* @example
|
|
12070
12122
|
* ```typescript
|
|
12071
12123
|
* // Attach a book to an author (many side)
|
|
@@ -12092,18 +12144,18 @@ var Environment = class extends Session {
|
|
|
12092
12144
|
}
|
|
12093
12145
|
/**
|
|
12094
12146
|
* Detach a target model from a relationship submodel.
|
|
12095
|
-
*
|
|
12147
|
+
*
|
|
12096
12148
|
* Handles bidirectional cleanup automatically.
|
|
12097
|
-
*
|
|
12149
|
+
*
|
|
12098
12150
|
* @param modelPath - The model instance path
|
|
12099
12151
|
* @param submodelPath - The relationship submodel
|
|
12100
12152
|
* @param targetPath - The target to detach (optional for "one" side; omit on "many" side to detach all)
|
|
12101
|
-
*
|
|
12153
|
+
*
|
|
12102
12154
|
* @example
|
|
12103
12155
|
* ```typescript
|
|
12104
12156
|
* // Detach a specific book
|
|
12105
12157
|
* await env.detach('tolkien', 'books', 'lord_of_the_rings');
|
|
12106
|
-
*
|
|
12158
|
+
*
|
|
12107
12159
|
* // Detach all books
|
|
12108
12160
|
* await env.detach('tolkien', 'books');
|
|
12109
12161
|
* ```
|
|
@@ -12127,11 +12179,11 @@ var Environment = class extends Session {
|
|
|
12127
12179
|
}
|
|
12128
12180
|
/**
|
|
12129
12181
|
* List all related models through a relationship submodel.
|
|
12130
|
-
*
|
|
12182
|
+
*
|
|
12131
12183
|
* @param modelPath - The model instance path
|
|
12132
12184
|
* @param submodelPath - The relationship submodel
|
|
12133
12185
|
* @returns Array of related model references
|
|
12134
|
-
*
|
|
12186
|
+
*
|
|
12135
12187
|
* @example
|
|
12136
12188
|
* ```typescript
|
|
12137
12189
|
* const books = await env.listRelated('tolkien', 'books');
|
|
@@ -12155,14 +12207,14 @@ var Environment = class extends Session {
|
|
|
12155
12207
|
}
|
|
12156
12208
|
/**
|
|
12157
12209
|
* Apply a manifest to the current environment's graph.
|
|
12158
|
-
*
|
|
12210
|
+
*
|
|
12159
12211
|
* Translates each manifest operation into GraphQL mutations and executes them
|
|
12160
12212
|
* in order. This is the core mechanism for creating classes, fields, and
|
|
12161
12213
|
* relationships from a declarative manifest.
|
|
12162
|
-
*
|
|
12214
|
+
*
|
|
12163
12215
|
* @param manifest - The manifest content to apply
|
|
12164
12216
|
* @returns Summary of applied operations
|
|
12165
|
-
*
|
|
12217
|
+
*
|
|
12166
12218
|
* @example
|
|
12167
12219
|
* ```typescript
|
|
12168
12220
|
* await environment.applyManifest({
|
|
@@ -12200,12 +12252,16 @@ var Environment = class extends Session {
|
|
|
12200
12252
|
applied++;
|
|
12201
12253
|
} catch (err) {
|
|
12202
12254
|
if (!err.message?.includes("already exists")) {
|
|
12203
|
-
errors.push(
|
|
12255
|
+
errors.push(
|
|
12256
|
+
`Import ${imp.name} operation failed: ${err.message}`
|
|
12257
|
+
);
|
|
12204
12258
|
}
|
|
12205
12259
|
}
|
|
12206
12260
|
}
|
|
12207
12261
|
} else {
|
|
12208
|
-
errors.push(
|
|
12262
|
+
errors.push(
|
|
12263
|
+
`Unknown module: "${imp.name}" (only built-in modules are supported)`
|
|
12264
|
+
);
|
|
12209
12265
|
}
|
|
12210
12266
|
}
|
|
12211
12267
|
}
|
|
@@ -12289,7 +12345,10 @@ var Environment = class extends Session {
|
|
|
12289
12345
|
}
|
|
12290
12346
|
}
|
|
12291
12347
|
async _applyEffectMetamodels(toolPath, metamodels) {
|
|
12292
|
-
for (const mutation of buildEffectMetamodelMutations(
|
|
12348
|
+
for (const mutation of buildEffectMetamodelMutations(
|
|
12349
|
+
toolPath,
|
|
12350
|
+
metamodels
|
|
12351
|
+
)) {
|
|
12293
12352
|
await this._runGraphql(mutation.query, mutation.label);
|
|
12294
12353
|
}
|
|
12295
12354
|
}
|
|
@@ -12338,7 +12397,9 @@ var Environment = class extends Session {
|
|
|
12338
12397
|
}
|
|
12339
12398
|
if (eventType.payloadSchema?.properties) {
|
|
12340
12399
|
const fieldSpecs = {};
|
|
12341
|
-
for (const [propName, propSchema] of Object.entries(
|
|
12400
|
+
for (const [propName, propSchema] of Object.entries(
|
|
12401
|
+
eventType.payloadSchema.properties
|
|
12402
|
+
)) {
|
|
12342
12403
|
const schema = propSchema;
|
|
12343
12404
|
fieldSpecs[propName] = {
|
|
12344
12405
|
type: schema.type ?? "string",
|
|
@@ -12621,7 +12682,9 @@ var Environment = class extends Session {
|
|
|
12621
12682
|
const wave = plans.slice(waveStart, waveStart + concurrency);
|
|
12622
12683
|
await Promise.all(
|
|
12623
12684
|
wave.map(async (plan) => {
|
|
12624
|
-
const { items, durationMs } = await this.executeRecordObjectsChunk(
|
|
12685
|
+
const { items, durationMs } = await this.executeRecordObjectsChunk(
|
|
12686
|
+
plan.slice
|
|
12687
|
+
);
|
|
12625
12688
|
if (items.length !== plan.slice.length) {
|
|
12626
12689
|
throw new Error(
|
|
12627
12690
|
`recordObjects: chunk ${plan.chunkIndex + 1} returned ${items.length} results, expected ${plan.slice.length}`
|
|
@@ -12651,13 +12714,10 @@ var Environment = class extends Session {
|
|
|
12651
12714
|
let lastError;
|
|
12652
12715
|
for (let attempt = 1; attempt <= DEFAULT_DIRECT_RECORD_OBJECTS_RETRY_COUNT; attempt += 1) {
|
|
12653
12716
|
try {
|
|
12654
|
-
const response = await this.controlPlaneRequest(
|
|
12655
|
-
|
|
12656
|
-
{
|
|
12657
|
-
|
|
12658
|
-
body: JSON.stringify({ records: chunk })
|
|
12659
|
-
}
|
|
12660
|
-
);
|
|
12717
|
+
const response = await this.controlPlaneRequest(`/control/environments/${this.environmentId}/records/batch`, {
|
|
12718
|
+
method: "POST",
|
|
12719
|
+
body: JSON.stringify({ records: chunk })
|
|
12720
|
+
});
|
|
12661
12721
|
const items = Array.isArray(response.items) ? response.items : [];
|
|
12662
12722
|
return { items, durationMs: Date.now() - wallStart };
|
|
12663
12723
|
} catch (error) {
|
|
@@ -12720,7 +12780,9 @@ var Environment = class extends Session {
|
|
|
12720
12780
|
* Fetch a single record import by id.
|
|
12721
12781
|
*/
|
|
12722
12782
|
async getRecordImport(importId) {
|
|
12723
|
-
return this.controlPlaneRequest(
|
|
12783
|
+
return this.controlPlaneRequest(
|
|
12784
|
+
`/control/record-imports/${importId}`
|
|
12785
|
+
);
|
|
12724
12786
|
}
|
|
12725
12787
|
/**
|
|
12726
12788
|
* Cancel a queued/background record import.
|
|
@@ -12733,36 +12795,186 @@ var Environment = class extends Session {
|
|
|
12733
12795
|
}
|
|
12734
12796
|
);
|
|
12735
12797
|
}
|
|
12736
|
-
|
|
12798
|
+
};
|
|
12799
|
+
var EnvironmentSession = class extends Session {
|
|
12800
|
+
environment;
|
|
12801
|
+
/** The last known graph container status, updated by checkReadiness() or on heartbeat */
|
|
12802
|
+
graphContainerStatus = null;
|
|
12803
|
+
constructor(client, environment, clientId) {
|
|
12804
|
+
super(client, clientId);
|
|
12805
|
+
this.environment = environment;
|
|
12806
|
+
}
|
|
12807
|
+
get environmentId() {
|
|
12808
|
+
return this.environment.environmentId;
|
|
12809
|
+
}
|
|
12810
|
+
get sandboxId() {
|
|
12811
|
+
return this.environment.sandboxId;
|
|
12812
|
+
}
|
|
12813
|
+
get ontologyId() {
|
|
12814
|
+
return this.environment.ontologyId;
|
|
12815
|
+
}
|
|
12816
|
+
get subjectId() {
|
|
12817
|
+
return this.environment.subjectId;
|
|
12818
|
+
}
|
|
12819
|
+
get envName() {
|
|
12820
|
+
return this.environment.envName;
|
|
12821
|
+
}
|
|
12822
|
+
get versionId() {
|
|
12823
|
+
return this.environment.versionId;
|
|
12824
|
+
}
|
|
12825
|
+
get granularId() {
|
|
12826
|
+
return this.environment.granularId;
|
|
12827
|
+
}
|
|
12828
|
+
get permissionProfileId() {
|
|
12829
|
+
return this.environment.permissionProfileId;
|
|
12830
|
+
}
|
|
12831
|
+
get apiEndpoint() {
|
|
12832
|
+
return this.environment.apiEndpoint;
|
|
12833
|
+
}
|
|
12834
|
+
get data() {
|
|
12835
|
+
return this.environment.data;
|
|
12836
|
+
}
|
|
12837
|
+
get feedback() {
|
|
12838
|
+
return this.environment.feedback;
|
|
12839
|
+
}
|
|
12737
12840
|
/**
|
|
12738
|
-
*
|
|
12841
|
+
* Return a plain JS snapshot of the synced session heap.
|
|
12739
12842
|
*/
|
|
12740
|
-
|
|
12741
|
-
|
|
12843
|
+
getHeap() {
|
|
12844
|
+
const doc = this.document;
|
|
12845
|
+
return normalizeHeapSnapshot(doc?.heap);
|
|
12846
|
+
}
|
|
12847
|
+
async graphql(query, variables) {
|
|
12848
|
+
return this.environment.graphql(query, variables);
|
|
12849
|
+
}
|
|
12850
|
+
async defineRelationship(options) {
|
|
12851
|
+
return this.environment.defineRelationship(options);
|
|
12852
|
+
}
|
|
12853
|
+
async getRelationships(modelPath) {
|
|
12854
|
+
return this.environment.getRelationships(modelPath);
|
|
12855
|
+
}
|
|
12856
|
+
async attach(modelPath, submodelPath, targetPath) {
|
|
12857
|
+
return this.environment.attach(modelPath, submodelPath, targetPath);
|
|
12858
|
+
}
|
|
12859
|
+
async detach(modelPath, submodelPath, targetPath) {
|
|
12860
|
+
return this.environment.detach(modelPath, submodelPath, targetPath);
|
|
12861
|
+
}
|
|
12862
|
+
async listRelated(modelPath, submodelPath) {
|
|
12863
|
+
return this.environment.listRelated(modelPath, submodelPath);
|
|
12864
|
+
}
|
|
12865
|
+
async applyManifest(manifest) {
|
|
12866
|
+
return this.environment.applyManifest(manifest);
|
|
12867
|
+
}
|
|
12868
|
+
async recordObject(options) {
|
|
12869
|
+
return this.environment.recordObject(options);
|
|
12870
|
+
}
|
|
12871
|
+
async recordObjects(records, options) {
|
|
12872
|
+
return this.environment.recordObjects(records, options);
|
|
12873
|
+
}
|
|
12874
|
+
async enqueueRecordImport(records, options = {}) {
|
|
12875
|
+
return this.environment.enqueueRecordImport(records, options);
|
|
12876
|
+
}
|
|
12877
|
+
async listRecordImports(status) {
|
|
12878
|
+
return this.environment.listRecordImports(status);
|
|
12879
|
+
}
|
|
12880
|
+
async getRecordImportSummary() {
|
|
12881
|
+
return this.environment.getRecordImportSummary();
|
|
12882
|
+
}
|
|
12883
|
+
async getAwaitingRecordCount() {
|
|
12884
|
+
return this.environment.getAwaitingRecordCount();
|
|
12885
|
+
}
|
|
12886
|
+
async getRecordImport(importId) {
|
|
12887
|
+
return this.environment.getRecordImport(importId);
|
|
12888
|
+
}
|
|
12889
|
+
async cancelRecordImport(importId) {
|
|
12890
|
+
return this.environment.cancelRecordImport(importId);
|
|
12891
|
+
}
|
|
12892
|
+
async listFeedback() {
|
|
12893
|
+
return this.environment.listFeedback();
|
|
12742
12894
|
}
|
|
12743
12895
|
/**
|
|
12744
|
-
*
|
|
12896
|
+
* Close the session and disconnect from the sandbox.
|
|
12897
|
+
*
|
|
12898
|
+
* Sends `client.goodbye` over WebSocket first, then issues an HTTP fallback
|
|
12899
|
+
* to the runtime goodbye endpoint if no definitive WS-side runtime notify
|
|
12900
|
+
* acknowledgement was observed.
|
|
12745
12901
|
*/
|
|
12746
|
-
async
|
|
12747
|
-
|
|
12902
|
+
async disconnect() {
|
|
12903
|
+
let wsNotifiedRuntime = false;
|
|
12904
|
+
try {
|
|
12905
|
+
const goodbye = await this.rpc(
|
|
12906
|
+
"client.goodbye",
|
|
12907
|
+
{
|
|
12908
|
+
timestamp: Date.now()
|
|
12909
|
+
}
|
|
12910
|
+
);
|
|
12911
|
+
wsNotifiedRuntime = Boolean(goodbye?.ok && goodbye?.via);
|
|
12912
|
+
} catch {
|
|
12913
|
+
wsNotifiedRuntime = false;
|
|
12914
|
+
}
|
|
12915
|
+
if (!wsNotifiedRuntime) {
|
|
12916
|
+
try {
|
|
12917
|
+
await fetch(
|
|
12918
|
+
`${this.environment.runtimeBaseUrl}/orchestrator/runtime/environments/${this.environmentId}/session-goodbye`,
|
|
12919
|
+
{
|
|
12920
|
+
method: "POST",
|
|
12921
|
+
headers: {
|
|
12922
|
+
"Content-Type": "application/json",
|
|
12923
|
+
Authorization: `Bearer ${this.environment.authToken}`,
|
|
12924
|
+
Connection: "close"
|
|
12925
|
+
},
|
|
12926
|
+
body: JSON.stringify({
|
|
12927
|
+
reason: "sdk_disconnect_http_fallback",
|
|
12928
|
+
sessionId: this.client.currentSessionId
|
|
12929
|
+
})
|
|
12930
|
+
}
|
|
12931
|
+
);
|
|
12932
|
+
} catch {
|
|
12933
|
+
}
|
|
12934
|
+
}
|
|
12935
|
+
this.client.disconnect();
|
|
12748
12936
|
}
|
|
12749
12937
|
/**
|
|
12750
|
-
*
|
|
12938
|
+
* Close only the socket transport without sending `client.goodbye`.
|
|
12751
12939
|
*/
|
|
12752
|
-
|
|
12753
|
-
|
|
12940
|
+
disconnectTransport() {
|
|
12941
|
+
this.client.disconnect();
|
|
12754
12942
|
}
|
|
12755
12943
|
/**
|
|
12756
|
-
*
|
|
12944
|
+
* Backwards-compatible alias for `disconnect()`.
|
|
12757
12945
|
*/
|
|
12758
|
-
async
|
|
12759
|
-
|
|
12946
|
+
async close() {
|
|
12947
|
+
await this.disconnect();
|
|
12760
12948
|
}
|
|
12761
12949
|
/**
|
|
12762
|
-
*
|
|
12950
|
+
* Check if the graph container is ready and warm.
|
|
12763
12951
|
*/
|
|
12764
|
-
async
|
|
12765
|
-
|
|
12952
|
+
async checkReadiness() {
|
|
12953
|
+
const result = await this.client.call("client.heartbeat", {});
|
|
12954
|
+
const containerStatus = result?.graphContainerStatus ?? {
|
|
12955
|
+
lastKeepAliveAt: Date.now(),
|
|
12956
|
+
status: "unknown"
|
|
12957
|
+
};
|
|
12958
|
+
this.graphContainerStatus = containerStatus;
|
|
12959
|
+
this.emit("readiness", containerStatus);
|
|
12960
|
+
return containerStatus;
|
|
12961
|
+
}
|
|
12962
|
+
};
|
|
12963
|
+
var OntologyHandle = class {
|
|
12964
|
+
granular;
|
|
12965
|
+
ontologyNameOrId;
|
|
12966
|
+
constructor(granular, ontologyNameOrId) {
|
|
12967
|
+
this.granular = granular;
|
|
12968
|
+
this.ontologyNameOrId = ontologyNameOrId;
|
|
12969
|
+
}
|
|
12970
|
+
get effects() {
|
|
12971
|
+
return {
|
|
12972
|
+
register: async (effect) => this.granular.registerEffect(this.ontologyNameOrId, effect),
|
|
12973
|
+
registerMany: async (effects) => this.granular.registerEffects(this.ontologyNameOrId, effects),
|
|
12974
|
+
unregister: async (name) => this.granular.unregisterEffect(this.ontologyNameOrId, name),
|
|
12975
|
+
clear: async () => this.granular.unregisterAllEffects(this.ontologyNameOrId),
|
|
12976
|
+
disconnect: async () => this.granular.disconnectEffects(this.ontologyNameOrId)
|
|
12977
|
+
};
|
|
12766
12978
|
}
|
|
12767
12979
|
};
|
|
12768
12980
|
var Granular = class _Granular {
|
|
@@ -12787,7 +12999,9 @@ var Granular = class _Granular {
|
|
|
12787
12999
|
constructor(options) {
|
|
12788
13000
|
const auth = options.token ?? options.apiKey;
|
|
12789
13001
|
if (!auth) {
|
|
12790
|
-
throw new Error(
|
|
13002
|
+
throw new Error(
|
|
13003
|
+
"Granular client requires either apiKey or token. Set GRANULAR_API_KEY or GRANULAR_TOKEN, or pass one in options."
|
|
13004
|
+
);
|
|
12791
13005
|
}
|
|
12792
13006
|
this.apiUrl = resolveApiUrl(options.apiUrl, options.endpointMode);
|
|
12793
13007
|
this.apiKey = resolveAuthTokenForApiUrl(auth, this.apiUrl);
|
|
@@ -12797,12 +13011,18 @@ var Granular = class _Granular {
|
|
|
12797
13011
|
this.onReconnectError = options.onReconnectError;
|
|
12798
13012
|
this.httpUrl = this.apiUrl.replace(/^wss:\/\//, "https://").replace(/^ws:\/\//, "http://").replace(/\/ws$/, "");
|
|
12799
13013
|
}
|
|
13014
|
+
/**
|
|
13015
|
+
* Return an ontology-scoped handle for effects and other ontology-level APIs.
|
|
13016
|
+
*/
|
|
13017
|
+
ontology(ontologyNameOrId) {
|
|
13018
|
+
return new OntologyHandle(this, ontologyNameOrId);
|
|
13019
|
+
}
|
|
12800
13020
|
/**
|
|
12801
13021
|
* Records/upserts a user and prepares them for sandbox connections
|
|
12802
|
-
*
|
|
13022
|
+
*
|
|
12803
13023
|
* @param options - User options
|
|
12804
13024
|
* @returns The recorded user with both `userId` and `granularId`
|
|
12805
|
-
*
|
|
13025
|
+
*
|
|
12806
13026
|
* @example
|
|
12807
13027
|
* ```typescript
|
|
12808
13028
|
* const user = await granular.recordUser({
|
|
@@ -12813,14 +13033,16 @@ var Granular = class _Granular {
|
|
|
12813
13033
|
* ```
|
|
12814
13034
|
*/
|
|
12815
13035
|
async recordUser(options) {
|
|
12816
|
-
const subject = normalizeSubject(
|
|
12817
|
-
|
|
12818
|
-
|
|
12819
|
-
|
|
12820
|
-
|
|
12821
|
-
|
|
13036
|
+
const subject = normalizeSubject(
|
|
13037
|
+
await this.request("/control/subjects", {
|
|
13038
|
+
method: "POST",
|
|
13039
|
+
body: JSON.stringify({
|
|
13040
|
+
identityId: options.userId,
|
|
13041
|
+
name: options.name,
|
|
13042
|
+
email: options.email
|
|
13043
|
+
})
|
|
12822
13044
|
})
|
|
12823
|
-
|
|
13045
|
+
);
|
|
12824
13046
|
return normalizeUser({
|
|
12825
13047
|
granularId: subject.granularId,
|
|
12826
13048
|
userId: options.userId,
|
|
@@ -12831,7 +13053,23 @@ var Granular = class _Granular {
|
|
|
12831
13053
|
permissions: options.permissions || []
|
|
12832
13054
|
});
|
|
12833
13055
|
}
|
|
13056
|
+
/**
|
|
13057
|
+
* Alias for `recordUser()` with user-facing naming that matches upsert semantics.
|
|
13058
|
+
*/
|
|
13059
|
+
async upsertUser(options) {
|
|
13060
|
+
return this.recordUser(options);
|
|
13061
|
+
}
|
|
12834
13062
|
async resolveConnectUser(options) {
|
|
13063
|
+
const providedIdentityCount = [
|
|
13064
|
+
Boolean(options.user),
|
|
13065
|
+
Boolean(options.userId),
|
|
13066
|
+
Boolean(options.granularId)
|
|
13067
|
+
].filter(Boolean).length;
|
|
13068
|
+
if (providedIdentityCount !== 1) {
|
|
13069
|
+
throw new Error(
|
|
13070
|
+
"openEnvironment() requires exactly one of userId, granularId, or a user object returned by recordUser()."
|
|
13071
|
+
);
|
|
13072
|
+
}
|
|
12835
13073
|
if (options.user) {
|
|
12836
13074
|
const user = normalizeUser(options.user);
|
|
12837
13075
|
return {
|
|
@@ -12867,76 +13105,141 @@ var Granular = class _Granular {
|
|
|
12867
13105
|
permissions: options.permissions || []
|
|
12868
13106
|
};
|
|
12869
13107
|
}
|
|
12870
|
-
throw new Error(
|
|
13108
|
+
throw new Error(
|
|
13109
|
+
"openEnvironment() requires either userId, granularId, or a user object returned by recordUser()."
|
|
13110
|
+
);
|
|
12871
13111
|
}
|
|
12872
13112
|
/**
|
|
12873
|
-
*
|
|
12874
|
-
*
|
|
12875
|
-
* Effects are registered at the sandbox level via `granular.registerEffect()`
|
|
12876
|
-
* or `granular.registerEffects()`. Sessions pick up live availability from
|
|
12877
|
-
* the sandbox registry automatically.
|
|
12878
|
-
*
|
|
12879
|
-
* @param options - Connection options
|
|
12880
|
-
* @returns An active environment session
|
|
12881
|
-
*
|
|
13113
|
+
* Open or resolve an ontology environment for one user without opening a session.
|
|
13114
|
+
*
|
|
12882
13115
|
* @example
|
|
12883
13116
|
* ```typescript
|
|
12884
|
-
* const environment = await granular.
|
|
13117
|
+
* const environment = await granular.openEnvironment({
|
|
12885
13118
|
* ontology: 'my-ontology',
|
|
12886
|
-
*
|
|
13119
|
+
* tag: 'dev',
|
|
12887
13120
|
* userId: 'user_123',
|
|
12888
13121
|
* permissions: ['agent'],
|
|
12889
13122
|
* });
|
|
12890
|
-
*
|
|
12891
|
-
* await
|
|
12892
|
-
*
|
|
12893
|
-
*
|
|
12894
|
-
*
|
|
12895
|
-
* handler: async () => 'Hello!',
|
|
13123
|
+
*
|
|
13124
|
+
* await environment.data.record({
|
|
13125
|
+
* className: 'customer',
|
|
13126
|
+
* id: 'acme',
|
|
13127
|
+
* fields: { name: 'Acme' },
|
|
12896
13128
|
* });
|
|
12897
|
-
*
|
|
12898
|
-
*
|
|
12899
|
-
* const job = await
|
|
12900
|
-
*
|
|
12901
|
-
* return await tools.greet({});
|
|
12902
|
-
* `);
|
|
12903
|
-
*
|
|
12904
|
-
* console.log(await job.result); // 'Hello!'
|
|
13129
|
+
*
|
|
13130
|
+
* const session = await environment.sessions.create();
|
|
13131
|
+
* const job = await session.submitJob(`return "hello";`);
|
|
13132
|
+
* console.log(await job.result);
|
|
12905
13133
|
* ```
|
|
12906
|
-
|
|
13134
|
+
*/
|
|
13135
|
+
async openEnvironment(options) {
|
|
13136
|
+
const envData = await this.resolveOpenEnvironmentData(
|
|
13137
|
+
options,
|
|
13138
|
+
"openEnvironment"
|
|
13139
|
+
);
|
|
13140
|
+
return this.bindEnvironmentHandle(envData);
|
|
13141
|
+
}
|
|
13142
|
+
/**
|
|
13143
|
+
* Deprecated compatibility alias for `openEnvironment()`.
|
|
13144
|
+
*
|
|
13145
|
+
* `connect()` no longer opens a runtime session automatically.
|
|
13146
|
+
*/
|
|
12907
13147
|
async connect(options) {
|
|
12908
|
-
|
|
13148
|
+
return this.openEnvironment({
|
|
13149
|
+
...options,
|
|
13150
|
+
tag: this.resolveRequestedTag(options, "connect"),
|
|
13151
|
+
permissions: options.permissions || options.user?.permissions || []
|
|
13152
|
+
});
|
|
13153
|
+
}
|
|
13154
|
+
resolveRequestedTag(options, methodName) {
|
|
13155
|
+
const tag = options.tag?.trim() || options.tagName?.trim() || options.environment?.trim();
|
|
13156
|
+
if (!tag) {
|
|
13157
|
+
throw new Error(`${methodName}() requires \`tag\`.`);
|
|
13158
|
+
}
|
|
13159
|
+
return tag;
|
|
13160
|
+
}
|
|
13161
|
+
buildManagedEnvironmentName(tag, versionId) {
|
|
13162
|
+
return `__sdk__${tag}__${versionId}`;
|
|
13163
|
+
}
|
|
13164
|
+
matchesTagTrackedEnvironment(environment, tagName, tagId) {
|
|
13165
|
+
const environmentTagName = environment.tag?.name || environment.buildPolicy.tagName || null;
|
|
13166
|
+
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);
|
|
13167
|
+
}
|
|
13168
|
+
sortEnvironmentsByRecency(environments) {
|
|
13169
|
+
return [...environments].sort(
|
|
13170
|
+
(left, right) => right.updatedAt - left.updatedAt
|
|
13171
|
+
);
|
|
13172
|
+
}
|
|
13173
|
+
async resolveOpenEnvironmentData(options, methodName) {
|
|
12909
13174
|
const ontology = options.ontology;
|
|
12910
13175
|
if (!ontology) {
|
|
12911
|
-
throw new Error(
|
|
13176
|
+
throw new Error(`${methodName}() requires \`ontology\`.`);
|
|
12912
13177
|
}
|
|
12913
|
-
const
|
|
12914
|
-
if (!
|
|
12915
|
-
throw new Error(
|
|
13178
|
+
const tagName = options.tag?.trim();
|
|
13179
|
+
if (!tagName) {
|
|
13180
|
+
throw new Error(`${methodName}() requires \`tag\`.`);
|
|
12916
13181
|
}
|
|
12917
|
-
const tagName = options.tagName?.trim() || void 0;
|
|
12918
13182
|
const user = await this.resolveConnectUser(options);
|
|
13183
|
+
if (!Array.isArray(user.permissions) || user.permissions.length === 0) {
|
|
13184
|
+
throw new Error(
|
|
13185
|
+
`${methodName}() requires at least one permission so the SDK can ensure assignments for new users.`
|
|
13186
|
+
);
|
|
13187
|
+
}
|
|
12919
13188
|
const sandbox = await this.findOrCreateSandbox(ontology);
|
|
12920
13189
|
for (const profileName of user.permissions) {
|
|
12921
|
-
const profileId = await this.ensurePermissionProfile(
|
|
12922
|
-
|
|
13190
|
+
const profileId = await this.ensurePermissionProfile(
|
|
13191
|
+
sandbox.sandboxId,
|
|
13192
|
+
profileName
|
|
13193
|
+
);
|
|
13194
|
+
await this.ensureAssignment(
|
|
13195
|
+
user.granularId,
|
|
13196
|
+
sandbox.sandboxId,
|
|
13197
|
+
profileId
|
|
13198
|
+
);
|
|
13199
|
+
}
|
|
13200
|
+
const tags = await this.request(
|
|
13201
|
+
`/control/sandboxes/${sandbox.sandboxId}/tags`
|
|
13202
|
+
);
|
|
13203
|
+
const tag = (tags.items || []).find(
|
|
13204
|
+
(candidate) => Boolean(candidate?.name === tagName)
|
|
13205
|
+
);
|
|
13206
|
+
if (!tag) {
|
|
13207
|
+
throw new Error(
|
|
13208
|
+
`Tag "${tagName}" was not found for ontology ${sandbox.sandboxId}.`
|
|
13209
|
+
);
|
|
13210
|
+
}
|
|
13211
|
+
const targetVersionId = tag.targetVersionId || tag.targetBuildId;
|
|
13212
|
+
if (!targetVersionId) {
|
|
13213
|
+
throw new Error(
|
|
13214
|
+
`Tag "${tagName}" does not currently point to a build/version.`
|
|
13215
|
+
);
|
|
13216
|
+
}
|
|
13217
|
+
const allEnvironments = await this.environments.list(sandbox.sandboxId);
|
|
13218
|
+
const userEnvironments = allEnvironments.filter(
|
|
13219
|
+
(environment) => environment.subjectId === user.granularId
|
|
13220
|
+
);
|
|
13221
|
+
const currentMatches = this.sortEnvironmentsByRecency(
|
|
13222
|
+
userEnvironments.filter(
|
|
13223
|
+
(environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag.tagId) && environment.versionId === targetVersionId
|
|
13224
|
+
)
|
|
13225
|
+
);
|
|
13226
|
+
if (currentMatches.length > 0) {
|
|
13227
|
+
return currentMatches[0];
|
|
13228
|
+
}
|
|
13229
|
+
const outdatedMatches = this.sortEnvironmentsByRecency(
|
|
13230
|
+
userEnvironments.filter(
|
|
13231
|
+
(environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag.tagId)
|
|
13232
|
+
)
|
|
13233
|
+
);
|
|
13234
|
+
if (outdatedMatches.length > 0 && options.createFreshIfOutdated !== true) {
|
|
13235
|
+
return outdatedMatches[0];
|
|
12923
13236
|
}
|
|
12924
|
-
|
|
13237
|
+
return this.environments.create(sandbox.sandboxId, {
|
|
12925
13238
|
subjectId: user.granularId,
|
|
12926
|
-
environment:
|
|
12927
|
-
|
|
13239
|
+
environment: this.buildManagedEnvironmentName(tagName, targetVersionId),
|
|
13240
|
+
tagId: tag.tagId,
|
|
12928
13241
|
permissionProfileId: null
|
|
12929
13242
|
});
|
|
12930
|
-
await this.activateEnvironment(envData.environmentId);
|
|
12931
|
-
const session = await this.request("/ws/sessions", {
|
|
12932
|
-
method: "POST",
|
|
12933
|
-
body: JSON.stringify({
|
|
12934
|
-
environmentId: envData.environmentId,
|
|
12935
|
-
clientId,
|
|
12936
|
-
initialHeap: options.initialHeap
|
|
12937
|
-
})
|
|
12938
|
-
});
|
|
12939
|
-
return this.bindWebSocketEnvironment(envData, clientId, session);
|
|
12940
13243
|
}
|
|
12941
13244
|
/**
|
|
12942
13245
|
* List active (open) sessions for an environment — each session is one agent conversation thread.
|
|
@@ -12971,7 +13274,9 @@ var Granular = class _Granular {
|
|
|
12971
13274
|
createdAt: _Granular.coerceIsoDate(row.createdAt ?? row.created_at),
|
|
12972
13275
|
lastSeenAt: _Granular.coerceIsoDate(row.lastSeenAt ?? row.last_seen_at),
|
|
12973
13276
|
summary: row.summary != null ? String(row.summary) : null,
|
|
12974
|
-
summaryUpdatedAt: row.summaryUpdatedAt != null || row.summary_updated_at != null ? _Granular.coerceIsoDate(
|
|
13277
|
+
summaryUpdatedAt: row.summaryUpdatedAt != null || row.summary_updated_at != null ? _Granular.coerceIsoDate(
|
|
13278
|
+
row.summaryUpdatedAt ?? row.summary_updated_at
|
|
13279
|
+
) : null,
|
|
12975
13280
|
subjectId: row.subjectId != null ? String(row.subjectId) : null,
|
|
12976
13281
|
jobCount: typeof row.jobCount === "number" ? row.jobCount : void 0,
|
|
12977
13282
|
toolCallCount: typeof row.toolCallCount === "number" ? row.toolCallCount : void 0
|
|
@@ -12997,6 +13302,7 @@ var Granular = class _Granular {
|
|
|
12997
13302
|
const clientId = options.clientId || `client_${Date.now()}`;
|
|
12998
13303
|
await this.activateEnvironment(options.environmentId);
|
|
12999
13304
|
const envData = await this.environments.get(options.environmentId);
|
|
13305
|
+
const environment = this.bindEnvironmentHandle(envData);
|
|
13000
13306
|
const session = await this.request("/ws/sessions", {
|
|
13001
13307
|
method: "POST",
|
|
13002
13308
|
body: JSON.stringify({
|
|
@@ -13005,7 +13311,7 @@ var Granular = class _Granular {
|
|
|
13005
13311
|
initialHeap: options.initialHeap
|
|
13006
13312
|
})
|
|
13007
13313
|
});
|
|
13008
|
-
return this.
|
|
13314
|
+
return this.bindWebSocketEnvironmentSession(environment, clientId, session);
|
|
13009
13315
|
}
|
|
13010
13316
|
/**
|
|
13011
13317
|
* Connect to an existing open session (same conversation thread) using a freshly minted WebSocket token.
|
|
@@ -13017,7 +13323,8 @@ var Granular = class _Granular {
|
|
|
13017
13323
|
body: JSON.stringify({})
|
|
13018
13324
|
});
|
|
13019
13325
|
const envData = await this.environments.get(minted.environmentId);
|
|
13020
|
-
|
|
13326
|
+
const environment = this.bindEnvironmentHandle(envData);
|
|
13327
|
+
return this.bindWebSocketEnvironmentSession(environment, clientId, minted);
|
|
13021
13328
|
}
|
|
13022
13329
|
/**
|
|
13023
13330
|
* Mark a session closed in the control plane. If `environment` is the connected handle for that
|
|
@@ -13049,7 +13356,11 @@ var Granular = class _Granular {
|
|
|
13049
13356
|
});
|
|
13050
13357
|
return this.connectSession({ sessionId, clientId: options?.clientId });
|
|
13051
13358
|
}
|
|
13052
|
-
|
|
13359
|
+
bindEnvironmentHandle(envData) {
|
|
13360
|
+
const graphqlEndpoint = `${this.httpUrl}/orchestrator/graphql`;
|
|
13361
|
+
return new Environment(this, envData, this.apiKey, graphqlEndpoint);
|
|
13362
|
+
}
|
|
13363
|
+
async bindWebSocketEnvironmentSession(environment, clientId, session) {
|
|
13053
13364
|
const client = new WSClient({
|
|
13054
13365
|
url: session.wsUrl,
|
|
13055
13366
|
sessionId: session.sessionId,
|
|
@@ -13060,10 +13371,13 @@ var Granular = class _Granular {
|
|
|
13060
13371
|
onReconnectError: this.onReconnectError
|
|
13061
13372
|
});
|
|
13062
13373
|
await client.connect();
|
|
13063
|
-
const
|
|
13064
|
-
|
|
13065
|
-
|
|
13066
|
-
|
|
13374
|
+
const environmentSession = new EnvironmentSession(
|
|
13375
|
+
client,
|
|
13376
|
+
environment,
|
|
13377
|
+
clientId
|
|
13378
|
+
);
|
|
13379
|
+
await environmentSession.hello();
|
|
13380
|
+
return environmentSession;
|
|
13067
13381
|
}
|
|
13068
13382
|
async activateEnvironment(environmentId) {
|
|
13069
13383
|
await this.request(`/orchestrator/runtime/environments/${environmentId}/activate`, {
|
|
@@ -13095,14 +13409,18 @@ var Granular = class _Granular {
|
|
|
13095
13409
|
};
|
|
13096
13410
|
}
|
|
13097
13411
|
async publishSandboxEffectCatalog(host) {
|
|
13098
|
-
const effects = Array.from(
|
|
13099
|
-
(
|
|
13100
|
-
);
|
|
13101
|
-
const result = await host.wsClient.call("effects.publishCatalog", {
|
|
13412
|
+
const effects = Array.from(
|
|
13413
|
+
this.getSandboxEffectMap(host.sandboxId).values()
|
|
13414
|
+
).map((effect) => this.serializeEffect(effect));
|
|
13415
|
+
const result = await host.wsClient.call("effects.publishCatalog", {
|
|
13416
|
+
effects
|
|
13417
|
+
});
|
|
13102
13418
|
const acceptedCount = typeof result?.acceptedCount === "number" ? result.acceptedCount : 0;
|
|
13103
13419
|
const rejected = Array.isArray(result?.rejected) ? result.rejected : [];
|
|
13104
13420
|
if (acceptedCount === 0 && rejected.length > 0) {
|
|
13105
|
-
const detail = rejected.map(
|
|
13421
|
+
const detail = rejected.map(
|
|
13422
|
+
(entry) => `${entry.name || "unknown"}: ${entry.reason || "rejected"}`
|
|
13423
|
+
).join("; ");
|
|
13106
13424
|
throw new Error(
|
|
13107
13425
|
`Failed to publish live effects for sandbox ${host.sandboxId}: ${detail}`
|
|
13108
13426
|
);
|
|
@@ -13136,13 +13454,15 @@ var Granular = class _Granular {
|
|
|
13136
13454
|
disconnectError
|
|
13137
13455
|
);
|
|
13138
13456
|
}
|
|
13139
|
-
void this.ensureSandboxEffectHost(host.sandboxId).catch(
|
|
13140
|
-
|
|
13141
|
-
|
|
13142
|
-
|
|
13143
|
-
|
|
13144
|
-
|
|
13145
|
-
|
|
13457
|
+
void this.ensureSandboxEffectHost(host.sandboxId).catch(
|
|
13458
|
+
(reconnectError) => {
|
|
13459
|
+
console.error(
|
|
13460
|
+
`[Granular] Failed to recover effect host for sandbox ${host.sandboxId} after heartbeat failure:`,
|
|
13461
|
+
reconnectError
|
|
13462
|
+
);
|
|
13463
|
+
console.error("[Granular] Original heartbeat failure:", error);
|
|
13464
|
+
}
|
|
13465
|
+
);
|
|
13146
13466
|
}
|
|
13147
13467
|
startEffectHostHeartbeat(host) {
|
|
13148
13468
|
if (host.heartbeatTimer) {
|
|
@@ -13163,9 +13483,15 @@ var Granular = class _Granular {
|
|
|
13163
13483
|
host.heartbeatInFlight = false;
|
|
13164
13484
|
});
|
|
13165
13485
|
};
|
|
13166
|
-
sendHeartbeat(
|
|
13486
|
+
sendHeartbeat(
|
|
13487
|
+
"[Granular] Initial effect host heartbeat failed for sandbox",
|
|
13488
|
+
false
|
|
13489
|
+
);
|
|
13167
13490
|
host.heartbeatTimer = setInterval(() => {
|
|
13168
|
-
sendHeartbeat(
|
|
13491
|
+
sendHeartbeat(
|
|
13492
|
+
"[Granular] Effect host heartbeat failed for sandbox",
|
|
13493
|
+
true
|
|
13494
|
+
);
|
|
13169
13495
|
}, 1e4);
|
|
13170
13496
|
}
|
|
13171
13497
|
stopEffectHostHeartbeat(host) {
|
|
@@ -13197,7 +13523,12 @@ var Granular = class _Granular {
|
|
|
13197
13523
|
const effectClientId = crypto.randomUUID();
|
|
13198
13524
|
const clientId = `effect-host:${sandboxId}:${effectClientId}`;
|
|
13199
13525
|
const wsClient = new WSClient({
|
|
13200
|
-
url: buildEffectHostUrl(
|
|
13526
|
+
url: buildEffectHostUrl(
|
|
13527
|
+
this.apiUrl,
|
|
13528
|
+
sandboxId,
|
|
13529
|
+
effectClientId,
|
|
13530
|
+
clientId
|
|
13531
|
+
),
|
|
13201
13532
|
sessionId: `effect-host:${effectClientId}`,
|
|
13202
13533
|
token: this.apiKey,
|
|
13203
13534
|
tokenProvider: this.tokenProvider,
|
|
@@ -13216,7 +13547,10 @@ var Granular = class _Granular {
|
|
|
13216
13547
|
};
|
|
13217
13548
|
wsClient.registerRpcHandler("effect.invoke", async (params) => {
|
|
13218
13549
|
const request = params;
|
|
13219
|
-
return invokeRegisteredEffect(
|
|
13550
|
+
return invokeRegisteredEffect(
|
|
13551
|
+
this.getSandboxEffectMap(sandboxId),
|
|
13552
|
+
request
|
|
13553
|
+
);
|
|
13220
13554
|
});
|
|
13221
13555
|
wsClient.on("open", () => {
|
|
13222
13556
|
void this.synchronizeEffectHost(host).catch((error) => {
|
|
@@ -13264,7 +13598,7 @@ var Granular = class _Granular {
|
|
|
13264
13598
|
}
|
|
13265
13599
|
/**
|
|
13266
13600
|
* Register multiple effects (tools) for a specific sandbox.
|
|
13267
|
-
*
|
|
13601
|
+
*
|
|
13268
13602
|
* batch version of `registerEffect`.
|
|
13269
13603
|
*/
|
|
13270
13604
|
async registerEffects(sandboxNameOrId, effects) {
|
|
@@ -13278,7 +13612,7 @@ var Granular = class _Granular {
|
|
|
13278
13612
|
}
|
|
13279
13613
|
/**
|
|
13280
13614
|
* Unregister an effect from a sandbox.
|
|
13281
|
-
*
|
|
13615
|
+
*
|
|
13282
13616
|
* Removes it from the local sandbox registry and updates the
|
|
13283
13617
|
* sandbox-scoped live catalog.
|
|
13284
13618
|
*/
|
|
@@ -13377,27 +13711,31 @@ var Granular = class _Granular {
|
|
|
13377
13711
|
const assignments = await this.request(
|
|
13378
13712
|
`/control/subjects/${subjectId}/assignments`
|
|
13379
13713
|
);
|
|
13380
|
-
const existing = assignments.items.find(
|
|
13381
|
-
(a) => a.sandboxId === sandboxId
|
|
13382
|
-
);
|
|
13714
|
+
const existing = assignments.items.find((a) => a.sandboxId === sandboxId);
|
|
13383
13715
|
if (existing) {
|
|
13384
13716
|
if (existing.permissionProfileId === permissionProfileId) {
|
|
13385
13717
|
return;
|
|
13386
13718
|
}
|
|
13387
|
-
await this.request(
|
|
13388
|
-
|
|
13389
|
-
|
|
13719
|
+
await this.request(
|
|
13720
|
+
`/control/assignments/${existing.assignmentId}`,
|
|
13721
|
+
{
|
|
13722
|
+
method: "DELETE"
|
|
13723
|
+
}
|
|
13724
|
+
);
|
|
13390
13725
|
}
|
|
13391
13726
|
} catch {
|
|
13392
13727
|
}
|
|
13393
|
-
await this.request(
|
|
13394
|
-
|
|
13395
|
-
|
|
13396
|
-
|
|
13397
|
-
|
|
13398
|
-
|
|
13399
|
-
|
|
13400
|
-
|
|
13728
|
+
await this.request(
|
|
13729
|
+
`/control/subjects/${subjectId}/assignments`,
|
|
13730
|
+
{
|
|
13731
|
+
method: "POST",
|
|
13732
|
+
body: JSON.stringify({
|
|
13733
|
+
sandboxId,
|
|
13734
|
+
subjectId,
|
|
13735
|
+
permissionProfileId
|
|
13736
|
+
})
|
|
13737
|
+
}
|
|
13738
|
+
);
|
|
13401
13739
|
}
|
|
13402
13740
|
/**
|
|
13403
13741
|
* Sandbox management API
|
|
@@ -13477,23 +13815,33 @@ var Granular = class _Granular {
|
|
|
13477
13815
|
},
|
|
13478
13816
|
get: async (environmentId) => {
|
|
13479
13817
|
return normalizeEnvironmentData(
|
|
13480
|
-
await this.request(
|
|
13818
|
+
await this.request(
|
|
13819
|
+
`/control/environments/${environmentId}`
|
|
13820
|
+
)
|
|
13481
13821
|
);
|
|
13482
13822
|
},
|
|
13483
13823
|
create: async (sandboxId, data) => {
|
|
13484
13824
|
const environmentName = data.environment || data.envName;
|
|
13485
|
-
return normalizeEnvironmentData(
|
|
13486
|
-
|
|
13487
|
-
|
|
13488
|
-
|
|
13489
|
-
|
|
13490
|
-
|
|
13491
|
-
|
|
13825
|
+
return normalizeEnvironmentData(
|
|
13826
|
+
await this.request(
|
|
13827
|
+
`/control/sandboxes/${sandboxId}/environments`,
|
|
13828
|
+
{
|
|
13829
|
+
method: "POST",
|
|
13830
|
+
body: JSON.stringify({
|
|
13831
|
+
...data,
|
|
13832
|
+
envName: environmentName
|
|
13833
|
+
})
|
|
13834
|
+
}
|
|
13835
|
+
)
|
|
13836
|
+
);
|
|
13492
13837
|
},
|
|
13493
13838
|
delete: async (environmentId) => {
|
|
13494
|
-
return this.request(
|
|
13495
|
-
|
|
13496
|
-
|
|
13839
|
+
return this.request(
|
|
13840
|
+
`/control/environments/${environmentId}`,
|
|
13841
|
+
{
|
|
13842
|
+
method: "DELETE"
|
|
13843
|
+
}
|
|
13844
|
+
);
|
|
13497
13845
|
}
|
|
13498
13846
|
};
|
|
13499
13847
|
}
|
|
@@ -13513,10 +13861,13 @@ var Granular = class _Granular {
|
|
|
13513
13861
|
}
|
|
13514
13862
|
if (params.since) query.set("since", params.since.toISOString());
|
|
13515
13863
|
if (params.until) query.set("until", params.until.toISOString());
|
|
13516
|
-
if (params.isAcked !== void 0)
|
|
13864
|
+
if (params.isAcked !== void 0)
|
|
13865
|
+
query.set("isAcked", params.isAcked ? "1" : "0");
|
|
13517
13866
|
if (params.limit) query.set("limit", String(params.limit));
|
|
13518
13867
|
if (params.offset) query.set("offset", String(params.offset));
|
|
13519
|
-
const result = await this.request(
|
|
13868
|
+
const result = await this.request(
|
|
13869
|
+
`/control/stream-events?${query.toString()}`
|
|
13870
|
+
);
|
|
13520
13871
|
return (result.items || []).map((row) => ({
|
|
13521
13872
|
eventId: row.event_id,
|
|
13522
13873
|
streamName: row.stream_name,
|
|
@@ -13547,7 +13898,9 @@ var Granular = class _Granular {
|
|
|
13547
13898
|
since: cursor,
|
|
13548
13899
|
limit: 100
|
|
13549
13900
|
});
|
|
13550
|
-
const orderedEvents = [...events].sort(
|
|
13901
|
+
const orderedEvents = [...events].sort(
|
|
13902
|
+
(a, b) => a.createdAt - b.createdAt
|
|
13903
|
+
);
|
|
13551
13904
|
for (const event of orderedEvents) {
|
|
13552
13905
|
if (seenEventIds.has(event.eventId)) {
|
|
13553
13906
|
continue;
|
|
@@ -13560,15 +13913,19 @@ var Granular = class _Granular {
|
|
|
13560
13913
|
params.onEvent(event);
|
|
13561
13914
|
}
|
|
13562
13915
|
} catch (err) {
|
|
13563
|
-
params.onError?.(
|
|
13916
|
+
params.onError?.(
|
|
13917
|
+
err instanceof Error ? err : new Error(String(err))
|
|
13918
|
+
);
|
|
13564
13919
|
}
|
|
13565
13920
|
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
13566
13921
|
}
|
|
13567
13922
|
};
|
|
13568
13923
|
poll();
|
|
13569
|
-
return {
|
|
13570
|
-
|
|
13571
|
-
|
|
13924
|
+
return {
|
|
13925
|
+
unsubscribe: () => {
|
|
13926
|
+
running = false;
|
|
13927
|
+
}
|
|
13928
|
+
};
|
|
13572
13929
|
},
|
|
13573
13930
|
ack: async (eventId) => {
|
|
13574
13931
|
await this.request("/control/stream-events/ack", {
|
|
@@ -13586,7 +13943,9 @@ var Granular = class _Granular {
|
|
|
13586
13943
|
const sandbox = await this._resolveSandboxId(params.ontology);
|
|
13587
13944
|
const query = new URLSearchParams({ sandboxId: sandbox });
|
|
13588
13945
|
if (params.environment) query.set("environmentId", params.environment);
|
|
13589
|
-
const result = await this.request(
|
|
13946
|
+
const result = await this.request(
|
|
13947
|
+
`/control/stream-events/stats?${query.toString()}`
|
|
13948
|
+
);
|
|
13590
13949
|
return (result.items || []).map((row) => ({
|
|
13591
13950
|
streamName: row.stream_name,
|
|
13592
13951
|
eventType: row.event_type,
|
|
@@ -13604,10 +13963,14 @@ var Granular = class _Granular {
|
|
|
13604
13963
|
get subjects() {
|
|
13605
13964
|
return {
|
|
13606
13965
|
get: async (subjectId) => {
|
|
13607
|
-
return normalizeSubject(
|
|
13966
|
+
return normalizeSubject(
|
|
13967
|
+
await this.request(`/control/subjects/${subjectId}`)
|
|
13968
|
+
);
|
|
13608
13969
|
},
|
|
13609
13970
|
listAssignments: async (subjectId) => {
|
|
13610
|
-
return this.request(
|
|
13971
|
+
return this.request(
|
|
13972
|
+
`/control/subjects/${subjectId}/assignments`
|
|
13973
|
+
);
|
|
13611
13974
|
}
|
|
13612
13975
|
};
|
|
13613
13976
|
}
|
|
@@ -13617,24 +13980,31 @@ var Granular = class _Granular {
|
|
|
13617
13980
|
get users() {
|
|
13618
13981
|
return {
|
|
13619
13982
|
create: async (data) => {
|
|
13620
|
-
return normalizeSubject(
|
|
13621
|
-
|
|
13622
|
-
|
|
13623
|
-
|
|
13624
|
-
|
|
13625
|
-
|
|
13983
|
+
return normalizeSubject(
|
|
13984
|
+
await this.request("/control/subjects", {
|
|
13985
|
+
method: "POST",
|
|
13986
|
+
body: JSON.stringify({
|
|
13987
|
+
identityId: data.id,
|
|
13988
|
+
name: data.name,
|
|
13989
|
+
email: data.email
|
|
13990
|
+
})
|
|
13626
13991
|
})
|
|
13627
|
-
|
|
13992
|
+
);
|
|
13628
13993
|
},
|
|
13629
13994
|
get: async (id) => {
|
|
13630
|
-
return normalizeSubject(
|
|
13995
|
+
return normalizeSubject(
|
|
13996
|
+
await this.request(`/control/subjects/${id}`)
|
|
13997
|
+
);
|
|
13631
13998
|
}
|
|
13632
13999
|
};
|
|
13633
14000
|
}
|
|
13634
14001
|
async _resolveSandboxId(ontologyNameOrId) {
|
|
13635
14002
|
if (ontologyNameOrId.startsWith("sbx_")) return ontologyNameOrId;
|
|
13636
|
-
const result = await this.request(
|
|
13637
|
-
|
|
14003
|
+
const result = await this.request(
|
|
14004
|
+
`/control/sandboxes?name=${encodeURIComponent(ontologyNameOrId)}`
|
|
14005
|
+
);
|
|
14006
|
+
if (result.items.length === 0)
|
|
14007
|
+
throw new Error(`Ontology not found: ${ontologyNameOrId}`);
|
|
13638
14008
|
return result.items[0].sandboxId;
|
|
13639
14009
|
}
|
|
13640
14010
|
/**
|
|
@@ -13649,9 +14019,9 @@ var Granular = class _Granular {
|
|
|
13649
14019
|
const response = await fetch(url, {
|
|
13650
14020
|
...options,
|
|
13651
14021
|
headers: {
|
|
13652
|
-
|
|
14022
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
13653
14023
|
"Content-Type": "application/json",
|
|
13654
|
-
|
|
14024
|
+
Connection: "close",
|
|
13655
14025
|
...options.headers
|
|
13656
14026
|
}
|
|
13657
14027
|
});
|
|
@@ -13662,7 +14032,11 @@ var Granular = class _Granular {
|
|
|
13662
14032
|
return response.json();
|
|
13663
14033
|
}
|
|
13664
14034
|
const errorText = await response.text();
|
|
13665
|
-
const retryable = isRetryableLocalWorkerRestart(
|
|
14035
|
+
const retryable = isRetryableLocalWorkerRestart(
|
|
14036
|
+
response.status,
|
|
14037
|
+
errorText,
|
|
14038
|
+
url
|
|
14039
|
+
);
|
|
13666
14040
|
if (retryable && attempt < LOCAL_CONTROL_REQUEST_RETRY_COUNT) {
|
|
13667
14041
|
if (this.debugHttp) {
|
|
13668
14042
|
console.warn(
|
|
@@ -13801,21 +14175,6 @@ function reviewGeneratedJobCode(code) {
|
|
|
13801
14175
|
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."
|
|
13802
14176
|
});
|
|
13803
14177
|
}
|
|
13804
|
-
const askUserCalls = normalized.match(/await\s+loop\.ask_user\s*\(\s*\{[\s\S]*?\}\s*\)/g) || [];
|
|
13805
|
-
for (const call of askUserCalls) {
|
|
13806
|
-
const usesChoiceType = /type\s*:\s*['"]choice['"]/.test(call);
|
|
13807
|
-
const usesInputType = /type\s*:\s*['"]input['"]/.test(call);
|
|
13808
|
-
const hasDisambiguationLanguage = /(which|choose|pick|select)/i.test(call) && /(invoice|order|shipment|request|case|work[\s_-]?order)/i.test(call);
|
|
13809
|
-
const includesShortlistOptions = /options\s*:\s*\[/.test(call);
|
|
13810
|
-
if (!usesChoiceType && (usesInputType || hasDisambiguationLanguage || includesShortlistOptions)) {
|
|
13811
|
-
issues.push({
|
|
13812
|
-
code: "disambiguation_requires_choice",
|
|
13813
|
-
severity: "error",
|
|
13814
|
-
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."
|
|
13815
|
-
});
|
|
13816
|
-
break;
|
|
13817
|
-
}
|
|
13818
|
-
}
|
|
13819
14178
|
}
|
|
13820
14179
|
const hasConversationalReturn = /return\s+[`'"]/.test(normalized) || /\breply\s*:/.test(normalized) || /\bagent_message\s*\(/.test(normalized) || /\bagent_text_message\s*\(/.test(normalized);
|
|
13821
14180
|
const returnsObjectLiteral = /return\s+\{[\s\S]*?\}/.test(normalized);
|
|
@@ -13861,6 +14220,9 @@ function extractFocusHintsFromActionSummary(actionSummaryLines) {
|
|
|
13861
14220
|
entryPaths: uniqueStrings(entryPaths, 8)
|
|
13862
14221
|
};
|
|
13863
14222
|
}
|
|
14223
|
+
function normalizeActionSummaryForPrompt(line) {
|
|
14224
|
+
return line.replace(/\blimit=/g, "perPage=").replace(/\blimit:/g, "perPage:");
|
|
14225
|
+
}
|
|
13864
14226
|
function getCurrentClosureId(liveDoc) {
|
|
13865
14227
|
const loop = asRecord2(liveDoc?.loop);
|
|
13866
14228
|
return typeof loop?.currentClosureId === "string" ? loop.currentClosureId : null;
|
|
@@ -14073,7 +14435,9 @@ function projectWorkflowFocus(liveDoc, pendingPrompts = [], options) {
|
|
|
14073
14435
|
variableNames: uniqueStrings(variableNames, 4),
|
|
14074
14436
|
listNames: uniqueStrings(listNames, 4),
|
|
14075
14437
|
entryPaths: uniqueStrings(entryPaths, 6),
|
|
14076
|
-
recentActionSummary: uniqueStrings(actionSummaryLines, 8)
|
|
14438
|
+
recentActionSummary: uniqueStrings(actionSummaryLines, 8).map(
|
|
14439
|
+
normalizeActionSummaryForPrompt
|
|
14440
|
+
)
|
|
14077
14441
|
};
|
|
14078
14442
|
}
|
|
14079
14443
|
function projectWorkflowSummary(liveDoc, pendingPrompts = [], options) {
|
|
@@ -14465,17 +14829,14 @@ ${resultPreview}` : null
|
|
|
14465
14829
|
].filter(Boolean).join("\n\n");
|
|
14466
14830
|
}
|
|
14467
14831
|
function buildGranularAgentDomainBlock(domainDocumentation) {
|
|
14468
|
-
return domainDocumentation?.trim() || "No domain
|
|
14832
|
+
return domainDocumentation?.trim() || "No domain reference available. The graph may not be ready yet.";
|
|
14469
14833
|
}
|
|
14470
14834
|
function buildGranularAgentSessionBlock(sessionContext) {
|
|
14471
14835
|
if (!sessionContext) return "No session metadata available.";
|
|
14472
14836
|
const rows = [
|
|
14473
14837
|
["sandboxId", sessionContext.sandboxId],
|
|
14474
14838
|
["environmentId", sessionContext.environmentId],
|
|
14475
|
-
["
|
|
14476
|
-
["granularId", sessionContext.granularId],
|
|
14477
|
-
["userName", sessionContext.userName],
|
|
14478
|
-
["domainRevision", sessionContext.domainRevision]
|
|
14839
|
+
["userName", sessionContext.userName]
|
|
14479
14840
|
];
|
|
14480
14841
|
const activeRows = rows.filter(([, value]) => Boolean(value));
|
|
14481
14842
|
if (activeRows.length === 0) return "No session metadata available.";
|
|
@@ -14484,6 +14845,9 @@ function buildGranularAgentSessionBlock(sessionContext) {
|
|
|
14484
14845
|
function buildGranularAgentHeapBlock(heapSummary) {
|
|
14485
14846
|
return heapSummary?.trim() || "Heap is empty for this session.";
|
|
14486
14847
|
}
|
|
14848
|
+
function buildGranularAgentReferentBlock(referentSummary) {
|
|
14849
|
+
return referentSummary?.trim() || "No recent referents recorded from prior assistant replies.";
|
|
14850
|
+
}
|
|
14487
14851
|
function buildGranularAgentLoopBlock(loopSummary) {
|
|
14488
14852
|
return loopSummary?.trim() || "No active loop state recorded for this session.";
|
|
14489
14853
|
}
|
|
@@ -14507,7 +14871,7 @@ function buildGranularAgentToolBlock(tools) {
|
|
|
14507
14871
|
(tool) => Boolean(tool.className && !tool.static)
|
|
14508
14872
|
);
|
|
14509
14873
|
const lines = [
|
|
14510
|
-
"Treat this block as the planning map. Use DOMAIN
|
|
14874
|
+
"Treat this block as the planning map. Use DOMAIN REFERENCE below for exact signatures and query examples."
|
|
14511
14875
|
];
|
|
14512
14876
|
const appendGroup = (title, group) => {
|
|
14513
14877
|
lines.push(`- ${title}:`);
|
|
@@ -14555,7 +14919,10 @@ function buildGranularAgentCheckpointBlock(checkpoint) {
|
|
|
14555
14919
|
if (Array.isArray(checkpoint.latestActionSummary) && checkpoint.latestActionSummary.length > 0) {
|
|
14556
14920
|
lines.push("latestActionSummary:");
|
|
14557
14921
|
for (const line of checkpoint.latestActionSummary.slice(0, 8)) {
|
|
14558
|
-
|
|
14922
|
+
const normalizedLine = normalizeActionSummaryForPrompt(line);
|
|
14923
|
+
lines.push(
|
|
14924
|
+
normalizedLine.startsWith("- ") ? normalizedLine : `- ${normalizedLine}`
|
|
14925
|
+
);
|
|
14559
14926
|
}
|
|
14560
14927
|
}
|
|
14561
14928
|
if (checkpoint.latestJobResult?.trim()) {
|
|
@@ -14571,9 +14938,10 @@ function buildGranularAgentSystemPrompt(input) {
|
|
|
14571
14938
|
const workflowBlock = buildGranularAgentWorkflowBlock(input.workflowSummary);
|
|
14572
14939
|
const checkpointBlock = buildGranularAgentCheckpointBlock(input.checkpoint);
|
|
14573
14940
|
const heapBlock = buildGranularAgentHeapBlock(input.heapSummary);
|
|
14941
|
+
const referentBlock = buildGranularAgentReferentBlock(input.referentSummary);
|
|
14574
14942
|
const loopBlock = buildGranularAgentLoopBlock(input.loopSummary);
|
|
14575
14943
|
return `You are an AI assistant for a live Granular session.
|
|
14576
|
-
You can help the user understand the domain, answer questions, or generate and execute
|
|
14944
|
+
You can help the user understand the domain, answer questions, or generate and execute code against the live session.
|
|
14577
14945
|
Your tone must be natural and human-like.
|
|
14578
14946
|
|
|
14579
14947
|
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.
|
|
@@ -14582,6 +14950,8 @@ When you call \`execute_code\`, additional assistant text must be either:
|
|
|
14582
14950
|
- a brief summary of the actions the generated code will perform.
|
|
14583
14951
|
Do not include any other kind of commentary when calling \`execute_code\`.
|
|
14584
14952
|
- 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(...)\`.
|
|
14953
|
+
- 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.
|
|
14954
|
+
- 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.
|
|
14585
14955
|
- 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.
|
|
14586
14956
|
|
|
14587
14957
|
\u2500\u2500\u2500 STREAMING COMMENT RULES \u2500\u2500\u2500
|
|
@@ -14601,6 +14971,7 @@ Do not include any other kind of commentary when calling \`execute_code\`.
|
|
|
14601
14971
|
- Do not say "sandbox" in user-facing text unless the user is explicitly asking about the runtime environment itself.
|
|
14602
14972
|
- If you need clarification, ask in everyday language.
|
|
14603
14973
|
- 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.
|
|
14974
|
+
- 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.
|
|
14604
14975
|
- Keep replies concise and clear.
|
|
14605
14976
|
- This is a conversation UI, not an API console. Favor human answers over machine-shaped payloads.
|
|
14606
14977
|
|
|
@@ -14610,9 +14981,9 @@ ${sessionBlock}
|
|
|
14610
14981
|
\u2500\u2500\u2500 CAPABILITY SNAPSHOT \u2500\u2500\u2500
|
|
14611
14982
|
${toolBlock}
|
|
14612
14983
|
|
|
14613
|
-
\u2500\u2500\u2500 DOMAIN
|
|
14984
|
+
\u2500\u2500\u2500 DOMAIN REFERENCE (from ./sandbox-tools) \u2500\u2500\u2500
|
|
14614
14985
|
Import classes and effect functions from \`./sandbox-tools\` in generated code.
|
|
14615
|
-
|
|
14986
|
+
Use the TypeScript declarations for exact signatures. When present, the generated usage notes below them show query patterns and examples.
|
|
14616
14987
|
|
|
14617
14988
|
${domainBlock}
|
|
14618
14989
|
|
|
@@ -14622,6 +14993,9 @@ ${checkpointBlock}
|
|
|
14622
14993
|
\u2500\u2500\u2500 WORKFLOW SNAPSHOT \u2500\u2500\u2500
|
|
14623
14994
|
${workflowBlock}
|
|
14624
14995
|
|
|
14996
|
+
\u2500\u2500\u2500 RECENT REFERENTS \u2500\u2500\u2500
|
|
14997
|
+
${referentBlock}
|
|
14998
|
+
|
|
14625
14999
|
\u2500\u2500\u2500 SESSION HEAP \u2500\u2500\u2500
|
|
14626
15000
|
${heapBlock}
|
|
14627
15001
|
|
|
@@ -14629,109 +15003,54 @@ ${heapBlock}
|
|
|
14629
15003
|
${loopBlock}
|
|
14630
15004
|
|
|
14631
15005
|
\u2500\u2500\u2500 LOOP PLAYBOOK \u2500\u2500\u2500
|
|
14632
|
-
-
|
|
14633
|
-
-
|
|
14634
|
-
-
|
|
14635
|
-
-
|
|
14636
|
-
-
|
|
14637
|
-
-
|
|
14638
|
-
- If
|
|
14639
|
-
-
|
|
14640
|
-
-
|
|
14641
|
-
-
|
|
14642
|
-
-
|
|
14643
|
-
-
|
|
14644
|
-
-
|
|
14645
|
-
-
|
|
14646
|
-
- Use \`loop.
|
|
14647
|
-
-
|
|
14648
|
-
-
|
|
14649
|
-
- If
|
|
14650
|
-
- 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.
|
|
14651
|
-
- 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.
|
|
14652
|
-
- 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.
|
|
14653
|
-
- 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.
|
|
14654
|
-
- 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.
|
|
14655
|
-
- 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.
|
|
14656
|
-
- Never ask for approval in plain text when \`loop.confirm(...)\` is available. Use \`loop.confirm(...)\` for consequential approval.
|
|
14657
|
-
- When the correct next step is a loop helper action, generate code and call that helper. Do not replace it with a conversational reply.
|
|
14658
|
-
- If you ask the user a new question in the current job, do not also call \`loop.close_loop(...)\` in that same job.
|
|
14659
|
-
- 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.
|
|
14660
|
-
- It is valid to branch on the value returned by \`await loop.ask_user(...)\` or \`await loop.confirm(...)\` after the job resumes.
|
|
14661
|
-
- 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".
|
|
14662
|
-
- 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."
|
|
14663
|
-
- 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.
|
|
14664
|
-
- If the user says stop, enough, or no further action, close the loop and end cleanly without asking another question.
|
|
14665
|
-
- If one clear item is already selected and the next step matters, prefer \`loop.confirm(...)\` over another exploratory question.
|
|
14666
|
-
- If one clear item is already selected and the only missing input is approval to proceed, use \`loop.confirm(...)\` rather than \`loop.ask_user(...)\`.
|
|
14667
|
-
- 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.
|
|
14668
|
-
- 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.
|
|
14669
|
-
- 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.
|
|
14670
|
-
- 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(...)\`.
|
|
14671
|
-
- 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.
|
|
14672
|
-
- Once you have one solid recommendation, prefer summarizing it and asking for approval over gathering more optional preferences.
|
|
14673
|
-
- Prefer asking the user for the next missing input over fetching extra related data they did not ask for yet.
|
|
14674
|
-
- Avoid serial menus. After one clarifying choice, prefer acting on it, asking one short text question, or confirming rather than opening another menu.
|
|
14675
|
-
- 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.
|
|
14676
|
-
- Call \`loop.close_loop(...)\` before stopping whenever the current workflow is completed, canceled, or clearly blocked.
|
|
15006
|
+
- 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.
|
|
15007
|
+
- Use CAPABILITY SNAPSHOT to choose the next step, then use DOMAIN REFERENCE for exact signatures and query shapes.
|
|
15008
|
+
- Take the minimum next step that directly helps the user. Avoid duplicate work, speculative cleanup, or extra fetching that is not needed yet.
|
|
15009
|
+
- 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.
|
|
15010
|
+
- 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.
|
|
15011
|
+
- Use RECENT REFERENTS to resolve follow-up references across turns, such as "that invoice", "that customer", "those products", or "the other one".
|
|
15012
|
+
- If the request has more than one reasonable interpretation, ask the user to clarify instead of guessing.
|
|
15013
|
+
- 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.
|
|
15014
|
+
- 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.
|
|
15015
|
+
- Reuse exact \`taskId\`, \`decisionId\`, and \`closureId\` values from AGENT LOOP STATE. Never invent or rewrite them.
|
|
15016
|
+
- If the request is ambiguous or clearly multi-step, create 2-4 short user-visible tasks and keep them updated as the workflow advances.
|
|
15017
|
+
- Use \`loop.ask_user({ type: 'choice', options: [...] })\` when you have a short, grounded shortlist the user can choose from. Otherwise use \`type: 'input'\`.
|
|
15018
|
+
- 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.
|
|
15019
|
+
- When \`type: 'choice'\` fits, do not ask the same question as plain text with bullets such as "Common options:" or "Choose one of these:".
|
|
15020
|
+
- Use \`loop.confirm(...)\` for consequential approval unless the user already clearly instructed you to perform that exact action now.
|
|
15021
|
+
- Await \`loop.ask_user(...)\` and \`loop.confirm(...)\`. After the job resumes, continue in the same job whenever the answer is enough to act.
|
|
15022
|
+
- 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.
|
|
15023
|
+
- If you ask a new question in the current job, do not also close the loop in that same job.
|
|
14677
15024
|
|
|
14678
15025
|
\u2500\u2500\u2500 CODE RULES \u2500\u2500\u2500
|
|
14679
15026
|
- Import from \`./sandbox-tools\`.
|
|
14680
|
-
- If you use \`heap\`, \`loop\`, \`agent_text_message\`,
|
|
15027
|
+
- If you use \`heap\`, \`loop\`, \`agent_text_message\`, or \`agent_heap_objects\`, import them explicitly from \`./sandbox-tools\`.
|
|
14681
15028
|
- Write top-level executable code with \`await\` at top level.
|
|
14682
|
-
- The generated job body must be plain runnable JavaScript.
|
|
14683
|
-
-
|
|
14684
|
-
-
|
|
14685
|
-
-
|
|
14686
|
-
-
|
|
14687
|
-
-
|
|
14688
|
-
-
|
|
14689
|
-
-
|
|
14690
|
-
-
|
|
14691
|
-
-
|
|
14692
|
-
-
|
|
14693
|
-
-
|
|
14694
|
-
-
|
|
14695
|
-
-
|
|
14696
|
-
-
|
|
14697
|
-
-
|
|
14698
|
-
-
|
|
14699
|
-
-
|
|
14700
|
-
-
|
|
14701
|
-
-
|
|
14702
|
-
-
|
|
14703
|
-
-
|
|
14704
|
-
-
|
|
14705
|
-
-
|
|
14706
|
-
-
|
|
14707
|
-
- 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.
|
|
14708
|
-
- When reading heap values, prefer generated generic typings such as \`await heap.getVar<Book[]>("my_books")\` or \`await heap.getVar<Book>("selected_book")\`.
|
|
14709
|
-
- 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")\`.
|
|
14710
|
-
- Use the injected \`loop\` helpers when you need to manage the workflow itself:
|
|
14711
|
-
\`loop.ask_user(...)\`, \`loop.confirm(...)\`, \`loop.open_decision(...)\`, \`loop.close_decision(...)\`,
|
|
14712
|
-
\`loop.create_task(...)\`, \`loop.update_task(...)\`, \`loop.complete_task(...)\`, and \`loop.close_loop(...)\`.
|
|
14713
|
-
- Loop helper semantics:
|
|
14714
|
-
- \`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\`.
|
|
14715
|
-
- \`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\`.
|
|
14716
|
-
- \`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.
|
|
14717
|
-
- \`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.
|
|
14718
|
-
- \`loop.create_task(...)\`, \`loop.update_task(...)\`, \`loop.complete_task(...)\`: keep a short task list that later jobs can continue and finish.
|
|
14719
|
-
- \`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.
|
|
14720
|
-
- Avoid \`as any\` and other broad casts when the DOMAIN TYPES block already tells you the correct class or list type.
|
|
14721
|
-
- Prefer manipulating heap-backed instances and typed lists instead of returning raw JSON blobs or object IDs unless the user explicitly asks for them.
|
|
14722
|
-
- 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(...)\`.
|
|
14723
|
-
- Use \`agent_text_message("...")\` for all user-visible text shown in the UI.
|
|
14724
|
-
- Use \`agent_heap_objects(...)\` only when you want the UI to render heap-backed records or lists.
|
|
14725
|
-
- If you want to show both text and records, call \`agent_text_message(...)\` and \`agent_heap_objects(...)\` separately in whatever order fits the interaction.
|
|
14726
|
-
- \`agent_text_message(...)\` should be used with a plain text string in normal generated code.
|
|
14727
|
-
- \`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(...)\`.
|
|
14728
|
-
- 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.
|
|
14729
|
-
- Do not assume heap changes will be displayed automatically. If records should appear in the UI, you must call \`agent_heap_objects(...)\`.
|
|
14730
|
-
- 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.
|
|
14731
|
-
- Do not rely on the final return value for user-visible output. A plain return value is not considered a displayed UI answer.
|
|
14732
|
-
- \`agent_message(...)\` remains available as a legacy compatibility alias, but prefer \`agent_text_message(...)\` and \`agent_heap_objects(...)\` in new code.
|
|
14733
|
-
- Do not return bare structured JSON, low-level diagnostics, or database-shaped payloads as the final answer unless the user explicitly asks for them.
|
|
14734
|
-
- 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.
|
|
15029
|
+
- The generated job body must be plain runnable JavaScript. Do not use TypeScript-only syntax.
|
|
15030
|
+
- Follow the exact classes, methods, and parameter shapes in DOMAIN REFERENCE. Do not invent helpers or unsupported arguments.
|
|
15031
|
+
- Use \`ClassName.get({ path })\` only for known graph paths when you want a direct graph fetch.
|
|
15032
|
+
- 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.
|
|
15033
|
+
- \`perPage\` defaults to \`100\` and is capped at \`100\`.
|
|
15034
|
+
- Push \`filter\`, \`search\`, and \`sort\` into graph queries instead of fetching a page and processing it locally.
|
|
15035
|
+
- 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.
|
|
15036
|
+
- 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.
|
|
15037
|
+
- If ordering alone answers the request, use \`sort\` without inventing a \`filter\`.
|
|
15038
|
+
- 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(...)\`.
|
|
15039
|
+
- 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.
|
|
15040
|
+
- Call instance methods on instances, static methods on classes, and global effects by name.
|
|
15041
|
+
- Use \`heap.getEntry(path)\` for remembered heap entries, \`heap.getList(name)\` for remembered lists, and \`heap.getVar(name)\` only for named variables.
|
|
15042
|
+
- Use \`heap.setVar(...)\` and \`heap.deleteVar(...)\` only when they help the next step.
|
|
15043
|
+
- Prefer \`heap.setVar(...)\` for scalars or one selected instance. Prefer \`ClassName.list({ saveAs })\` for reusable typed lists. Empty arrays are allowed.
|
|
15044
|
+
- 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.
|
|
15045
|
+
- Use the \`loop\` helpers to manage workflow state: \`ask_user\`, \`confirm\`, \`open_decision\`, \`close_decision\`, \`create_task\`, \`update_task\`, \`complete_task\`, and \`close_loop\`.
|
|
15046
|
+
- Use \`type: 'choice'\` only for short grounded options. Use \`type: 'input'\` when the answer should stay open-ended.
|
|
15047
|
+
- \`loop.confirm(...)\` is for consequential approval. Do not ask for approval in plain text.
|
|
15048
|
+
- After \`await loop.ask_user(...)\` or \`await loop.confirm(...)\`, continue in the same resumed job when the answer is enough to act.
|
|
15049
|
+
- Every job that answers the user must emit \`agent_text_message(...)\` and/or \`agent_heap_objects(...)\`.
|
|
15050
|
+
- Use \`agent_text_message(...)\` for user-visible text.
|
|
15051
|
+
- 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.
|
|
15052
|
+
- 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.
|
|
15053
|
+
- Keep the code small and direct. Avoid speculative branches, broad casts, and raw JSON dumps unless the user asked for them.
|
|
14735
15054
|
- Use \`console.log()\` only for intermediate diagnostics, not for the final user-facing answer.`;
|
|
14736
15055
|
}
|
|
14737
15056
|
|
|
@@ -14890,6 +15209,10 @@ function fallbackResponseText(entries, lists) {
|
|
|
14890
15209
|
return entries.length === 1 ? "I found one relevant record." : `I found ${entries.length} relevant records.`;
|
|
14891
15210
|
}
|
|
14892
15211
|
if (lists.length > 0) {
|
|
15212
|
+
const emptyOnly = lists.every((list) => (list.paths || []).length === 0);
|
|
15213
|
+
if (emptyOnly) {
|
|
15214
|
+
return lists.length === 1 ? "I saved one empty result set." : `I saved ${lists.length} empty result sets.`;
|
|
15215
|
+
}
|
|
14893
15216
|
return lists.length === 1 ? "I saved one result set." : `I saved ${lists.length} result sets.`;
|
|
14894
15217
|
}
|
|
14895
15218
|
return null;
|
|
@@ -14993,19 +15316,27 @@ function matchesPattern(text, matcher) {
|
|
|
14993
15316
|
function assertMatches(label, text, includes = [], excludes = []) {
|
|
14994
15317
|
for (const matcher of includes) {
|
|
14995
15318
|
if (!matchesPattern(text, matcher)) {
|
|
14996
|
-
throw new Error(
|
|
14997
|
-
${
|
|
15319
|
+
throw new Error(
|
|
15320
|
+
`${label} did not match ${matcherToString(matcher)}:
|
|
15321
|
+
${text}`
|
|
15322
|
+
);
|
|
14998
15323
|
}
|
|
14999
15324
|
}
|
|
15000
15325
|
for (const matcher of excludes) {
|
|
15001
15326
|
if (matchesPattern(text, matcher)) {
|
|
15002
|
-
throw new Error(
|
|
15003
|
-
${
|
|
15327
|
+
throw new Error(
|
|
15328
|
+
`${label} matched forbidden ${matcherToString(matcher)}:
|
|
15329
|
+
${text}`
|
|
15330
|
+
);
|
|
15004
15331
|
}
|
|
15005
15332
|
}
|
|
15006
15333
|
}
|
|
15007
15334
|
function buildArtifactDir(baseDir, suiteName = "granular-agent-evals") {
|
|
15008
|
-
return path.join(
|
|
15335
|
+
return path.join(
|
|
15336
|
+
baseDir || path.join(process.cwd(), "test-artifacts"),
|
|
15337
|
+
suiteName,
|
|
15338
|
+
timestampId()
|
|
15339
|
+
);
|
|
15009
15340
|
}
|
|
15010
15341
|
function createTimestampedArtifactDirectory(options) {
|
|
15011
15342
|
return buildArtifactDir(options?.baseDir, options?.suiteName);
|
|
@@ -15019,17 +15350,16 @@ function buildScenarioSteps(scenario) {
|
|
|
15019
15350
|
return scenario.steps;
|
|
15020
15351
|
}
|
|
15021
15352
|
if (!scenario.request) {
|
|
15022
|
-
throw new Error(
|
|
15353
|
+
throw new Error(
|
|
15354
|
+
`Scenario ${scenario.id} must provide either request or steps`
|
|
15355
|
+
);
|
|
15023
15356
|
}
|
|
15024
15357
|
const compatibilityStep = {
|
|
15025
15358
|
id: scenario.id,
|
|
15026
15359
|
request: scenario.request,
|
|
15027
15360
|
human: scenario.human,
|
|
15028
15361
|
expect: scenario.expect,
|
|
15029
|
-
inspect: [
|
|
15030
|
-
...asArray2(scenario.inspect),
|
|
15031
|
-
...asArray2(scenario.verify)
|
|
15032
|
-
],
|
|
15362
|
+
inspect: [...asArray2(scenario.inspect), ...asArray2(scenario.verify)],
|
|
15033
15363
|
check: scenario.check,
|
|
15034
15364
|
maxIterations: scenario.maxIterations,
|
|
15035
15365
|
setup: {
|
|
@@ -15047,22 +15377,27 @@ function buildAssistantHistoryContent(entry) {
|
|
|
15047
15377
|
${entry.content}`);
|
|
15048
15378
|
if (entry.jobStatus) parts.push(`[Job status]
|
|
15049
15379
|
${entry.jobStatus}`);
|
|
15050
|
-
if (entry.jobResultPreview)
|
|
15380
|
+
if (entry.jobResultPreview)
|
|
15381
|
+
parts.push(`[Job result]
|
|
15051
15382
|
${entry.jobResultPreview}`);
|
|
15052
15383
|
if (entry.error) parts.push(`[Job error]
|
|
15053
15384
|
${entry.error}`);
|
|
15054
15385
|
return parts.join("\n\n") || entry.content;
|
|
15055
15386
|
}
|
|
15056
15387
|
function buildHistory(entries) {
|
|
15057
|
-
return entries.reduce(
|
|
15058
|
-
|
|
15059
|
-
if (entry.
|
|
15388
|
+
return entries.reduce(
|
|
15389
|
+
(history, entry) => {
|
|
15390
|
+
if (entry.role === "user") {
|
|
15391
|
+
if (entry.content.trim())
|
|
15392
|
+
history.push({ role: "user", content: entry.content });
|
|
15393
|
+
return history;
|
|
15394
|
+
}
|
|
15395
|
+
const content = buildAssistantHistoryContent(entry).trim();
|
|
15396
|
+
if (content) history.push({ role: "assistant", content });
|
|
15060
15397
|
return history;
|
|
15061
|
-
}
|
|
15062
|
-
|
|
15063
|
-
|
|
15064
|
-
return history;
|
|
15065
|
-
}, []);
|
|
15398
|
+
},
|
|
15399
|
+
[]
|
|
15400
|
+
);
|
|
15066
15401
|
}
|
|
15067
15402
|
function getOpenPromptsFromDoc(liveDoc) {
|
|
15068
15403
|
const jobsById = asRecord4(asRecord4(liveDoc?.jobs)?.byId) || {};
|
|
@@ -15071,7 +15406,8 @@ function getOpenPromptsFromDoc(liveDoc) {
|
|
|
15071
15406
|
const promptRecords = asRecord4(asRecord4(job)?.prompts) || {};
|
|
15072
15407
|
for (const raw of Object.values(promptRecords)) {
|
|
15073
15408
|
const record = asRecord4(raw);
|
|
15074
|
-
if (!record || record.status !== "open" || typeof record.promptId !== "string")
|
|
15409
|
+
if (!record || record.status !== "open" || typeof record.promptId !== "string")
|
|
15410
|
+
continue;
|
|
15075
15411
|
const prompt = normalizePrompt({
|
|
15076
15412
|
promptId: record.promptId,
|
|
15077
15413
|
kind: record.kind,
|
|
@@ -15115,7 +15451,9 @@ ${prompt.message || ""}`;
|
|
|
15115
15451
|
return resolvePromptAnswer(prompt, rawAnswer);
|
|
15116
15452
|
}
|
|
15117
15453
|
if (fallback) return fallback({ prompt, history });
|
|
15118
|
-
throw new Error(
|
|
15454
|
+
throw new Error(
|
|
15455
|
+
`No scripted prompt responder matched prompt ${prompt.id}: ${promptText}`
|
|
15456
|
+
);
|
|
15119
15457
|
};
|
|
15120
15458
|
}
|
|
15121
15459
|
function extractJsonObject(text) {
|
|
@@ -15139,13 +15477,19 @@ function modelOutputInstruction() {
|
|
|
15139
15477
|
].join("\n");
|
|
15140
15478
|
}
|
|
15141
15479
|
function createOpenAIChatTurnGenerator(options) {
|
|
15142
|
-
const baseUrl = (options.baseUrl || "https://api.openai.com/v1").replace(
|
|
15480
|
+
const baseUrl = (options.baseUrl || "https://api.openai.com/v1").replace(
|
|
15481
|
+
/\/$/,
|
|
15482
|
+
""
|
|
15483
|
+
);
|
|
15143
15484
|
const model = options.model || "gpt-5-mini";
|
|
15144
15485
|
return async (input) => {
|
|
15145
15486
|
const messages = [
|
|
15146
|
-
{
|
|
15487
|
+
{
|
|
15488
|
+
role: "system",
|
|
15489
|
+
content: `${input.systemPrompt}
|
|
15147
15490
|
|
|
15148
|
-
${modelOutputInstruction()}`
|
|
15491
|
+
${modelOutputInstruction()}`
|
|
15492
|
+
},
|
|
15149
15493
|
...input.history,
|
|
15150
15494
|
{ role: "user", content: input.request }
|
|
15151
15495
|
];
|
|
@@ -15174,10 +15518,14 @@ ${modelOutputInstruction()}` },
|
|
|
15174
15518
|
await sleep2(500 * attempt);
|
|
15175
15519
|
continue;
|
|
15176
15520
|
}
|
|
15177
|
-
throw new Error(
|
|
15521
|
+
throw new Error(
|
|
15522
|
+
`OpenAI chat generation failed: ${response.status} ${errorText}`
|
|
15523
|
+
);
|
|
15178
15524
|
}
|
|
15179
15525
|
const raw = await response.json();
|
|
15180
|
-
const content = asRecord4(
|
|
15526
|
+
const content = asRecord4(
|
|
15527
|
+
asRecord4(raw.choices?.[0])?.message
|
|
15528
|
+
)?.content;
|
|
15181
15529
|
const text = typeof content === "string" ? content : Array.isArray(content) ? content.map((part) => asRecord4(part)?.text || "").join("") : "";
|
|
15182
15530
|
const parsed = extractJsonObject(text);
|
|
15183
15531
|
if (!parsed) {
|
|
@@ -15195,7 +15543,9 @@ ${text}`);
|
|
|
15195
15543
|
};
|
|
15196
15544
|
} catch (error) {
|
|
15197
15545
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
15198
|
-
if (attempt < 3 && /socket connection was closed unexpectedly|ECONNRESET|network|timed out/i.test(
|
|
15546
|
+
if (attempt < 3 && /socket connection was closed unexpectedly|ECONNRESET|network|timed out/i.test(
|
|
15547
|
+
lastError.message
|
|
15548
|
+
)) {
|
|
15199
15549
|
await sleep2(500 * attempt);
|
|
15200
15550
|
continue;
|
|
15201
15551
|
}
|
|
@@ -15217,7 +15567,10 @@ async function withTimeout(promise, ms, label) {
|
|
|
15217
15567
|
return await Promise.race([
|
|
15218
15568
|
promise,
|
|
15219
15569
|
new Promise((_, reject) => {
|
|
15220
|
-
timeoutId = setTimeout(
|
|
15570
|
+
timeoutId = setTimeout(
|
|
15571
|
+
() => reject(new Error(`${label} timed out after ${ms}ms`)),
|
|
15572
|
+
ms
|
|
15573
|
+
);
|
|
15221
15574
|
})
|
|
15222
15575
|
]);
|
|
15223
15576
|
} finally {
|
|
@@ -15227,7 +15580,9 @@ async function withTimeout(promise, ms, label) {
|
|
|
15227
15580
|
function getActionSummary(liveDoc, jobId) {
|
|
15228
15581
|
const jobsById = asRecord4(asRecord4(liveDoc?.jobs)?.byId) || {};
|
|
15229
15582
|
const job = asRecord4(jobsById[jobId]);
|
|
15230
|
-
return Array.isArray(job?.actionSummary) ? job.actionSummary.filter(
|
|
15583
|
+
return Array.isArray(job?.actionSummary) ? job.actionSummary.filter(
|
|
15584
|
+
(line) => typeof line === "string"
|
|
15585
|
+
) : [];
|
|
15231
15586
|
}
|
|
15232
15587
|
function normalizeHeapSnapshot2(heap) {
|
|
15233
15588
|
return {
|
|
@@ -15257,12 +15612,20 @@ async function waitForJobOutcome(input) {
|
|
|
15257
15612
|
const startedAt = Date.now();
|
|
15258
15613
|
while (Date.now() - startedAt < input.timeoutMs) {
|
|
15259
15614
|
const liveDoc = cloneJson(input.environment.document);
|
|
15260
|
-
const prompts = filterPromptsByBoundary(
|
|
15615
|
+
const prompts = filterPromptsByBoundary(
|
|
15616
|
+
liveDoc,
|
|
15617
|
+
getOpenPromptsFromDoc(liveDoc),
|
|
15618
|
+
input.boundaryTimestamp
|
|
15619
|
+
);
|
|
15261
15620
|
if (prompts.length > 0) {
|
|
15262
15621
|
return { kind: "prompt", prompts, liveDoc, stdout, stderr };
|
|
15263
15622
|
}
|
|
15264
15623
|
try {
|
|
15265
|
-
const result = await withTimeout(
|
|
15624
|
+
const result = await withTimeout(
|
|
15625
|
+
input.job.result,
|
|
15626
|
+
input.pollIntervalMs,
|
|
15627
|
+
`job ${input.job.id} tick`
|
|
15628
|
+
);
|
|
15266
15629
|
return { kind: "completed", result, liveDoc, stdout, stderr };
|
|
15267
15630
|
} catch (error) {
|
|
15268
15631
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -15328,7 +15691,9 @@ function buildResultReport(result) {
|
|
|
15328
15691
|
...result.actionSummary.length ? result.actionSummary.map((line) => `- ${line}`) : ["- None"],
|
|
15329
15692
|
"",
|
|
15330
15693
|
"## Prompt Interactions",
|
|
15331
|
-
...result.promptInteractions.length ? result.promptInteractions.map(
|
|
15694
|
+
...result.promptInteractions.length ? result.promptInteractions.map(
|
|
15695
|
+
(interaction) => `- [${interaction.type}] ${interaction.message || interaction.title} -> ${JSON.stringify(interaction.answer)}`
|
|
15696
|
+
) : ["- None"],
|
|
15332
15697
|
"",
|
|
15333
15698
|
...stepSection,
|
|
15334
15699
|
"## Raw Files",
|
|
@@ -15343,7 +15708,9 @@ function buildSuiteIndex(results) {
|
|
|
15343
15708
|
const lines = [
|
|
15344
15709
|
"# Agent Eval Report Index",
|
|
15345
15710
|
"",
|
|
15346
|
-
...results.map(
|
|
15711
|
+
...results.map(
|
|
15712
|
+
(result) => `- [${result.scenario.id}](./${result.scenario.id}/REPORT.md) - ${result.status}`
|
|
15713
|
+
)
|
|
15347
15714
|
];
|
|
15348
15715
|
return `${lines.join("\n")}
|
|
15349
15716
|
`;
|
|
@@ -15357,7 +15724,7 @@ async function applySetup(setup, context) {
|
|
|
15357
15724
|
await context.environment.recordObjects(setup.records);
|
|
15358
15725
|
}
|
|
15359
15726
|
if (setup.effects?.length) {
|
|
15360
|
-
await context.environment.
|
|
15727
|
+
await context.granular.ontology(context.environment.sandboxId).effects.registerMany(setup.effects);
|
|
15361
15728
|
}
|
|
15362
15729
|
if (setup.run) {
|
|
15363
15730
|
await setup.run(context);
|
|
@@ -15379,6 +15746,7 @@ async function runAgentEvalSuite(options) {
|
|
|
15379
15746
|
);
|
|
15380
15747
|
try {
|
|
15381
15748
|
await applySetup(scenario.setup, {
|
|
15749
|
+
granular: options.harness.granular,
|
|
15382
15750
|
conversation,
|
|
15383
15751
|
environment: conversation.environment,
|
|
15384
15752
|
turnDir: conversation.artifactDir
|
|
@@ -15391,14 +15759,19 @@ async function runAgentEvalSuite(options) {
|
|
|
15391
15759
|
conversation,
|
|
15392
15760
|
request: step.request,
|
|
15393
15761
|
prepare: async (ctx) => {
|
|
15394
|
-
await applySetup(step.setup,
|
|
15762
|
+
await applySetup(step.setup, {
|
|
15763
|
+
granular: options.harness.granular,
|
|
15764
|
+
...ctx
|
|
15765
|
+
});
|
|
15395
15766
|
},
|
|
15396
15767
|
human: step.human,
|
|
15397
15768
|
maxIterations: step.maxIterations,
|
|
15398
15769
|
autoAnswerPrompts: step.autoAnswerPrompts
|
|
15399
15770
|
});
|
|
15400
15771
|
if ("prompts" in completed) {
|
|
15401
|
-
throw new Error(
|
|
15772
|
+
throw new Error(
|
|
15773
|
+
`Scenario ${scenario.id} step ${index + 1} paused for human input instead of completing automatically`
|
|
15774
|
+
);
|
|
15402
15775
|
}
|
|
15403
15776
|
const inspectionResults = [];
|
|
15404
15777
|
const stepChecks = asArray2(step.check);
|
|
@@ -15414,12 +15787,23 @@ async function runAgentEvalSuite(options) {
|
|
|
15414
15787
|
actionSummary: completed.actionSummary,
|
|
15415
15788
|
promptInteractions: completed.promptInteractions,
|
|
15416
15789
|
result: completed.result,
|
|
15417
|
-
heap: normalizeHeapSnapshot2(
|
|
15418
|
-
|
|
15790
|
+
heap: normalizeHeapSnapshot2(
|
|
15791
|
+
asRecord4(
|
|
15792
|
+
cloneJson(conversation.environment.document)?.heap
|
|
15793
|
+
)
|
|
15794
|
+
),
|
|
15795
|
+
openPrompts: getOpenPromptsFromDoc(
|
|
15796
|
+
cloneJson(conversation.environment.document)
|
|
15797
|
+
),
|
|
15419
15798
|
liveDoc: cloneJson(conversation.environment.document),
|
|
15420
15799
|
inspect: async (code) => {
|
|
15421
|
-
const
|
|
15422
|
-
|
|
15800
|
+
const session = conversation.environment;
|
|
15801
|
+
const job = await session.submitJob(code);
|
|
15802
|
+
return withTimeout(
|
|
15803
|
+
job.result,
|
|
15804
|
+
9e4,
|
|
15805
|
+
`inspection job ${job.id}`
|
|
15806
|
+
);
|
|
15423
15807
|
},
|
|
15424
15808
|
assertMatches
|
|
15425
15809
|
};
|
|
@@ -15477,7 +15861,9 @@ async function runAgentEvalSuite(options) {
|
|
|
15477
15861
|
}
|
|
15478
15862
|
const lastStep = stepResults[stepResults.length - 1];
|
|
15479
15863
|
if (!lastStep) {
|
|
15480
|
-
throw new Error(
|
|
15864
|
+
throw new Error(
|
|
15865
|
+
`Scenario ${scenario.id} produced no completed steps`
|
|
15866
|
+
);
|
|
15481
15867
|
}
|
|
15482
15868
|
const result = {
|
|
15483
15869
|
scenario,
|
|
@@ -15491,9 +15877,18 @@ async function runAgentEvalSuite(options) {
|
|
|
15491
15877
|
steps: stepResults,
|
|
15492
15878
|
turnDir: conversation.artifactDir
|
|
15493
15879
|
};
|
|
15494
|
-
await writeJson(
|
|
15495
|
-
|
|
15496
|
-
|
|
15880
|
+
await writeJson(
|
|
15881
|
+
path.join(conversation.artifactDir, "result.json"),
|
|
15882
|
+
result
|
|
15883
|
+
);
|
|
15884
|
+
await writeJson(
|
|
15885
|
+
path.join(conversation.artifactDir, "report.json"),
|
|
15886
|
+
result
|
|
15887
|
+
);
|
|
15888
|
+
await writeFile(
|
|
15889
|
+
path.join(conversation.artifactDir, "REPORT.md"),
|
|
15890
|
+
buildResultReport(result)
|
|
15891
|
+
);
|
|
15497
15892
|
finalResult = result;
|
|
15498
15893
|
} catch (error) {
|
|
15499
15894
|
const failureMessage = error instanceof Error ? error.message : String(error);
|
|
@@ -15515,7 +15910,10 @@ async function runAgentEvalSuite(options) {
|
|
|
15515
15910
|
await ensureDir(failed.turnDir);
|
|
15516
15911
|
await writeJson(path.join(failed.turnDir, "result.json"), failed);
|
|
15517
15912
|
await writeJson(path.join(failed.turnDir, "report.json"), failed);
|
|
15518
|
-
await writeFile(
|
|
15913
|
+
await writeFile(
|
|
15914
|
+
path.join(failed.turnDir, "REPORT.md"),
|
|
15915
|
+
buildResultReport(failed)
|
|
15916
|
+
);
|
|
15519
15917
|
finalResult = failed;
|
|
15520
15918
|
} finally {
|
|
15521
15919
|
await options.harness.closeConversation(conversation);
|
|
@@ -15536,12 +15934,21 @@ async function runAgentEvalSuite(options) {
|
|
|
15536
15934
|
}
|
|
15537
15935
|
results.push(finalResult);
|
|
15538
15936
|
}
|
|
15539
|
-
await writeJson(
|
|
15540
|
-
|
|
15937
|
+
await writeJson(
|
|
15938
|
+
path.join(options.harness.artifactDir, "summary.json"),
|
|
15939
|
+
results
|
|
15940
|
+
);
|
|
15941
|
+
await writeFile(
|
|
15942
|
+
path.join(options.harness.artifactDir, "REPORT_INDEX.md"),
|
|
15943
|
+
buildSuiteIndex(results)
|
|
15944
|
+
);
|
|
15541
15945
|
return { artifactDir: options.harness.artifactDir, results };
|
|
15542
15946
|
}
|
|
15543
15947
|
function createAgentEvalHarness(options) {
|
|
15544
|
-
const artifactDir = buildArtifactDir(
|
|
15948
|
+
const artifactDir = buildArtifactDir(
|
|
15949
|
+
options.artifactBaseDir,
|
|
15950
|
+
options.suiteName
|
|
15951
|
+
);
|
|
15545
15952
|
const controllerBudgets = {
|
|
15546
15953
|
...DEFAULT_CONTROLLER_BUDGETS,
|
|
15547
15954
|
...options.controllerBudgets || {}
|
|
@@ -15553,7 +15960,9 @@ function createAgentEvalHarness(options) {
|
|
|
15553
15960
|
await ensureDir(artifactDir);
|
|
15554
15961
|
const clientId = `${slugify(label)}-${Date.now()}`;
|
|
15555
15962
|
if (!options.openEnvironment && !options.environmentId) {
|
|
15556
|
-
throw new Error(
|
|
15963
|
+
throw new Error(
|
|
15964
|
+
"createAgentEvalHarness requires either environmentId or openEnvironment"
|
|
15965
|
+
);
|
|
15557
15966
|
}
|
|
15558
15967
|
const environment = options.openEnvironment ? await options.openEnvironment({ label, clientId }) : await options.granular.createSession({
|
|
15559
15968
|
environmentId: options.environmentId,
|
|
@@ -15577,12 +15986,15 @@ function createAgentEvalHarness(options) {
|
|
|
15577
15986
|
}
|
|
15578
15987
|
async function closeConversation(conversation) {
|
|
15579
15988
|
try {
|
|
15580
|
-
await options.granular.closeSession(
|
|
15989
|
+
await options.granular.closeSession(
|
|
15990
|
+
conversation.environment.sessionId,
|
|
15991
|
+
conversation.environment
|
|
15992
|
+
);
|
|
15581
15993
|
} catch {
|
|
15582
15994
|
}
|
|
15583
15995
|
}
|
|
15584
|
-
async function runCheckJob(code,
|
|
15585
|
-
const job = await
|
|
15996
|
+
async function runCheckJob(code, session) {
|
|
15997
|
+
const job = await session.submitJob(code);
|
|
15586
15998
|
return withTimeout(job.result, jobTimeoutMs, `check job ${job.id}`);
|
|
15587
15999
|
}
|
|
15588
16000
|
function buildCheckContext(conversation, completed, turnDir) {
|
|
@@ -15627,8 +16039,12 @@ function createAgentEvalHarness(options) {
|
|
|
15627
16039
|
async function resumePendingTurn(pending, responder) {
|
|
15628
16040
|
const prompt = pending.prompts[0];
|
|
15629
16041
|
if (!prompt) throw new Error("Pending turn has no prompts to answer");
|
|
15630
|
-
const answer = await responder({
|
|
15631
|
-
|
|
16042
|
+
const answer = await responder({
|
|
16043
|
+
prompt,
|
|
16044
|
+
history: pending.promptInteractions
|
|
16045
|
+
});
|
|
16046
|
+
const session = pending.conversation.environment;
|
|
16047
|
+
await session.answerPrompt(prompt.id, answer);
|
|
15632
16048
|
pending.promptInteractions.push({
|
|
15633
16049
|
promptId: prompt.id,
|
|
15634
16050
|
type: prompt.type,
|
|
@@ -15652,7 +16068,9 @@ function createAgentEvalHarness(options) {
|
|
|
15652
16068
|
};
|
|
15653
16069
|
}
|
|
15654
16070
|
await sleep2(350);
|
|
15655
|
-
const liveDoc = cloneJson(
|
|
16071
|
+
const liveDoc = cloneJson(
|
|
16072
|
+
pending.conversation.environment.document
|
|
16073
|
+
);
|
|
15656
16074
|
const presentation = resolveJobPresentation({
|
|
15657
16075
|
jobId: pending.job.id,
|
|
15658
16076
|
result: resumed.result,
|
|
@@ -15697,25 +16115,40 @@ function createAgentEvalHarness(options) {
|
|
|
15697
16115
|
await conversation.environment.recordObjects(input.prepareRecords);
|
|
15698
16116
|
}
|
|
15699
16117
|
if (input.prepareTools?.length) {
|
|
15700
|
-
await conversation.environment.
|
|
16118
|
+
await options.granular.ontology(conversation.environment.sandboxId).effects.registerMany(input.prepareTools);
|
|
15701
16119
|
}
|
|
15702
16120
|
if (input.prepare) {
|
|
15703
|
-
await input.prepare({
|
|
16121
|
+
await input.prepare({
|
|
16122
|
+
conversation,
|
|
16123
|
+
environment: conversation.environment,
|
|
16124
|
+
turnDir
|
|
16125
|
+
});
|
|
15704
16126
|
}
|
|
15705
16127
|
const boundaryTimestamp = Date.now();
|
|
15706
16128
|
conversation.history.push({ role: "user", content: input.request });
|
|
15707
|
-
await writeJson(path.join(turnDir, "request.json"), {
|
|
16129
|
+
await writeJson(path.join(turnDir, "request.json"), {
|
|
16130
|
+
request: input.request,
|
|
16131
|
+
boundaryTimestamp
|
|
16132
|
+
});
|
|
15708
16133
|
let iteration = 0;
|
|
15709
16134
|
let noProgressCount = 0;
|
|
15710
16135
|
let previousSnapshot = null;
|
|
15711
16136
|
let latestCheckpoint = null;
|
|
15712
16137
|
const maxIterations = input.maxIterations || controllerBudgets.maxIterations;
|
|
15713
16138
|
const autoAnswerPrompts = input.autoAnswerPrompts ?? true;
|
|
15714
|
-
const baselineClosureId = getCurrentClosureId(
|
|
16139
|
+
const baselineClosureId = getCurrentClosureId(
|
|
16140
|
+
cloneJson(conversation.environment.document)
|
|
16141
|
+
);
|
|
15715
16142
|
while (iteration < maxIterations) {
|
|
15716
16143
|
const liveDoc = cloneJson(conversation.environment.document);
|
|
15717
|
-
const pendingPrompts = filterPromptsByBoundary(
|
|
15718
|
-
|
|
16144
|
+
const pendingPrompts = filterPromptsByBoundary(
|
|
16145
|
+
liveDoc,
|
|
16146
|
+
getOpenPromptsFromDoc(liveDoc),
|
|
16147
|
+
boundaryTimestamp
|
|
16148
|
+
);
|
|
16149
|
+
const workflowFocus = projectWorkflowFocus(liveDoc, pendingPrompts, {
|
|
16150
|
+
boundaryTimestamp
|
|
16151
|
+
});
|
|
15719
16152
|
const systemPrompt = buildGranularAgentSystemPrompt({
|
|
15720
16153
|
domainDocumentation: await conversation.environment.getDomainDocumentation(),
|
|
15721
16154
|
sessionContext: {
|
|
@@ -15726,8 +16159,12 @@ function createAgentEvalHarness(options) {
|
|
|
15726
16159
|
heapSummary: projectHeapSummary(liveDoc, {
|
|
15727
16160
|
focus: workflowFocus
|
|
15728
16161
|
}),
|
|
15729
|
-
loopSummary: projectLoopSummary(liveDoc, pendingPrompts, {
|
|
15730
|
-
|
|
16162
|
+
loopSummary: projectLoopSummary(liveDoc, pendingPrompts, {
|
|
16163
|
+
boundaryTimestamp
|
|
16164
|
+
}),
|
|
16165
|
+
workflowSummary: projectWorkflowSummary(liveDoc, pendingPrompts, {
|
|
16166
|
+
boundaryTimestamp
|
|
16167
|
+
}),
|
|
15731
16168
|
tools: conversation.environment.getEffects().map((tool) => ({
|
|
15732
16169
|
name: tool.name,
|
|
15733
16170
|
description: tool.description,
|
|
@@ -15737,14 +16174,23 @@ function createAgentEvalHarness(options) {
|
|
|
15737
16174
|
})),
|
|
15738
16175
|
checkpoint: latestCheckpoint
|
|
15739
16176
|
});
|
|
15740
|
-
const request = iteration === 0 ? input.request : buildContinuationInstruction(
|
|
15741
|
-
|
|
15742
|
-
|
|
15743
|
-
|
|
15744
|
-
|
|
15745
|
-
|
|
15746
|
-
|
|
15747
|
-
|
|
16177
|
+
const request = iteration === 0 ? input.request : buildContinuationInstruction(
|
|
16178
|
+
buildContinuationPreview(latestCheckpoint, noProgressCount)
|
|
16179
|
+
);
|
|
16180
|
+
const generation = await withTimeout(
|
|
16181
|
+
generateTurnWithRepair(options.generator, {
|
|
16182
|
+
systemPrompt,
|
|
16183
|
+
history: buildHistory(conversation.history),
|
|
16184
|
+
request,
|
|
16185
|
+
attempt: 1
|
|
16186
|
+
}),
|
|
16187
|
+
chatTimeoutMs,
|
|
16188
|
+
`chat generation for ${conversation.label} iteration ${iteration + 1}`
|
|
16189
|
+
);
|
|
16190
|
+
await writeJson(
|
|
16191
|
+
path.join(turnDir, `iteration-${iteration + 1}-generation.json`),
|
|
16192
|
+
generation
|
|
16193
|
+
);
|
|
15748
16194
|
if (!generation.code) {
|
|
15749
16195
|
const responseText2 = generation.reply?.trim() || "Done.";
|
|
15750
16196
|
conversation.history.push({ role: "assistant", content: responseText2 });
|
|
@@ -15760,12 +16206,18 @@ function createAgentEvalHarness(options) {
|
|
|
15760
16206
|
result: generation.reply?.trim() || responseText2
|
|
15761
16207
|
};
|
|
15762
16208
|
if (input.verification) {
|
|
15763
|
-
completed.verification = await runInspection(
|
|
16209
|
+
completed.verification = await runInspection(
|
|
16210
|
+
conversation,
|
|
16211
|
+
input.verification,
|
|
16212
|
+
completed,
|
|
16213
|
+
turnDir
|
|
16214
|
+
);
|
|
15764
16215
|
}
|
|
15765
16216
|
await writeJson(path.join(turnDir, "result.json"), completed);
|
|
15766
16217
|
return completed;
|
|
15767
16218
|
}
|
|
15768
|
-
const
|
|
16219
|
+
const session = conversation.environment;
|
|
16220
|
+
const job = await session.submitJob(generation.code);
|
|
15769
16221
|
const outcome = await waitForJobOutcome({
|
|
15770
16222
|
environment: conversation.environment,
|
|
15771
16223
|
job,
|
|
@@ -15790,7 +16242,9 @@ function createAgentEvalHarness(options) {
|
|
|
15790
16242
|
};
|
|
15791
16243
|
}
|
|
15792
16244
|
if (!input.human) {
|
|
15793
|
-
throw new Error(
|
|
16245
|
+
throw new Error(
|
|
16246
|
+
"This turn reached a human prompt but no responder was provided"
|
|
16247
|
+
);
|
|
15794
16248
|
}
|
|
15795
16249
|
let pending = {
|
|
15796
16250
|
conversation,
|
|
@@ -15812,16 +16266,25 @@ function createAgentEvalHarness(options) {
|
|
|
15812
16266
|
continue;
|
|
15813
16267
|
}
|
|
15814
16268
|
if (input.verification) {
|
|
15815
|
-
resumed.verification = await runInspection(
|
|
16269
|
+
resumed.verification = await runInspection(
|
|
16270
|
+
conversation,
|
|
16271
|
+
input.verification,
|
|
16272
|
+
resumed,
|
|
16273
|
+
turnDir
|
|
16274
|
+
);
|
|
15816
16275
|
}
|
|
15817
16276
|
return resumed;
|
|
15818
16277
|
}
|
|
15819
16278
|
}
|
|
15820
16279
|
if (outcome.kind !== "completed") {
|
|
15821
|
-
throw new Error(
|
|
16280
|
+
throw new Error(
|
|
16281
|
+
"Unexpected non-completed outcome after prompt handling"
|
|
16282
|
+
);
|
|
15822
16283
|
}
|
|
15823
16284
|
await sleep2(350);
|
|
15824
|
-
const settledLiveDoc = cloneJson(
|
|
16285
|
+
const settledLiveDoc = cloneJson(
|
|
16286
|
+
conversation.environment.document
|
|
16287
|
+
);
|
|
15825
16288
|
const sessionHeap = normalizeHeapSnapshot2(asRecord4(settledLiveDoc?.heap));
|
|
15826
16289
|
const presentation = resolveJobPresentation({
|
|
15827
16290
|
jobId: job.id,
|
|
@@ -15842,7 +16305,11 @@ function createAgentEvalHarness(options) {
|
|
|
15842
16305
|
baselineClosureId,
|
|
15843
16306
|
currentClosureId: getCurrentClosureId(settledLiveDoc),
|
|
15844
16307
|
liveDoc: settledLiveDoc,
|
|
15845
|
-
pendingPrompts: filterPromptsByBoundary(
|
|
16308
|
+
pendingPrompts: filterPromptsByBoundary(
|
|
16309
|
+
settledLiveDoc,
|
|
16310
|
+
getOpenPromptsFromDoc(settledLiveDoc),
|
|
16311
|
+
boundaryTimestamp
|
|
16312
|
+
),
|
|
15846
16313
|
projectionOptions: { boundaryTimestamp },
|
|
15847
16314
|
latestResponseText: responseText,
|
|
15848
16315
|
previousSnapshot,
|
|
@@ -15867,12 +16334,15 @@ function createAgentEvalHarness(options) {
|
|
|
15867
16334
|
jobStatus: "succeeded",
|
|
15868
16335
|
jobResultPreview: JSON.stringify(outcome.result, null, 2)
|
|
15869
16336
|
});
|
|
15870
|
-
await writeJson(
|
|
15871
|
-
|
|
15872
|
-
|
|
15873
|
-
|
|
15874
|
-
|
|
15875
|
-
|
|
16337
|
+
await writeJson(
|
|
16338
|
+
path.join(turnDir, `iteration-${iteration + 1}-result.json`),
|
|
16339
|
+
{
|
|
16340
|
+
responseText,
|
|
16341
|
+
continuation,
|
|
16342
|
+
actionSummary: latestCheckpoint.latestActionSummary,
|
|
16343
|
+
result: outcome.result
|
|
16344
|
+
}
|
|
16345
|
+
);
|
|
15876
16346
|
if (!continuation.shouldContinue) {
|
|
15877
16347
|
const completed = {
|
|
15878
16348
|
conversation,
|
|
@@ -15887,17 +16357,25 @@ function createAgentEvalHarness(options) {
|
|
|
15887
16357
|
result: outcome.result
|
|
15888
16358
|
};
|
|
15889
16359
|
if (input.verification) {
|
|
15890
|
-
completed.verification = await runInspection(
|
|
16360
|
+
completed.verification = await runInspection(
|
|
16361
|
+
conversation,
|
|
16362
|
+
input.verification,
|
|
16363
|
+
completed,
|
|
16364
|
+
turnDir
|
|
16365
|
+
);
|
|
15891
16366
|
}
|
|
15892
16367
|
await writeJson(path.join(turnDir, "result.json"), completed);
|
|
15893
16368
|
return completed;
|
|
15894
16369
|
}
|
|
15895
16370
|
iteration += 1;
|
|
15896
16371
|
}
|
|
15897
|
-
throw new Error(
|
|
16372
|
+
throw new Error(
|
|
16373
|
+
`Turn exceeded iteration budget (${maxIterations}) for ${conversation.label}`
|
|
16374
|
+
);
|
|
15898
16375
|
}
|
|
15899
16376
|
return {
|
|
15900
16377
|
artifactDir,
|
|
16378
|
+
granular: options.granular,
|
|
15901
16379
|
openConversation,
|
|
15902
16380
|
closeConversation,
|
|
15903
16381
|
runTurn,
|
|
@@ -15933,12 +16411,11 @@ function createAgentTester(options) {
|
|
|
15933
16411
|
}
|
|
15934
16412
|
if ("connect" in options.target && !connectSeeded) {
|
|
15935
16413
|
connectSeeded = true;
|
|
15936
|
-
const environment = await granular.
|
|
15937
|
-
...options.target.connect
|
|
15938
|
-
clientId
|
|
16414
|
+
const environment = await granular.openEnvironment({
|
|
16415
|
+
...options.target.connect
|
|
15939
16416
|
});
|
|
15940
16417
|
resolvedEnvironmentId = environment.environmentId;
|
|
15941
|
-
return environment;
|
|
16418
|
+
return environment.sessions.create({ clientId });
|
|
15942
16419
|
}
|
|
15943
16420
|
if ("createEnvironment" in options.target && !resolvedEnvironmentId) {
|
|
15944
16421
|
const envData = await granular.environments.create(
|