@opengeni/worker-bundle 2.2.0 → 2.2.1-canary.36919050519001
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/activities/agent-run-admission.d.ts +3 -1
- package/dist/activities/agent-turn/admission.d.ts +24 -3
- package/dist/activities/agent-turn/agent-build.d.ts +1 -0
- package/dist/activities/agent-turn/claude-usage-observer.d.ts +13 -1
- package/dist/activities/agent-turn/compaction-prep.d.ts +20 -0
- package/dist/activities/agent-turn/final-reply.d.ts +18 -0
- package/dist/activities/agent-turn/governance-model.d.ts +6 -0
- package/dist/activities/agent-turn/model-call-admission.d.ts +15 -0
- package/dist/activities/agent-turn/run-credentials.d.ts +2 -0
- package/dist/activities/agent-turn/sandbox-establish.d.ts +1 -0
- package/dist/activities/agent-turn/session-title.d.ts +3 -1
- package/dist/activities/agent-turn/stream-attempt.d.ts +1 -0
- package/dist/activities/agent-turn/turn-context.d.ts +1 -0
- package/dist/activities/automations.d.ts +3 -1
- package/dist/activities/browser-deadline-checkpoint.d.ts +29 -0
- package/dist/activities/goals.d.ts +8 -0
- package/dist/activities/run-credentials.d.ts +4 -2
- package/dist/activities/types.d.ts +1 -1
- package/dist/{activities-control-MHNA3CYV.js → activities-control-55ECL47G.js} +403 -116
- package/dist/activities-control-55ECL47G.js.map +1 -0
- package/dist/activities-control.d.ts +30 -0
- package/dist/{activities-turn-FQ5OO2WW.js → activities-turn-T7Y3BWYO.js} +678 -246
- package/dist/activities-turn-T7Y3BWYO.js.map +1 -0
- package/dist/activities.d.ts +30 -0
- package/dist/browser-deadline-checkpoint-contract.d.ts +2 -0
- package/dist/{chunk-RZAAGRFQ.js → chunk-OWNLATJL.js} +189 -80
- package/dist/chunk-OWNLATJL.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +45 -23
- package/dist/index.js.map +1 -1
- package/dist/sandbox-resume.d.ts +4 -1
- package/dist/workflow-bundle.js +72 -10
- package/dist/workflows/browser-deadline-checkpoint.d.ts +9 -0
- package/dist/workflows.d.ts +1 -0
- package/package.json +21 -20
- package/src/activities/agent-run-admission.ts +17 -1
- package/src/activities/agent-turn/admission.ts +64 -1
- package/src/activities/agent-turn/agent-build.ts +24 -3
- package/src/activities/agent-turn/claim.ts +56 -14
- package/src/activities/agent-turn/claude-usage-observer.ts +33 -1
- package/src/activities/agent-turn/compaction-prep.ts +67 -16
- package/src/activities/agent-turn/errors.ts +55 -0
- package/src/activities/agent-turn/failure-settlement.ts +26 -6
- package/src/activities/agent-turn/final-reply.ts +46 -0
- package/src/activities/agent-turn/governance-model.ts +12 -0
- package/src/activities/agent-turn/model-call-admission.ts +94 -0
- package/src/activities/agent-turn/run-credentials.ts +15 -3
- package/src/activities/agent-turn/run.ts +43 -5
- package/src/activities/agent-turn/sandbox-establish.ts +9 -5
- package/src/activities/agent-turn/session-title.ts +8 -2
- package/src/activities/agent-turn/stream-attempt.ts +144 -38
- package/src/activities/agent-turn/tool-environment.ts +51 -39
- package/src/activities/agent-turn/turn-context.ts +1 -0
- package/src/activities/automations.ts +55 -3
- package/src/activities/browser-deadline-checkpoint.ts +279 -0
- package/src/activities/capabilities.ts +52 -14
- package/src/activities/goals.ts +67 -95
- package/src/activities/knowledge-indexing.ts +29 -0
- package/src/activities/open-suffix-resume.ts +2 -1
- package/src/activities/run-credentials.ts +22 -16
- package/src/activities/scheduled-tasks.ts +83 -13
- package/src/activities/types.ts +1 -0
- package/src/activities/workspace-credential-provider.ts +12 -0
- package/src/activities-control.ts +2 -0
- package/src/activities.ts +3 -0
- package/src/browser-deadline-checkpoint-contract.ts +5 -0
- package/src/index.ts +53 -25
- package/src/sandbox-resume.ts +23 -8
- package/src/sandbox-routing.ts +4 -2
- package/src/workflows/browser-deadline-checkpoint.ts +50 -0
- package/src/workflows.ts +4 -0
- package/dist/activities-control-MHNA3CYV.js.map +0 -1
- package/dist/activities-turn-FQ5OO2WW.js.map +0 -1
- package/dist/chunk-RZAAGRFQ.js.map +0 -1
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { Context } from "@temporalio/activity";
|
|
2
|
+
import {
|
|
3
|
+
environmentsEncryptionKeyBytes,
|
|
4
|
+
resolveFirstPartyDelegationSecret,
|
|
5
|
+
type Settings,
|
|
6
|
+
} from "@opengeni/config";
|
|
7
|
+
import { BROWSER_STATE_ARTIFACT_CONTENT_TYPE } from "@opengeni/contracts";
|
|
8
|
+
import {
|
|
9
|
+
browserDeadlineCheckpoint,
|
|
10
|
+
clearSuspendedBrowserSessionController,
|
|
11
|
+
commitBrowserSessionSuspension,
|
|
12
|
+
dispatchBrowserSessionOperation,
|
|
13
|
+
failBrowserSessionSuspension,
|
|
14
|
+
listBrowserDeadlineCheckpoints,
|
|
15
|
+
readLease,
|
|
16
|
+
releaseLeaseHolder,
|
|
17
|
+
withRlsContext,
|
|
18
|
+
type BrowserDeadlineCheckpointTarget,
|
|
19
|
+
} from "@opengeni/db";
|
|
20
|
+
import { deriveBrowserControllerAdminToken } from "@opengeni/interaction/browser-controller-authority";
|
|
21
|
+
import {
|
|
22
|
+
browserStateArtifactAad,
|
|
23
|
+
browserStateManifestDigest,
|
|
24
|
+
browserStateObjectKey,
|
|
25
|
+
deriveBrowserStateDataKey,
|
|
26
|
+
wrapBrowserStateDataKey,
|
|
27
|
+
} from "@opengeni/interaction/browser-state-authority";
|
|
28
|
+
import {
|
|
29
|
+
BrowserControlClient,
|
|
30
|
+
BrowserControlRequestError,
|
|
31
|
+
createSandboxClientForBackend,
|
|
32
|
+
deserializeSandboxSessionStateEnvelope,
|
|
33
|
+
resumeExactSandboxSession,
|
|
34
|
+
type BrowserControlPlacementSession,
|
|
35
|
+
} from "@opengeni/runtime";
|
|
36
|
+
import type { ControlActivityServices } from "./types";
|
|
37
|
+
|
|
38
|
+
const UPLOAD_CLEANUP_GRACE_MS = 24 * 60 * 60_000;
|
|
39
|
+
|
|
40
|
+
function heartbeat(): void {
|
|
41
|
+
try {
|
|
42
|
+
Context.current().heartbeat();
|
|
43
|
+
} catch {
|
|
44
|
+
// Direct embedded calls have no Temporal context. In-flight controller
|
|
45
|
+
// operations still settle under their exact durable database claim.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function withActivityHeartbeat<T>(run: () => Promise<T>): Promise<T> {
|
|
50
|
+
heartbeat();
|
|
51
|
+
const timer = setInterval(heartbeat, 5_000);
|
|
52
|
+
try {
|
|
53
|
+
return await run();
|
|
54
|
+
} finally {
|
|
55
|
+
clearInterval(timer);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type CheckpointController = Pick<BrowserControlClient, "captureState" | "endSession">;
|
|
60
|
+
type CheckpointPlacement = {
|
|
61
|
+
target: BrowserDeadlineCheckpointTarget;
|
|
62
|
+
lease: NonNullable<Awaited<ReturnType<typeof readLease>>>;
|
|
63
|
+
settings: Settings;
|
|
64
|
+
controllerRoot: string;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
async function resumeCheckpointController({
|
|
68
|
+
target,
|
|
69
|
+
lease,
|
|
70
|
+
settings,
|
|
71
|
+
controllerRoot,
|
|
72
|
+
}: CheckpointPlacement): Promise<CheckpointController> {
|
|
73
|
+
// This edge cannot create a box or route through a user's current machine.
|
|
74
|
+
const provider = createSandboxClientForBackend("modal", settings) as Parameters<
|
|
75
|
+
typeof deserializeSandboxSessionStateEnvelope
|
|
76
|
+
>[0];
|
|
77
|
+
const envelope = lease.resumeState as { sessionState?: unknown };
|
|
78
|
+
const state = await deserializeSandboxSessionStateEnvelope(
|
|
79
|
+
provider,
|
|
80
|
+
envelope.sessionState ?? lease.resumeState,
|
|
81
|
+
target.instanceId,
|
|
82
|
+
);
|
|
83
|
+
const resumed = await resumeExactSandboxSession(provider, "modal", state, target.instanceId);
|
|
84
|
+
return new BrowserControlClient(resumed.session as BrowserControlPlacementSession, {
|
|
85
|
+
adminToken: deriveBrowserControllerAdminToken({
|
|
86
|
+
rootSecret: controllerRoot,
|
|
87
|
+
accountId: target.accountId,
|
|
88
|
+
workspaceId: target.workspaceId,
|
|
89
|
+
placement: { kind: "sandbox_group", sandboxGroupId: target.sandboxGroupId },
|
|
90
|
+
placementInstanceId: target.instanceId,
|
|
91
|
+
}),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function createBrowserDeadlineCheckpointActivities(
|
|
96
|
+
services: () => Promise<ControlActivityServices>,
|
|
97
|
+
connectController: (
|
|
98
|
+
placement: CheckpointPlacement,
|
|
99
|
+
) => Promise<CheckpointController> = resumeCheckpointController,
|
|
100
|
+
) {
|
|
101
|
+
return {
|
|
102
|
+
async listDueBrowserCheckpoints() {
|
|
103
|
+
return await withActivityHeartbeat(async () => {
|
|
104
|
+
const { db, settings, objectStorage } = await services();
|
|
105
|
+
if (
|
|
106
|
+
!settings.sandboxOwnershipEnabled ||
|
|
107
|
+
!objectStorage ||
|
|
108
|
+
!resolveFirstPartyDelegationSecret(settings)
|
|
109
|
+
)
|
|
110
|
+
return [];
|
|
111
|
+
const root = environmentsEncryptionKeyBytes(settings);
|
|
112
|
+
if (!root) return [];
|
|
113
|
+
root.fill(0);
|
|
114
|
+
return await listBrowserDeadlineCheckpoints(db);
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
async checkpointBrowserBeforeDeadline(target: BrowserDeadlineCheckpointTarget) {
|
|
118
|
+
return await withActivityHeartbeat(async () => {
|
|
119
|
+
const { db, settings, objectStorage } = await services();
|
|
120
|
+
const rootKey = environmentsEncryptionKeyBytes(settings);
|
|
121
|
+
const controllerRoot = resolveFirstPartyDelegationSecret(settings);
|
|
122
|
+
if (!rootKey || !objectStorage || !controllerRoot) {
|
|
123
|
+
rootKey?.fill(0);
|
|
124
|
+
throw new Error("browser deadline checkpoint storage or encryption is unavailable");
|
|
125
|
+
}
|
|
126
|
+
let dataKey: Buffer | null = null;
|
|
127
|
+
let aad: Buffer | null = null;
|
|
128
|
+
let timer: ReturnType<typeof setInterval> | null = null;
|
|
129
|
+
try {
|
|
130
|
+
const claim = await browserDeadlineCheckpoint(db, target, { prepare: true, touch: true });
|
|
131
|
+
if (!claim) return { status: "skipped" as const };
|
|
132
|
+
let pulseRunning = false;
|
|
133
|
+
let authorityLost = false;
|
|
134
|
+
const pulse = async () => {
|
|
135
|
+
if (pulseRunning) return;
|
|
136
|
+
pulseRunning = true;
|
|
137
|
+
try {
|
|
138
|
+
if (!(await browserDeadlineCheckpoint(db, target, { touch: true })))
|
|
139
|
+
authorityLost = true;
|
|
140
|
+
} finally {
|
|
141
|
+
pulseRunning = false;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
timer = setInterval(() => {
|
|
145
|
+
void pulse().catch(() => undefined);
|
|
146
|
+
}, 5_000);
|
|
147
|
+
const lease = await readLease(db, target.workspaceId, target.sandboxGroupId);
|
|
148
|
+
if (
|
|
149
|
+
!lease ||
|
|
150
|
+
lease.instanceId !== target.instanceId ||
|
|
151
|
+
lease.leaseEpoch !== target.leaseEpoch ||
|
|
152
|
+
!lease.resumeState
|
|
153
|
+
) {
|
|
154
|
+
throw new Error("browser deadline checkpoint placement changed");
|
|
155
|
+
}
|
|
156
|
+
const placement = {
|
|
157
|
+
kind: "sandbox_group" as const,
|
|
158
|
+
sandboxGroupId: target.sandboxGroupId,
|
|
159
|
+
};
|
|
160
|
+
const client = await connectController({ target, lease, settings, controllerRoot });
|
|
161
|
+
if (authorityLost || !(await browserDeadlineCheckpoint(db, target, { touch: true }))) {
|
|
162
|
+
throw new Error("browser deadline checkpoint authority changed");
|
|
163
|
+
}
|
|
164
|
+
if (claim.state !== "completed") {
|
|
165
|
+
const scope = { ...target, operationId: claim.operationId };
|
|
166
|
+
const objectKey = browserStateObjectKey(target.workspaceId, claim.operationId);
|
|
167
|
+
dataKey = deriveBrowserStateDataKey(rootKey, { ...scope, objectKey });
|
|
168
|
+
aad = browserStateArtifactAad({ ...scope, objectKey });
|
|
169
|
+
await dispatchBrowserSessionOperation(db, {
|
|
170
|
+
...scope,
|
|
171
|
+
deadlineTarget: target,
|
|
172
|
+
stateUpload: {
|
|
173
|
+
objectKey,
|
|
174
|
+
cleanupAfter: new Date(Date.now() + UPLOAD_CLEANUP_GRACE_MS),
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
const signed = await objectStorage.createPutUrl({
|
|
178
|
+
key: objectKey,
|
|
179
|
+
contentType: BROWSER_STATE_ARTIFACT_CONTENT_TYPE,
|
|
180
|
+
audience: "sandbox",
|
|
181
|
+
});
|
|
182
|
+
const receipt = await client
|
|
183
|
+
.captureState({
|
|
184
|
+
...scope,
|
|
185
|
+
objectKey,
|
|
186
|
+
afterCapture: "stop",
|
|
187
|
+
dataKey,
|
|
188
|
+
aad,
|
|
189
|
+
upload: {
|
|
190
|
+
url: signed.url,
|
|
191
|
+
requiredHeaders: signed.requiredHeaders,
|
|
192
|
+
expiresAt: signed.expiresAt.toISOString(),
|
|
193
|
+
},
|
|
194
|
+
})
|
|
195
|
+
.catch(async (error: unknown) => {
|
|
196
|
+
if (
|
|
197
|
+
error instanceof BrowserControlRequestError &&
|
|
198
|
+
(error.error.code === "outcome_unknown" || !error.error.retryable)
|
|
199
|
+
) {
|
|
200
|
+
await failBrowserSessionSuspension(db, { ...scope, error: error.error });
|
|
201
|
+
}
|
|
202
|
+
throw error;
|
|
203
|
+
});
|
|
204
|
+
if (authorityLost) throw new Error("browser deadline checkpoint authority changed");
|
|
205
|
+
const portable = receipt.manifest.profileCrypto !== "platform_bound";
|
|
206
|
+
await commitBrowserSessionSuspension(db, {
|
|
207
|
+
...scope,
|
|
208
|
+
deadlineTarget: target,
|
|
209
|
+
artifact: {
|
|
210
|
+
kind: "chromium_profile",
|
|
211
|
+
format: receipt.format,
|
|
212
|
+
objectKey,
|
|
213
|
+
artifactDigest: receipt.artifactDigest,
|
|
214
|
+
contentDigest: receipt.contentDigest,
|
|
215
|
+
manifestDigest: browserStateManifestDigest(receipt.manifest),
|
|
216
|
+
sizeBytes: receipt.sizeBytes,
|
|
217
|
+
encryptedDataKey: wrapBrowserStateDataKey(rootKey, dataKey, {
|
|
218
|
+
...scope,
|
|
219
|
+
objectKey,
|
|
220
|
+
artifactDigest: receipt.artifactDigest,
|
|
221
|
+
contentDigest: receipt.contentDigest,
|
|
222
|
+
}),
|
|
223
|
+
materialization: {
|
|
224
|
+
portability: portable ? "portable" : "placement_bound",
|
|
225
|
+
reason: portable
|
|
226
|
+
? null
|
|
227
|
+
: "Profile encryption depends on the source operating-system credential store.",
|
|
228
|
+
platform: receipt.manifest.platform,
|
|
229
|
+
architecture: receipt.manifest.architecture,
|
|
230
|
+
engine: receipt.manifest.engine,
|
|
231
|
+
engineVersion: receipt.manifest.engineVersion,
|
|
232
|
+
driverId: receipt.manifest.driverId,
|
|
233
|
+
driverSchemaVersion: receipt.manifest.driverSchemaVersion,
|
|
234
|
+
profileCrypto: receipt.manifest.profileCrypto,
|
|
235
|
+
providerId: null,
|
|
236
|
+
placement: portable ? null : placement,
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
// Only remove the local profile after the encrypted artifact and its
|
|
242
|
+
// authority committed. A crash resumes this cleanup, never capture.
|
|
243
|
+
await client.endSession(target, { removeState: true }).catch((error: unknown) => {
|
|
244
|
+
if (!(error instanceof BrowserControlRequestError && error.status === 404)) throw error;
|
|
245
|
+
});
|
|
246
|
+
await withRlsContext(
|
|
247
|
+
db,
|
|
248
|
+
target,
|
|
249
|
+
async (tx) => {
|
|
250
|
+
if ((await browserDeadlineCheckpoint(tx, target))?.state !== "completed") {
|
|
251
|
+
throw new Error("browser deadline checkpoint cleanup authority changed");
|
|
252
|
+
}
|
|
253
|
+
await clearSuspendedBrowserSessionController(tx, {
|
|
254
|
+
...target,
|
|
255
|
+
expectedControllerGeneration: target.controllerGeneration,
|
|
256
|
+
});
|
|
257
|
+
// One transaction prevents a human resume from acquiring a new
|
|
258
|
+
// same-browser holder between clearing and releasing the old one.
|
|
259
|
+
await releaseLeaseHolder(tx, {
|
|
260
|
+
...target,
|
|
261
|
+
kind: "interaction",
|
|
262
|
+
holderId: `browser-session:${target.browserSessionId}`,
|
|
263
|
+
idleGraceMs: settings.sandboxIdleGraceMs,
|
|
264
|
+
});
|
|
265
|
+
},
|
|
266
|
+
undefined,
|
|
267
|
+
"none",
|
|
268
|
+
);
|
|
269
|
+
return { status: "suspended" as const };
|
|
270
|
+
} finally {
|
|
271
|
+
if (timer) clearInterval(timer);
|
|
272
|
+
dataKey?.fill(0);
|
|
273
|
+
aad?.fill(0);
|
|
274
|
+
rootKey.fill(0);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
}
|
|
@@ -40,6 +40,8 @@ import {
|
|
|
40
40
|
listWorkspaceProviderCustomModels,
|
|
41
41
|
getWorkspaceProviderCustomModelForExecution,
|
|
42
42
|
loadWorkspaceProviderApiKey,
|
|
43
|
+
loadClaudeSubscriptionUsageCredential,
|
|
44
|
+
assertModelConnectionAllowsTurn,
|
|
43
45
|
type Database,
|
|
44
46
|
type SessionMcpServerForRun,
|
|
45
47
|
} from "@opengeni/db";
|
|
@@ -303,12 +305,30 @@ export async function settingsWithOrganizationProviderCredentials(
|
|
|
303
305
|
if (kind === "claude_subscription" && !settings.claudeSubscriptionEnabled) continue;
|
|
304
306
|
const models = await buildModels(kind, claudeProviderId(kind) + "/");
|
|
305
307
|
result = withClaudeConnectionCatalog(result, { [kind]: { models } });
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
308
|
+
const binding =
|
|
309
|
+
kind === "claude_subscription"
|
|
310
|
+
? await loadClaudeSubscriptionUsageCredential(db, settings, {
|
|
311
|
+
accountId,
|
|
312
|
+
workspaceId,
|
|
313
|
+
scope: "organization",
|
|
314
|
+
})
|
|
315
|
+
: null;
|
|
316
|
+
const credential =
|
|
317
|
+
kind === "claude_subscription"
|
|
318
|
+
? binding?.serializedCredential
|
|
319
|
+
: await loadOrganizationModelProviderApiKey(db, settings, {
|
|
320
|
+
accountId,
|
|
321
|
+
workspaceId,
|
|
322
|
+
providerKind: kind,
|
|
323
|
+
});
|
|
324
|
+
if (credential)
|
|
325
|
+
result = withClaudeConnectionCredential(
|
|
326
|
+
result,
|
|
327
|
+
kind,
|
|
328
|
+
credential,
|
|
329
|
+
"organization",
|
|
330
|
+
binding ?? undefined,
|
|
331
|
+
);
|
|
312
332
|
const workspaceModels = await listWorkspaceProviderCustomModels(db, {
|
|
313
333
|
accountId,
|
|
314
334
|
workspaceId,
|
|
@@ -333,15 +353,33 @@ export async function settingsWithOrganizationProviderCredentials(
|
|
|
333
353
|
{ [kind]: { models: workspaceModels } },
|
|
334
354
|
"workspace",
|
|
335
355
|
);
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
356
|
+
const workspaceBinding =
|
|
357
|
+
kind === "claude_subscription"
|
|
358
|
+
? await loadClaudeSubscriptionUsageCredential(db, settings, {
|
|
359
|
+
accountId,
|
|
360
|
+
workspaceId,
|
|
361
|
+
scope: "workspace",
|
|
362
|
+
})
|
|
363
|
+
: null;
|
|
364
|
+
if (workspaceBinding && workspaceModelId)
|
|
365
|
+
await assertModelConnectionAllowsTurn(db, {
|
|
366
|
+
workspaceId,
|
|
367
|
+
subjectId: "worker:model-access",
|
|
368
|
+
modelId: workspaceModelId,
|
|
369
|
+
workspaceProviderConnectionId: workspaceBinding.connectionId,
|
|
370
|
+
});
|
|
371
|
+
const workspaceCredential =
|
|
372
|
+
kind === "claude_subscription"
|
|
373
|
+
? workspaceBinding?.serializedCredential
|
|
374
|
+
: await loadWorkspaceProviderApiKey(db, settings, workspaceId, kind, workspaceModelId);
|
|
343
375
|
if (workspaceCredential)
|
|
344
|
-
result = withClaudeConnectionCredential(
|
|
376
|
+
result = withClaudeConnectionCredential(
|
|
377
|
+
result,
|
|
378
|
+
kind,
|
|
379
|
+
workspaceCredential,
|
|
380
|
+
"workspace",
|
|
381
|
+
workspaceBinding ?? undefined,
|
|
382
|
+
);
|
|
345
383
|
}
|
|
346
384
|
return result;
|
|
347
385
|
}
|
package/src/activities/goals.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
allowedFirstPartyMcpToolsForSession,
|
|
3
3
|
configuredStaticUsageLimits,
|
|
4
|
+
isModelAvailableForNewSelection,
|
|
4
5
|
policyProviderIdForModel,
|
|
5
6
|
resolveModelProvider,
|
|
6
7
|
resolveTurnExecutionPolicyV1,
|
|
@@ -19,14 +20,10 @@ import {
|
|
|
19
20
|
import { isCodexBilledModel } from "@opengeni/codex";
|
|
20
21
|
import {
|
|
21
22
|
enqueueSessionWorkflowWakeIfRunnable,
|
|
22
|
-
getBillingBalance,
|
|
23
23
|
getWorkspaceModelPolicy,
|
|
24
24
|
getSessionGoal,
|
|
25
|
-
isCodexBilledTurn,
|
|
26
25
|
materializeGoalContinuation,
|
|
27
26
|
requireSession,
|
|
28
|
-
sumUsageQuantity,
|
|
29
|
-
type Database,
|
|
30
27
|
} from "@opengeni/db";
|
|
31
28
|
import type {
|
|
32
29
|
ControlActivityServices,
|
|
@@ -38,6 +35,7 @@ import {
|
|
|
38
35
|
resolveCatalogSettings,
|
|
39
36
|
resolveWorkspaceCatalogSettings,
|
|
40
37
|
} from "@opengeni/core";
|
|
38
|
+
import { agentRunAdmissionDenial } from "./agent-run-admission";
|
|
41
39
|
|
|
42
40
|
export function createGoalActivities(services: () => Promise<ControlActivityServices>) {
|
|
43
41
|
async function enqueueGoalRetryWake(input: MaybeContinueGoalInput): Promise<void> {
|
|
@@ -65,7 +63,6 @@ export function createGoalActivities(services: () => Promise<ControlActivityServ
|
|
|
65
63
|
if (!existingGoal || existingGoal.status !== "active") {
|
|
66
64
|
return { action: "none" };
|
|
67
65
|
}
|
|
68
|
-
let settings = (await resolveCatalogSettings(db, catalogSourceSettings)).settings;
|
|
69
66
|
// Loaded before the budget check so the codex-billed predicate and the
|
|
70
67
|
// synthesized turn use the SAME effective policy. An explicit per-turn
|
|
71
68
|
// model can differ from the persisted session default; follow-up goal work
|
|
@@ -74,6 +71,13 @@ export function createGoalActivities(services: () => Promise<ControlActivityServ
|
|
|
74
71
|
// Kept below the goal-less fast path so a non-goal session still skips the
|
|
75
72
|
// reads entirely.
|
|
76
73
|
const session = await requireSession(db, input.workspaceId, input.sessionId);
|
|
74
|
+
// Terminal sessions retain their goal for human recovery, but cannot
|
|
75
|
+
// continue. Do not validate an obsolete model before the locked guard gets
|
|
76
|
+
// the chance to reject that work; otherwise a deterministic error retries.
|
|
77
|
+
if (session.status === "failed" || session.status === "cancelled") {
|
|
78
|
+
return { action: "none" };
|
|
79
|
+
}
|
|
80
|
+
let settings = (await resolveCatalogSettings(db, catalogSourceSettings)).settings;
|
|
77
81
|
const inheritedContinuationModel = session.model;
|
|
78
82
|
let continuationModel = inheritedContinuationModel;
|
|
79
83
|
const continuationReasoningEffort = session.reasoningEffort;
|
|
@@ -109,39 +113,17 @@ export function createGoalActivities(services: () => Promise<ControlActivityServ
|
|
|
109
113
|
) {
|
|
110
114
|
modelPolicyBlocked = `session is locked to Codex remote compaction v2; model "${continuationModel}" is not a Codex subscription model`;
|
|
111
115
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
continuationModel,
|
|
124
|
-
isCodexRun,
|
|
125
|
-
);
|
|
126
|
-
// Budget exhaustion pauses the goal visibly instead of failing the
|
|
127
|
-
// session. Computed up front and applied inside the locked decision so a
|
|
128
|
-
// limits pause never consumes continuation budget.
|
|
129
|
-
const budgetBlocked = await goalRunBudgetBlocked(
|
|
130
|
-
settings,
|
|
131
|
-
db,
|
|
132
|
-
input.accountId,
|
|
133
|
-
input.workspaceId,
|
|
134
|
-
fundedWithoutCredits,
|
|
135
|
-
);
|
|
136
|
-
const turnExecutionPolicy = resolveTurnExecutionPolicyV1(settings, {
|
|
137
|
-
modelId: continuationModel,
|
|
138
|
-
requestedModelId: null,
|
|
139
|
-
modelSource: "continuation",
|
|
140
|
-
reasoningEffort: continuationReasoningEffort,
|
|
141
|
-
reasoningSource: "continuation",
|
|
142
|
-
latencyMode: continuationLatencyMode,
|
|
143
|
-
latencyModeSource: "continuation",
|
|
144
|
-
});
|
|
116
|
+
const turnExecutionPolicy = modelPolicyBlocked
|
|
117
|
+
? undefined
|
|
118
|
+
: resolveTurnExecutionPolicyV1(settings, {
|
|
119
|
+
modelId: continuationModel,
|
|
120
|
+
requestedModelId: null,
|
|
121
|
+
modelSource: "continuation",
|
|
122
|
+
reasoningEffort: continuationReasoningEffort,
|
|
123
|
+
reasoningSource: "continuation",
|
|
124
|
+
latencyMode: continuationLatencyMode,
|
|
125
|
+
latencyModeSource: "continuation",
|
|
126
|
+
});
|
|
145
127
|
const decision = await materializeGoalContinuation(db, {
|
|
146
128
|
accountId: input.accountId,
|
|
147
129
|
workspaceId: input.workspaceId,
|
|
@@ -157,12 +139,30 @@ export function createGoalActivities(services: () => Promise<ControlActivityServ
|
|
|
157
139
|
// A model-policy block takes precedence: it is deterministic (a budget
|
|
158
140
|
// pause can clear on its own; a policy pause needs a model/policy change)
|
|
159
141
|
// and rides the same visible-pause channel.
|
|
160
|
-
|
|
142
|
+
admission: async (tx, causalTurn) => {
|
|
143
|
+
const budgetBlocked = modelPolicyBlocked
|
|
144
|
+
? null
|
|
145
|
+
: await goalRunBudgetBlocked(
|
|
146
|
+
{ ...service, settings, db: tx },
|
|
147
|
+
{
|
|
148
|
+
accountId: input.accountId,
|
|
149
|
+
workspaceId: input.workspaceId,
|
|
150
|
+
model: continuationModel,
|
|
151
|
+
initiatingHumanSubjectId: causalTurn?.initiatingHumanSubjectId ?? null,
|
|
152
|
+
},
|
|
153
|
+
);
|
|
154
|
+
return {
|
|
155
|
+
budgetBlocked: modelPolicyBlocked ?? budgetBlocked?.message ?? null,
|
|
156
|
+
budgetPausedReason: modelPolicyBlocked
|
|
157
|
+
? "limits"
|
|
158
|
+
: (budgetBlocked?.pausedReason ?? "limits"),
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
161
|
policy: {
|
|
162
162
|
model: continuationModel,
|
|
163
163
|
reasoningEffort: continuationReasoningEffort,
|
|
164
164
|
latencyMode: continuationLatencyMode,
|
|
165
|
-
turnExecutionPolicy,
|
|
165
|
+
...(turnExecutionPolicy ? { turnExecutionPolicy } : {}),
|
|
166
166
|
tools: withFirstPartyTools(settings, session.tools),
|
|
167
167
|
sandboxBackend: session.sandboxBackend,
|
|
168
168
|
},
|
|
@@ -215,18 +215,21 @@ export function goalContinuationModelDecision(input: {
|
|
|
215
215
|
providerId: policyProviderIdForModel(catalogSettings, modelId),
|
|
216
216
|
modelId,
|
|
217
217
|
}).allowed;
|
|
218
|
-
if (
|
|
219
|
-
resolveModelProvider(catalogSettings, input.inheritedModel) &&
|
|
220
|
-
!policyBlocks(input.inheritedModel)
|
|
221
|
-
) {
|
|
222
|
-
return { model: input.inheritedModel, blocked: null };
|
|
223
|
-
}
|
|
224
218
|
if (!resolveModelProvider(catalogSettings, input.inheritedModel)) {
|
|
225
219
|
return {
|
|
226
220
|
model: input.inheritedModel,
|
|
227
221
|
blocked: `model "${input.inheritedModel}" is no longer in the deployment or workspace catalog; choose an available model before resuming the goal`,
|
|
228
222
|
};
|
|
229
223
|
}
|
|
224
|
+
if (!isModelAvailableForNewSelection(catalogSettings, input.inheritedModel)) {
|
|
225
|
+
return {
|
|
226
|
+
model: input.inheritedModel,
|
|
227
|
+
blocked: `model "${input.inheritedModel}" is retired from new selection; choose an available model before resuming the goal`,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
if (!policyBlocks(input.inheritedModel)) {
|
|
231
|
+
return { model: input.inheritedModel, blocked: null };
|
|
232
|
+
}
|
|
230
233
|
return {
|
|
231
234
|
model: input.inheritedModel,
|
|
232
235
|
blocked: `workspace model policy blocks model "${input.inheritedModel}"; pick an allowed model or change the workspace model policy`,
|
|
@@ -307,6 +310,8 @@ export function goalContinuationPrompt(
|
|
|
307
310
|
"- For document report deliverables (a document the user asked for, or a large report meant to be kept or shared), follow the Documents Skill: create the durable native document first, inspect its relevant final head after the last edit, and provide the returned artifact reference. Declare report requirements through the available goal tools before authoring and satisfy every persisted report requirement with verified artifact delivery evidence before completion. Sandbox paths and raw file IDs do not prove report delivery. If artifact tooling or access is unavailable, keep that deliverable incomplete and state the blocker; never invent proof or silently substitute a local report. Ordinary chat answers, short progress updates, source-code links, and explicitly requested local-file work remain outside this report contract.",
|
|
308
311
|
"",
|
|
309
312
|
"Do not rely on intent, partial progress, memory of earlier work, or a plausible final answer as proof of completion. Call opengeni__goal_complete with concrete evidence only when the full objective is actually achieved and no required work remains.",
|
|
313
|
+
"Goal evidence is a short proof for the ledger, not the deliverable. After goal_complete succeeds, finish this same turn with the requested user-facing answer, or a concise summary and retained artifact link. Goal completion stops future automatic continuations; it does not send the answer or end this turn. Never compress a report into evidence or omit the final reply.",
|
|
314
|
+
"Goal progress notes are short human-readable milestone statuses, not raw transcripts or continuation instructions. Keep normal spaces and summarize detail instead of squeezing words into a ledger field. The text and successCriteria fields each allow 8192 UTF-8 bytes, progressNote allows 8192 UTF-8 bytes, rationale allows 2048 UTF-8 bytes, and evidence allows 8192 characters.",
|
|
310
315
|
"",
|
|
311
316
|
...waitingGuidance,
|
|
312
317
|
...childNoticeGuidance,
|
|
@@ -336,55 +341,22 @@ export function withFirstPartyTools(settings: Settings, tools: ToolRef[]): ToolR
|
|
|
336
341
|
}
|
|
337
342
|
|
|
338
343
|
/**
|
|
339
|
-
*
|
|
340
|
-
* readable reason when balance or monthly caps block another agent run.
|
|
344
|
+
* Goals share scheduled admission and pause visibly without synthesizing work.
|
|
341
345
|
*/
|
|
342
|
-
async function goalRunBudgetBlocked(
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
// credit-balance gate and monthly model-cost cap. The agent-run COUNT cap
|
|
351
|
-
// below is a volume quota (not a credit/cost gate) and remains enforced.
|
|
352
|
-
if (
|
|
353
|
-
!fundedWithoutCredits &&
|
|
354
|
-
(settings.billingMode === "stripe" || settings.usageLimitsMode === "managed")
|
|
355
|
-
) {
|
|
356
|
-
const balance = await getBillingBalance(db, accountId);
|
|
357
|
-
if (balance.balanceMicros <= 0) {
|
|
358
|
-
return "insufficient OpenGeni credits";
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
if (settings.usageLimitsMode === "static" || settings.usageLimitsMode === "managed") {
|
|
362
|
-
const limits = configuredStaticUsageLimits(settings);
|
|
363
|
-
if (!fundedWithoutCredits && limits.maxMonthlyCostMicrosPerAccount) {
|
|
364
|
-
const used = await sumUsageQuantity(db, {
|
|
365
|
-
accountId,
|
|
366
|
-
eventType: "model.cost",
|
|
367
|
-
since: startOfUtcMonth(),
|
|
368
|
-
});
|
|
369
|
-
if (used >= limits.maxMonthlyCostMicrosPerAccount) {
|
|
370
|
-
return `monthly model cost limit reached (${limits.maxMonthlyCostMicrosPerAccount} micros)`;
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
if (limits.maxMonthlyAgentRunsPerWorkspace) {
|
|
374
|
-
const used = await sumUsageQuantity(db, {
|
|
375
|
-
workspaceId,
|
|
376
|
-
eventType: "agent_run.created",
|
|
377
|
-
since: startOfUtcMonth(),
|
|
378
|
-
});
|
|
379
|
-
if (used + 1 > limits.maxMonthlyAgentRunsPerWorkspace) {
|
|
380
|
-
return `monthly agent run limit reached (${limits.maxMonthlyAgentRunsPerWorkspace})`;
|
|
381
|
-
}
|
|
382
|
-
}
|
|
346
|
+
export async function goalRunBudgetBlocked(
|
|
347
|
+
services: Parameters<typeof agentRunAdmissionDenial>[0],
|
|
348
|
+
input: Omit<Parameters<typeof agentRunAdmissionDenial>[1], "requestedAgentRuns">,
|
|
349
|
+
): Promise<{ pausedReason: "limits" | "allowance"; message: string } | null> {
|
|
350
|
+
const denial = await agentRunAdmissionDenial(services, { ...input, requestedAgentRuns: 1 });
|
|
351
|
+
if (denial === null) return null;
|
|
352
|
+
if (denial === "allowance_exhausted") {
|
|
353
|
+
return { pausedReason: "allowance", message: "OpenGeni usage allowance exhausted" };
|
|
383
354
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
355
|
+
const limits = configuredStaticUsageLimits(services.settings);
|
|
356
|
+
const messages = {
|
|
357
|
+
insufficient_credits: "insufficient OpenGeni credits",
|
|
358
|
+
monthly_model_cost_limit: `monthly model cost limit reached (${limits.maxMonthlyCostMicrosPerAccount} micros)`,
|
|
359
|
+
monthly_agent_run_limit: `monthly agent run limit reached (${limits.maxMonthlyAgentRunsPerWorkspace})`,
|
|
360
|
+
};
|
|
361
|
+
return { pausedReason: "limits", message: messages[denial] };
|
|
390
362
|
}
|
|
@@ -4,8 +4,10 @@ import {
|
|
|
4
4
|
applyCreditDebitAfterUse,
|
|
5
5
|
appendKnowledgeIndexChunks,
|
|
6
6
|
claimKnowledgeIndexJobs,
|
|
7
|
+
checkWorkspaceAllowance,
|
|
7
8
|
completeKnowledgeIndexJob,
|
|
8
9
|
continueKnowledgeIndexJob,
|
|
10
|
+
creditDebitAttributionMetadata,
|
|
9
11
|
deferKnowledgeIndexJob,
|
|
10
12
|
freezeKnowledgeIndexBillingMode,
|
|
11
13
|
getBillingBalance,
|
|
@@ -198,6 +200,16 @@ export function createKnowledgeIndexingActivities(
|
|
|
198
200
|
return;
|
|
199
201
|
}
|
|
200
202
|
const paid = frozenPolicy.mode === "credits";
|
|
203
|
+
if (paid && claim.billingAttribution.kind === "unknown") {
|
|
204
|
+
// Unknown legacy/source-preparation causality is not service.
|
|
205
|
+
// Keep the checkpoint without making an uncountable paid call.
|
|
206
|
+
observability.warn("Paid Knowledge indexing awaits initiating attribution", {
|
|
207
|
+
errorCode: "knowledge_index_attribution_unavailable",
|
|
208
|
+
});
|
|
209
|
+
await deferKnowledgeIndexJob(lockedDb, claim);
|
|
210
|
+
result.deferred++;
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
201
213
|
if (paid && current.nextIndex === 0) {
|
|
202
214
|
const balance = await getBillingBalance(lockedDb, claim.accountId);
|
|
203
215
|
if (balance.balanceMicros <= 0) {
|
|
@@ -206,6 +218,22 @@ export function createKnowledgeIndexingActivities(
|
|
|
206
218
|
return;
|
|
207
219
|
}
|
|
208
220
|
}
|
|
221
|
+
if (paid) {
|
|
222
|
+
const refusal = await checkWorkspaceAllowance(lockedDb, {
|
|
223
|
+
accountId: claim.accountId,
|
|
224
|
+
workspaceId: current.billingWorkspaceId,
|
|
225
|
+
subjectId:
|
|
226
|
+
claim.billingAttribution.kind === "turn" ||
|
|
227
|
+
claim.billingAttribution.kind === "human"
|
|
228
|
+
? claim.billingAttribution.initiatingHumanSubjectId
|
|
229
|
+
: null,
|
|
230
|
+
});
|
|
231
|
+
if (refusal) {
|
|
232
|
+
await deferKnowledgeIndexJob(lockedDb, claim);
|
|
233
|
+
result.deferred++;
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
209
237
|
if (settings.usageLimitsMode === "static" || settings.usageLimitsMode === "managed") {
|
|
210
238
|
const limit =
|
|
211
239
|
configuredStaticUsageLimits(settings).maxDocumentIndexedChunksPerWorkspace;
|
|
@@ -309,6 +337,7 @@ export function createKnowledgeIndexingActivities(
|
|
|
309
337
|
sourceId: claim.revisionId,
|
|
310
338
|
idempotencyKey: `knowledge.embedding:${claim.revisionId}:${claim.generation}:${current.nextIndex}`,
|
|
311
339
|
metadata: {
|
|
340
|
+
...creditDebitAttributionMetadata(claim.billingAttribution),
|
|
312
341
|
model: claim.model,
|
|
313
342
|
bytes,
|
|
314
343
|
chunks: chunks.length,
|