@opengeni/core 1.0.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/billing/agent-run-admission.d.ts +17 -0
- package/dist/default-session-model.d.ts +5 -3
- package/dist/dependencies.d.ts +3 -1
- package/dist/domain/knowledge-messages.d.ts +2 -2
- package/dist/goal-admission.d.ts +488 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +287 -52
- package/dist/index.js.map +1 -1
- package/package.json +12 -12
- package/src/application/session-tenancy.ts +4 -8
- package/src/application/user-resource-grants.ts +0 -13
- package/src/billing/agent-run-admission.ts +107 -0
- package/src/billing/limits.ts +14 -3
- package/src/default-session-model.ts +23 -12
- package/src/dependencies.ts +3 -0
- package/src/domain/knowledge-search.test.ts +1 -1
- package/src/domain/knowledge-search.ts +2 -2
- package/src/domain/session-tool-policy.ts +14 -5
- package/src/domain/sessions.ts +11 -6
- package/src/goal-admission.ts +187 -0
- package/src/index.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/core",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "OpenGeni framework-agnostic core: the domain, access, and billing layers (neutral access, off-HTTP V2 surface). Behavior-preserving extraction from apps/api — keeps Hono's HTTPException for error throwing (typed-errors cleanup deferred).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -50,17 +50,17 @@
|
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
53
|
-
"@opengeni/capabilities": "1.0
|
|
54
|
-
"@opengeni/codex": "1.0
|
|
55
|
-
"@opengeni/config": "1.0
|
|
56
|
-
"@opengeni/contracts": "1.0
|
|
57
|
-
"@opengeni/db": "1.0
|
|
58
|
-
"@opengeni/documents": "1.0
|
|
59
|
-
"@opengeni/events": "1.0
|
|
60
|
-
"@opengeni/network": "1.0
|
|
61
|
-
"@opengeni/observability": "1.0
|
|
62
|
-
"@opengeni/runtime": "1.0
|
|
63
|
-
"@opengeni/storage": "1.0
|
|
53
|
+
"@opengeni/capabilities": "1.2.0",
|
|
54
|
+
"@opengeni/codex": "1.2.0",
|
|
55
|
+
"@opengeni/config": "1.2.0",
|
|
56
|
+
"@opengeni/contracts": "1.2.0",
|
|
57
|
+
"@opengeni/db": "1.2.0",
|
|
58
|
+
"@opengeni/documents": "1.2.0",
|
|
59
|
+
"@opengeni/events": "1.2.0",
|
|
60
|
+
"@opengeni/network": "1.2.0",
|
|
61
|
+
"@opengeni/observability": "1.2.0",
|
|
62
|
+
"@opengeni/runtime": "1.2.0",
|
|
63
|
+
"@opengeni/storage": "1.2.0",
|
|
64
64
|
"hono": "^4.12.18",
|
|
65
65
|
"zod": "^4.2.1"
|
|
66
66
|
},
|
|
@@ -19,10 +19,8 @@ import {
|
|
|
19
19
|
isRetryableDatabaseTransportFailure,
|
|
20
20
|
nestedPostgresSqlState,
|
|
21
21
|
replayAppliedSessionFork,
|
|
22
|
-
sessionTenancyProductActivated,
|
|
23
22
|
SessionTenancyAccessError,
|
|
24
23
|
SessionTenancyInvalidRequestError,
|
|
25
|
-
SessionTenancyNotActivatedError,
|
|
26
24
|
transitionSessionVisibility,
|
|
27
25
|
type Database,
|
|
28
26
|
type ForkSessionContentResult,
|
|
@@ -101,9 +99,10 @@ export async function getManagedHumanSessionCreateCapabilities(
|
|
|
101
99
|
}
|
|
102
100
|
throw error;
|
|
103
101
|
}
|
|
104
|
-
// Mirror the database create fence exactly:
|
|
105
|
-
//
|
|
106
|
-
//
|
|
102
|
+
// Mirror the database create fence exactly: every organization is
|
|
103
|
+
// session-tenancy activated (migration 0611), so a member's own Personal
|
|
104
|
+
// workspace is always private-capable, while a shared organization workspace
|
|
105
|
+
// additionally needs the owner/admin Only-me product setting.
|
|
107
106
|
let activated = false;
|
|
108
107
|
try {
|
|
109
108
|
const policy = await getPrivateSessionCreatePolicy(deps.db, {
|
|
@@ -153,9 +152,6 @@ async function requireSessionTenancyMutationGate(
|
|
|
153
152
|
// missing, shared, or another owner's private session.
|
|
154
153
|
requireVerifiedOwningUser(authorization, workspaceId);
|
|
155
154
|
for (const permission of permissions) requirePermission(authorization.grant, permission);
|
|
156
|
-
if (!(await sessionTenancyProductActivated(deps.db, workspaceId))) {
|
|
157
|
-
throw new SessionTenancyNotActivatedError();
|
|
158
|
-
}
|
|
159
155
|
}
|
|
160
156
|
|
|
161
157
|
async function publishExactCommittedEvent(
|
|
@@ -8,8 +8,6 @@ import {
|
|
|
8
8
|
issueSelfUserResourceGrant,
|
|
9
9
|
listSelfUserResourceAuthorities,
|
|
10
10
|
revokeSelfUserResourceGrant,
|
|
11
|
-
sessionTenancyProductActivated,
|
|
12
|
-
SessionTenancyNotActivatedError,
|
|
13
11
|
} from "@opengeni/db";
|
|
14
12
|
import { requirePermission, type AccessGrantAuthorization } from "../access";
|
|
15
13
|
import type { AppDependencies } from "../dependencies";
|
|
@@ -46,9 +44,6 @@ async function requireOwnerProductGate(
|
|
|
46
44
|
// Target-free gates must run before an optional target-session lookup or host
|
|
47
45
|
// callback so rejected principals cannot use grant management as an oracle.
|
|
48
46
|
requireOwnerAuthority(authorization, workspaceId, permissions);
|
|
49
|
-
if (!(await sessionTenancyProductActivated(deps.db, workspaceId))) {
|
|
50
|
-
throw new SessionTenancyNotActivatedError();
|
|
51
|
-
}
|
|
52
47
|
}
|
|
53
48
|
|
|
54
49
|
function requireOwnerAuthority(
|
|
@@ -75,14 +70,6 @@ export async function listManagedHumanUserResourceAuthorities(
|
|
|
75
70
|
input: { resourceKind: UserResourceKind; cursor?: string | undefined; limit: number },
|
|
76
71
|
) {
|
|
77
72
|
requireOwnerAuthority(authorization, workspaceId, LIST_PERMISSIONS[input.resourceKind]);
|
|
78
|
-
// Before activation there cannot be usable personal-resource authority in
|
|
79
|
-
// this organization. Discovery therefore has an exact empty answer; only
|
|
80
|
-
// issue/revoke and runtime use remain behind the forward-only product gate.
|
|
81
|
-
// This keeps ordinary workspace-owned Rig/Variable Set selection independent
|
|
82
|
-
// from an optional personal-resource product that is not enabled.
|
|
83
|
-
if (!(await sessionTenancyProductActivated(deps.db, workspaceId))) {
|
|
84
|
-
return { authorities: [], nextCursor: null };
|
|
85
|
-
}
|
|
86
73
|
return await listSelfUserResourceAuthorities(deps.db, {
|
|
87
74
|
accountId: authorization.grant.accountId,
|
|
88
75
|
workspaceId,
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import {
|
|
2
|
+
canonicalizeConfiguredModelId,
|
|
3
|
+
configuredStaticUsageLimits,
|
|
4
|
+
type Settings,
|
|
5
|
+
} from "@opengeni/config";
|
|
6
|
+
import type { EntitlementsPort } from "@opengeni/contracts";
|
|
7
|
+
import { modelFundingForAdmission } from "./limits";
|
|
8
|
+
import {
|
|
9
|
+
checkWorkspaceAllowance,
|
|
10
|
+
getSpendableCreditBalance,
|
|
11
|
+
isCodexBilledTurn,
|
|
12
|
+
sumUsageQuantity,
|
|
13
|
+
type Database,
|
|
14
|
+
} from "@opengeni/db";
|
|
15
|
+
|
|
16
|
+
export type AgentRunAdmissionDenial =
|
|
17
|
+
| "insufficient_credits"
|
|
18
|
+
| "allowance_exhausted"
|
|
19
|
+
| "monthly_model_cost_limit"
|
|
20
|
+
| "monthly_agent_run_limit";
|
|
21
|
+
|
|
22
|
+
/** Read-only admission shared by service-authored runs and goal Resume. */
|
|
23
|
+
export async function agentRunAdmissionDenial(
|
|
24
|
+
services: { db: Database; settings: Settings; entitlements?: EntitlementsPort | null },
|
|
25
|
+
input: {
|
|
26
|
+
accountId: string;
|
|
27
|
+
workspaceId: string;
|
|
28
|
+
model: string;
|
|
29
|
+
requestedAgentRuns: number;
|
|
30
|
+
/** The accepted work's causal human, not the scheduler/service caller. */
|
|
31
|
+
initiatingHumanSubjectId?: string | null;
|
|
32
|
+
},
|
|
33
|
+
): Promise<AgentRunAdmissionDenial | null> {
|
|
34
|
+
const codexBilled = await isCodexBilledTurn({
|
|
35
|
+
db: services.db,
|
|
36
|
+
settings: services.settings,
|
|
37
|
+
workspaceId: input.workspaceId,
|
|
38
|
+
model: input.model,
|
|
39
|
+
});
|
|
40
|
+
const externallyBilled = modelFundingForAdmission(
|
|
41
|
+
services.settings,
|
|
42
|
+
input.model,
|
|
43
|
+
codexBilled,
|
|
44
|
+
).fundedWithoutCredits;
|
|
45
|
+
if (
|
|
46
|
+
!externallyBilled &&
|
|
47
|
+
(services.settings.billingMode === "stripe" || services.settings.usageLimitsMode === "managed")
|
|
48
|
+
) {
|
|
49
|
+
if (services.entitlements) {
|
|
50
|
+
const decision = await services.entitlements.admitRun({
|
|
51
|
+
accountId: input.accountId,
|
|
52
|
+
workspaceId: input.workspaceId,
|
|
53
|
+
action: "agent_run:create",
|
|
54
|
+
quantity: input.requestedAgentRuns,
|
|
55
|
+
});
|
|
56
|
+
if (!decision.allowed) return "insufficient_credits";
|
|
57
|
+
} else {
|
|
58
|
+
const balance = await getSpendableCreditBalance(
|
|
59
|
+
services.db,
|
|
60
|
+
input.accountId,
|
|
61
|
+
canonicalizeConfiguredModelId(services.settings, input.model),
|
|
62
|
+
);
|
|
63
|
+
if (balance.balanceMicros <= 0) return "insufficient_credits";
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (!externallyBilled) {
|
|
67
|
+
const refusal = await checkWorkspaceAllowance(services.db, {
|
|
68
|
+
accountId: input.accountId,
|
|
69
|
+
workspaceId: input.workspaceId,
|
|
70
|
+
subjectId: input.initiatingHumanSubjectId ?? null,
|
|
71
|
+
});
|
|
72
|
+
if (refusal) return refusal.code;
|
|
73
|
+
}
|
|
74
|
+
if (
|
|
75
|
+
services.settings.usageLimitsMode !== "static" &&
|
|
76
|
+
services.settings.usageLimitsMode !== "managed"
|
|
77
|
+
) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const limits = configuredStaticUsageLimits(services.settings);
|
|
81
|
+
if (!externallyBilled && limits.maxMonthlyCostMicrosPerAccount) {
|
|
82
|
+
const used = await sumUsageQuantity(services.db, {
|
|
83
|
+
accountId: input.accountId,
|
|
84
|
+
eventType: "model.cost",
|
|
85
|
+
since: startOfUtcMonth(),
|
|
86
|
+
});
|
|
87
|
+
if (used >= limits.maxMonthlyCostMicrosPerAccount) {
|
|
88
|
+
return "monthly_model_cost_limit";
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (limits.maxMonthlyAgentRunsPerWorkspace) {
|
|
92
|
+
const used = await sumUsageQuantity(services.db, {
|
|
93
|
+
workspaceId: input.workspaceId,
|
|
94
|
+
eventType: "agent_run.created",
|
|
95
|
+
since: startOfUtcMonth(),
|
|
96
|
+
});
|
|
97
|
+
if (used + input.requestedAgentRuns > limits.maxMonthlyAgentRunsPerWorkspace) {
|
|
98
|
+
return "monthly_agent_run_limit";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function startOfUtcMonth(): Date {
|
|
105
|
+
const now = new Date();
|
|
106
|
+
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
|
|
107
|
+
}
|
package/src/billing/limits.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
configuredStaticUsageLimits,
|
|
3
|
+
canonicalizeConfiguredModelId,
|
|
3
4
|
resolveModelProviderForTurn,
|
|
4
5
|
type Settings,
|
|
5
6
|
} from "@opengeni/config";
|
|
@@ -16,7 +17,7 @@ import {
|
|
|
16
17
|
countActiveOrganizationApiKeysForAccount,
|
|
17
18
|
countScheduledTasksForWorkspace,
|
|
18
19
|
countWorkspacesForAccount,
|
|
19
|
-
|
|
20
|
+
getSpendableCreditBalance,
|
|
20
21
|
isCodexBilledTurn,
|
|
21
22
|
recordUsageEvent,
|
|
22
23
|
sumUsageQuantity,
|
|
@@ -148,11 +149,21 @@ async function checkCreditBalance(
|
|
|
148
149
|
if (!usesCreditLimits(deps) || !isCostlyAction(input.action)) {
|
|
149
150
|
return { allowed: true };
|
|
150
151
|
}
|
|
151
|
-
const balance = await
|
|
152
|
+
const balance = await getSpendableCreditBalance(
|
|
153
|
+
deps.db,
|
|
154
|
+
input.accountId,
|
|
155
|
+
input.model ? canonicalizeConfiguredModelId(deps.settings, input.model) : undefined,
|
|
156
|
+
);
|
|
152
157
|
if (balance.balanceMicros > 0) {
|
|
153
158
|
return { allowed: true };
|
|
154
159
|
}
|
|
155
|
-
return {
|
|
160
|
+
return {
|
|
161
|
+
allowed: false,
|
|
162
|
+
code: "insufficient_credits",
|
|
163
|
+
message: input.model
|
|
164
|
+
? "No credits available for this model. Choose a model covered by your promotional credits or add credits."
|
|
165
|
+
: "No general credits available. Promotional credits cover eligible models only.",
|
|
166
|
+
};
|
|
156
167
|
}
|
|
157
168
|
|
|
158
169
|
async function checkStaticCaps(
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type WorkspaceSessionDefaults,
|
|
14
14
|
type XaiProviderAccountAuthoritySnapshotV1,
|
|
15
15
|
type ClaudeProviderAccountAuthoritySnapshotV1,
|
|
16
|
+
type BillingBalance,
|
|
16
17
|
} from "@opengeni/contracts";
|
|
17
18
|
import {
|
|
18
19
|
getScheduledTaskXaiProviderAccountAuthoritySnapshot,
|
|
@@ -30,7 +31,8 @@ import {
|
|
|
30
31
|
workspaceCodexSubscriptionActive,
|
|
31
32
|
workspaceOpenRouterConnectionActive,
|
|
32
33
|
workspaceVercelAiGatewayConnectionActive,
|
|
33
|
-
|
|
34
|
+
getBillingBalance,
|
|
35
|
+
spendableCreditMicros,
|
|
34
36
|
workspaceXaiSubscriptionActive,
|
|
35
37
|
workspaceXaiSubscriptionActiveForAuthority,
|
|
36
38
|
XaiAuthorityPoolInactiveError,
|
|
@@ -112,16 +114,22 @@ export type DefaultSessionModelInput = {
|
|
|
112
114
|
* `organizationHoldsCredits`).
|
|
113
115
|
*/
|
|
114
116
|
creditsAvailable: boolean;
|
|
117
|
+
creditBalance?: BillingBalance;
|
|
115
118
|
};
|
|
116
119
|
|
|
117
120
|
function creditsCandidate(
|
|
118
|
-
input: Pick<DefaultSessionModelInput, "settings" | "selections">,
|
|
121
|
+
input: Pick<DefaultSessionModelInput, "settings" | "selections" | "creditBalance">,
|
|
119
122
|
): DefaultModelSelection | null {
|
|
123
|
+
const selections = input.creditBalance
|
|
124
|
+
? input.selections.filter(
|
|
125
|
+
(selection) => spendableCreditMicros(input.creditBalance!, selection.model.id) > 0,
|
|
126
|
+
)
|
|
127
|
+
: input.selections;
|
|
120
128
|
const fallbackEffort = input.settings.openaiReasoningEffort;
|
|
121
|
-
const deployment = findSelection(
|
|
122
|
-
const configured = findSelection(
|
|
129
|
+
const deployment = findSelection(selections, input.settings.openaiModel);
|
|
130
|
+
const configured = findSelection(selections, input.settings.creditsDefaultModel);
|
|
123
131
|
if (deployment?.availability.selectable && deployment.model.cost === "credits") {
|
|
124
|
-
//
|
|
132
|
+
// A funded paid deployment default is preserved. When it is the
|
|
125
133
|
// credits default model itself, credit holders get the credits default
|
|
126
134
|
// effort rather than the deployment-wide fallback effort.
|
|
127
135
|
if (configured?.model.id !== deployment.model.id) return null;
|
|
@@ -146,7 +154,7 @@ function creditsCandidate(
|
|
|
146
154
|
source: "credits",
|
|
147
155
|
};
|
|
148
156
|
}
|
|
149
|
-
const first =
|
|
157
|
+
const first = selections.find(
|
|
150
158
|
(selection) => selection.availability.selectable && selection.model.cost === "credits",
|
|
151
159
|
);
|
|
152
160
|
return first
|
|
@@ -175,8 +183,8 @@ function creditsCandidate(
|
|
|
175
183
|
* configured credits default (`OPENGENI_CREDITS_DEFAULT_MODEL`, effort
|
|
176
184
|
* clamped to what the model supports), or the first selectable
|
|
177
185
|
* credits-billed model when that one is not selectable. When the
|
|
178
|
-
* deployment default is already a selectable credits-billed model it
|
|
179
|
-
*
|
|
186
|
+
* deployment default is already a funded, selectable credits-billed model it
|
|
187
|
+
* keeps the deployment effort, except that it takes the
|
|
180
188
|
* credits default effort when it is the credits default model itself.
|
|
181
189
|
* 4. `deployment`: the deployment default with the deployment reasoning effort
|
|
182
190
|
* when stably admissible; otherwise the first stably admissible catalog
|
|
@@ -247,6 +255,7 @@ export function creditsDefaultSessionModel(input: {
|
|
|
247
255
|
settings: Settings;
|
|
248
256
|
selections: readonly WorkspaceModelSelection[];
|
|
249
257
|
workspaceSettings: unknown;
|
|
258
|
+
creditBalance?: BillingBalance;
|
|
250
259
|
}): DefaultModelSelection | null {
|
|
251
260
|
if (input.settings.billingMode !== "stripe") return null;
|
|
252
261
|
return selectDefaultSessionModel({
|
|
@@ -254,6 +263,7 @@ export function creditsDefaultSessionModel(input: {
|
|
|
254
263
|
selections: input.selections,
|
|
255
264
|
workspaceDefaults: resolveWorkspaceSessionDefaults(input.workspaceSettings),
|
|
256
265
|
creditsAvailable: true,
|
|
266
|
+
...(input.creditBalance ? { creditBalance: input.creditBalance } : {}),
|
|
257
267
|
});
|
|
258
268
|
}
|
|
259
269
|
|
|
@@ -279,13 +289,14 @@ export async function resolveDefaultSessionModelForSelections(
|
|
|
279
289
|
if (
|
|
280
290
|
withoutCredits.source !== "deployment" ||
|
|
281
291
|
input.settings.billingMode !== "stripe" ||
|
|
282
|
-
|
|
292
|
+
!input.selections.some(
|
|
293
|
+
(selection) => selection.availability.selectable && selection.model.cost === "credits",
|
|
294
|
+
)
|
|
283
295
|
) {
|
|
284
296
|
return withoutCredits;
|
|
285
297
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
: withoutCredits;
|
|
298
|
+
const creditBalance = await getBillingBalance(db, input.accountId);
|
|
299
|
+
return selectDefaultSessionModel({ ...decision, creditsAvailable: true, creditBalance });
|
|
289
300
|
}
|
|
290
301
|
|
|
291
302
|
export type WorkspaceModelSelectionContext = {
|
package/src/dependencies.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
ConnectionCredentialsPort,
|
|
4
4
|
Document,
|
|
5
5
|
DocumentAuthorityKind,
|
|
6
|
+
EntitlementsPort,
|
|
6
7
|
GitHubAppApiPort,
|
|
7
8
|
ScheduledTask,
|
|
8
9
|
ScheduledTaskTriggerType,
|
|
@@ -155,6 +156,8 @@ export type AppDependencies = {
|
|
|
155
156
|
*/
|
|
156
157
|
catalogSourceSettings?: Settings;
|
|
157
158
|
db: Database;
|
|
159
|
+
/** Read-only host funding admission; unset uses the standalone billing ledger. */
|
|
160
|
+
entitlements?: EntitlementsPort | null;
|
|
158
161
|
/**
|
|
159
162
|
* Host-composed editable artifact engine. Standalone startup binds the same
|
|
160
163
|
* native kernel/DB/object-store implementation; embedded hosts may inject an
|
|
@@ -40,7 +40,7 @@ const usage = mock(
|
|
|
40
40
|
);
|
|
41
41
|
mock.module("@opengeni/db", () => ({
|
|
42
42
|
listKnowledgeEntries: list,
|
|
43
|
-
|
|
43
|
+
getSpendableCreditBalance: async () => ({ balanceMicros: balance }),
|
|
44
44
|
applyCreditDebitAfterUse: debits,
|
|
45
45
|
checkWorkspaceAllowance: allowanceChecks,
|
|
46
46
|
creditDebitAttributionForTurn: async (_db: unknown, input: { turnId: string }) => ({
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
applyCreditDebitAfterUse,
|
|
5
5
|
checkWorkspaceAllowance,
|
|
6
6
|
creditDebitAttributionForTurn,
|
|
7
|
-
|
|
7
|
+
getSpendableCreditBalance,
|
|
8
8
|
listKnowledgeEntries,
|
|
9
9
|
recordUsageEvent,
|
|
10
10
|
sumUsageQuantity,
|
|
@@ -190,7 +190,7 @@ export async function searchKnowledgeEntries(
|
|
|
190
190
|
context.accountId,
|
|
191
191
|
context.workspaceId,
|
|
192
192
|
async (lockedDb) => {
|
|
193
|
-
const balance = await
|
|
193
|
+
const balance = await getSpendableCreditBalance(lockedDb, context.accountId);
|
|
194
194
|
if (balance.balanceMicros <= 0)
|
|
195
195
|
return keywordFallback(lockedDb, new KnowledgeVectorFundingError(), "awaiting_funding");
|
|
196
196
|
const attribution =
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import {
|
|
9
9
|
AGENT_SKILL_MANAGE_TOOL_NAMES,
|
|
10
10
|
AUTOMATIC_SESSION_TITLE_FALLBACK,
|
|
11
|
+
bundledSkillSelectionForAgentConfig,
|
|
11
12
|
DEFAULT_FIRST_PARTY_MCP_PERMISSIONS,
|
|
12
13
|
FIRST_PARTY_IN_PROCESS_TOOL_NAMES,
|
|
13
14
|
type FirstPartyMcpToolName,
|
|
@@ -395,14 +396,19 @@ export async function workspaceSessionEffectiveToolsContext(
|
|
|
395
396
|
|
|
396
397
|
function sessionHasSkills(session: Session, context: SessionEffectiveToolsContext): boolean {
|
|
397
398
|
if (context.hasWorkspaceSkills || session.skills.length > 0) return true;
|
|
398
|
-
// Undefined is the worker's bundled default. Explicit [] means no bundles
|
|
399
|
-
|
|
399
|
+
// Undefined is the worker's bundled default. Explicit [] means no bundles,
|
|
400
|
+
// and a "none" agent without an explicit list has none (as in the worker).
|
|
401
|
+
const bundledSkillIds = bundledSkillSelectionForAgentConfig(
|
|
402
|
+
session.bundledSkillIds,
|
|
403
|
+
session.agent,
|
|
404
|
+
);
|
|
405
|
+
if (bundledSkillIds === undefined) return true;
|
|
400
406
|
const tools = new Set(
|
|
401
407
|
resolveAgentToolFamilies(session.agent).firstPartyTools(
|
|
402
408
|
allowedFirstPartyMcpToolsForSession(context.settings, session.firstPartyMcpTools),
|
|
403
409
|
),
|
|
404
410
|
);
|
|
405
|
-
return
|
|
411
|
+
return bundledSkillIds.some((id) => {
|
|
406
412
|
if (
|
|
407
413
|
[
|
|
408
414
|
"builtin:opengeni-documents",
|
|
@@ -479,7 +485,7 @@ export function sessionEffectiveToolProjectionInput(
|
|
|
479
485
|
(!session.title?.trim() || session.title.trim() === AUTOMATIC_SESSION_TITLE_FALLBACK);
|
|
480
486
|
const firstPartyMcpTools =
|
|
481
487
|
context && toolRefs.some((ref) => ref.id === "opengeni")
|
|
482
|
-
? resolveAgentToolFamilies(session.agent)
|
|
488
|
+
? resolveAgentToolFamilies(session.agent, { sandboxAttached: sandboxAvailable })
|
|
483
489
|
.firstPartyTools(
|
|
484
490
|
allowedFirstPartyMcpToolsForSession(context.settings, session.firstPartyMcpTools),
|
|
485
491
|
)
|
|
@@ -512,7 +518,9 @@ export function sessionEffectiveToolProjectionInput(
|
|
|
512
518
|
firstPartyModelNames.set(name, `interaction__${name}`);
|
|
513
519
|
if (
|
|
514
520
|
!firstPartyMcpTools.includes(name) &&
|
|
515
|
-
resolveAgentToolFamilies(session.agent
|
|
521
|
+
resolveAgentToolFamilies(session.agent, {
|
|
522
|
+
sandboxAttached: sandboxAvailable,
|
|
523
|
+
}).allowsFirstPartyTool(name) &&
|
|
516
524
|
session.firstPartyMcpTools.includes(name)
|
|
517
525
|
)
|
|
518
526
|
firstPartyMcpTools.push(name);
|
|
@@ -561,6 +569,7 @@ export function sessionEffectiveToolProjectionInput(
|
|
|
561
569
|
routerInHistory:
|
|
562
570
|
context?.routerInHistory === true ||
|
|
563
571
|
context?.routerHistorySessionIds?.has(session.id) === true,
|
|
572
|
+
...(context ? { sandboxAttached: sandboxAvailable } : {}),
|
|
564
573
|
};
|
|
565
574
|
const families = resolveAgentToolFamilies(session.agent, environment);
|
|
566
575
|
environment.hasDeferredTools =
|
package/src/domain/sessions.ts
CHANGED
|
@@ -72,6 +72,7 @@ import {
|
|
|
72
72
|
OPENGENI_SLACK_BOT_SESSION_METADATA_KEY,
|
|
73
73
|
SessionSkills,
|
|
74
74
|
resolveBundledSkillSelection,
|
|
75
|
+
bundledSkillSelectionForAgentConfig,
|
|
75
76
|
SessionSpawnDenial,
|
|
76
77
|
ServiceTurnInitiator,
|
|
77
78
|
ServiceTurnInitiatorContext,
|
|
@@ -182,7 +183,6 @@ import {
|
|
|
182
183
|
SessionToolPolicyVersionConflictError,
|
|
183
184
|
SessionCreateIdempotencyConflictError,
|
|
184
185
|
PersonalResourceAttachmentAcceptanceError,
|
|
185
|
-
sessionTenancyProductActivated,
|
|
186
186
|
workspaceControlRequestLockTimeoutMs,
|
|
187
187
|
WorkspaceControlBusyError,
|
|
188
188
|
type SessionCommandActor,
|
|
@@ -327,11 +327,6 @@ async function requireAtomicPersonalResourceAttachment(
|
|
|
327
327
|
cause: error,
|
|
328
328
|
});
|
|
329
329
|
}
|
|
330
|
-
if (!(await sessionTenancyProductActivated(deps.db, workspaceId))) {
|
|
331
|
-
throw new HTTPException(409, {
|
|
332
|
-
message: "Session tenancy is not activated for this organization.",
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
330
|
if (existingSession && intent.expectedAuthorityEpoch === undefined) {
|
|
336
331
|
throw new HTTPException(422, {
|
|
337
332
|
message: "Personal-resource attachment requires expectedAuthorityEpoch.",
|
|
@@ -1051,6 +1046,13 @@ export async function createAndStartSessionWithOutcome(input: {
|
|
|
1051
1046
|
allowNestedAgentDepthIncrease?: boolean;
|
|
1052
1047
|
subjectId?: string | null;
|
|
1053
1048
|
}): Promise<CreateSessionOutcome> {
|
|
1049
|
+
const frozenBundledSkillIds = bundledSkillSelectionForAgentConfig(
|
|
1050
|
+
input.bundledSkillIds,
|
|
1051
|
+
input.agentConfig,
|
|
1052
|
+
);
|
|
1053
|
+
if (frozenBundledSkillIds !== input.bundledSkillIds) {
|
|
1054
|
+
input = { ...input, bundledSkillIds: frozenBundledSkillIds };
|
|
1055
|
+
}
|
|
1054
1056
|
const sessionMetadata = metadataWithTurnExecutionPolicyV1(
|
|
1055
1057
|
{
|
|
1056
1058
|
...input.metadata,
|
|
@@ -2545,6 +2547,9 @@ async function createSessionForRequestInFileScope(
|
|
|
2545
2547
|
if (agentResolution.instructions !== undefined) {
|
|
2546
2548
|
payload.instructions = agentResolution.instructions;
|
|
2547
2549
|
}
|
|
2550
|
+
// Freeze what the agent configuration implies before keyed replay compares
|
|
2551
|
+
// it: a `"none"` agent omits the bundled guides unless the request lists them.
|
|
2552
|
+
bundledSkillIds = bundledSkillSelectionForAgentConfig(bundledSkillIds, agentConfig);
|
|
2548
2553
|
const parentCallingTurn =
|
|
2549
2554
|
parentSession && creationInitiator.actor
|
|
2550
2555
|
? await getSessionTurnForAttempt(
|