@aglyn/plugins-ai 1.0.0-beta.218 → 1.0.0-beta.220
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 +2 -2
- package/package.json +18 -18
- package/src/index.d.ts +1 -0
- package/src/index.js.map +1 -1
- package/src/lib/activity/ai-activity-actions.js +8 -1
- package/src/lib/activity/ai-activity-actions.js.map +1 -1
- package/src/lib/billing/ai-month-meter.d.ts +51 -0
- package/src/lib/billing/ai-month-meter.js +89 -0
- package/src/lib/billing/ai-month-meter.js.map +1 -0
- package/src/lib/billing/ai-overage-close.d.ts +11 -33
- package/src/lib/billing/ai-overage-close.js +4 -3
- package/src/lib/billing/ai-overage-close.js.map +1 -1
- package/src/lib/billing/ai-overage-cutover.d.ts +28 -12
- package/src/lib/billing/ai-overage-cutover.js +1 -28
- package/src/lib/billing/ai-overage-cutover.js.map +1 -1
- package/src/lib/billing/ai-overage-ledger.js +1 -1
- package/src/lib/billing/ai-overage-ledger.js.map +1 -1
- package/src/lib/billing/ai-overage-standing.js +1 -1
- package/src/lib/billing/ai-overage-standing.js.map +1 -1
- package/src/lib/components/ai-allotment-editor.component.js +1 -1
- package/src/lib/components/ai-allotment-editor.component.js.map +1 -1
- package/src/lib/components/ai-assist-provider.component.js +5 -4
- package/src/lib/components/ai-assist-provider.component.js.map +1 -1
- package/src/lib/components/ai-automation-zones.d.ts +63 -0
- package/src/lib/components/ai-automation-zones.js +31 -0
- package/src/lib/components/ai-automation-zones.js.map +1 -0
- package/src/lib/components/ai-brief-dialog.component.js +3 -3
- package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
- package/src/lib/components/ai-describe-automation.component.d.ts +1 -1
- package/src/lib/components/ai-describe-automation.component.js.map +1 -1
- package/src/lib/components/ai-describe-button.component.d.ts +6 -3
- package/src/lib/components/ai-describe-button.component.js +4 -1
- package/src/lib/components/ai-describe-button.component.js.map +1 -1
- package/src/lib/components/ai-experiment-cards.component.js +1 -1
- package/src/lib/components/ai-experiment-cards.component.js.map +1 -1
- package/src/lib/components/ai-explain-automation.component.d.ts +1 -1
- package/src/lib/components/ai-explain-automation.component.js.map +1 -1
- package/src/lib/components/ai-job-plan.component.d.ts +8 -0
- package/src/lib/components/ai-job-plan.component.js +21 -4
- package/src/lib/components/ai-job-plan.component.js.map +1 -1
- package/src/lib/components/ai-permissions-on-host.component.d.ts +12 -2
- package/src/lib/components/ai-permissions-on-host.component.js +12 -3
- package/src/lib/components/ai-permissions-on-host.component.js.map +1 -1
- package/src/lib/components/ai-product-copy-card.component.d.ts +1 -1
- package/src/lib/components/ai-product-copy-card.component.js.map +1 -1
- package/src/lib/components/ai-product-import-option.component.d.ts +1 -1
- package/src/lib/components/ai-product-import-option.component.js.map +1 -1
- package/src/lib/components/ai-product-zones.d.ts +124 -0
- package/src/lib/components/ai-product-zones.js +32 -0
- package/src/lib/components/ai-product-zones.js.map +1 -0
- package/src/lib/components/ai-products-hub-card.component.d.ts +1 -1
- package/src/lib/components/ai-products-hub-card.component.js.map +1 -1
- package/src/lib/components/ai-products-proposals.component.d.ts +1 -1
- package/src/lib/components/ai-products-proposals.component.js.map +1 -1
- package/src/lib/components/ai-seo-audit-card.component.js +35 -92
- package/src/lib/components/ai-seo-audit-card.component.js.map +1 -1
- package/src/lib/components/ai-seo-fields-card.component.js.map +1 -1
- package/src/lib/components/ai-theme-proposal-card.component.js.map +1 -1
- package/src/lib/components/assist-jobs-drawer.component.js +21 -15
- package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
- package/src/lib/components/assist-panel.component.js +2 -2
- package/src/lib/components/assist-panel.component.js.map +1 -1
- package/src/lib/components/assist-signals-page.component.js +2 -2
- package/src/lib/components/assist-signals-page.component.js.map +1 -1
- package/src/lib/components/billing-ai-allotments.component.js +1 -1
- package/src/lib/components/billing-ai-allotments.component.js.map +1 -1
- package/src/lib/components/billing-assist-overage-card.component.js +1 -1
- package/src/lib/components/billing-assist-overage-card.component.js.map +1 -1
- package/src/lib/constants.d.ts +5 -0
- package/src/lib/constants.js +4 -0
- package/src/lib/constants.js.map +1 -1
- package/src/lib/declarations.js +2 -0
- package/src/lib/declarations.js.map +1 -1
- package/src/lib/declarations.server.js +24 -10
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/insights/ai-dataset-catalog.d.ts +49 -0
- package/src/lib/insights/ai-dataset-catalog.js +57 -0
- package/src/lib/insights/ai-dataset-catalog.js.map +1 -0
- package/src/lib/insights/ai-figure-readers.d.ts +6 -39
- package/src/lib/insights/ai-figure-readers.js +2 -391
- package/src/lib/insights/ai-figure-readers.js.map +1 -1
- package/src/lib/insights/ai-insight-digest.d.ts +10 -1
- package/src/lib/insights/ai-insight-digest.js +16 -4
- package/src/lib/insights/ai-insight-digest.js.map +1 -1
- package/src/lib/insights/ai-insight-readers.d.ts +1 -1
- package/src/lib/insights/ai-insight-readers.js.map +1 -1
- package/src/lib/jobs/ai-email-bindings.d.ts +28 -21
- package/src/lib/jobs/ai-email-bindings.js +75 -42
- package/src/lib/jobs/ai-email-bindings.js.map +1 -1
- package/src/lib/jobs/ai-job-campaign-step.d.ts +0 -2
- package/src/lib/jobs/ai-job-campaign-step.js +5 -10
- package/src/lib/jobs/ai-job-campaign-step.js.map +1 -1
- package/src/lib/jobs/ai-job-component-step.d.ts +6 -1
- package/src/lib/jobs/ai-job-component-step.js +32 -9
- package/src/lib/jobs/ai-job-component-step.js.map +1 -1
- package/src/lib/jobs/ai-job-draft-ids.js +11 -0
- package/src/lib/jobs/ai-job-draft-ids.js.map +1 -1
- package/src/lib/jobs/ai-job-drafts.js +1 -1
- package/src/lib/jobs/ai-job-drafts.js.map +1 -1
- package/src/lib/jobs/ai-job-email-step.d.ts +5 -3
- package/src/lib/jobs/ai-job-email-step.js +11 -4
- package/src/lib/jobs/ai-job-email-step.js.map +1 -1
- package/src/lib/jobs/ai-job-experiment-step.d.ts +1 -1
- package/src/lib/jobs/ai-job-experiment-step.js +1 -1
- package/src/lib/jobs/ai-job-experiment-step.js.map +1 -1
- package/src/lib/jobs/ai-job-form-step.d.ts +17 -1
- package/src/lib/jobs/ai-job-form-step.js +60 -8
- package/src/lib/jobs/ai-job-form-step.js.map +1 -1
- package/src/lib/jobs/ai-job-generation.d.ts +22 -1
- package/src/lib/jobs/ai-job-generation.js +63 -3
- package/src/lib/jobs/ai-job-generation.js.map +1 -1
- package/src/lib/jobs/ai-job-insight-step.js +1 -1
- package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.d.ts +15 -4
- package/src/lib/jobs/ai-job-layout-step.js +46 -13
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-sections.d.ts +4 -2
- package/src/lib/jobs/ai-job-page-sections.js +7 -4
- package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.js +7 -2
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-conformance.d.ts +5 -0
- package/src/lib/jobs/ai-job-plan-conformance.js +40 -1
- package/src/lib/jobs/ai-job-plan-conformance.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-creations.d.ts +80 -0
- package/src/lib/jobs/ai-job-plan-creations.js +199 -0
- package/src/lib/jobs/ai-job-plan-creations.js.map +1 -0
- package/src/lib/jobs/ai-job-plan-step.d.ts +11 -2
- package/src/lib/jobs/ai-job-plan-step.js +49 -23
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plugin-drafts.d.ts +14 -6
- package/src/lib/jobs/ai-job-plugin-drafts.js +12 -4
- package/src/lib/jobs/ai-job-plugin-drafts.js.map +1 -1
- package/src/lib/jobs/ai-job-products-step.d.ts +7 -0
- package/src/lib/jobs/ai-job-products-step.js +54 -32
- package/src/lib/jobs/ai-job-products-step.js.map +1 -1
- package/src/lib/jobs/ai-job-seo-step.d.ts +3 -24
- package/src/lib/jobs/ai-job-seo-step.js +35 -108
- package/src/lib/jobs/ai-job-seo-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.d.ts +2 -0
- package/src/lib/jobs/ai-job-site-step.js +6 -6
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-template-step.d.ts +9 -6
- package/src/lib/jobs/ai-job-template-step.js +73 -15
- package/src/lib/jobs/ai-job-template-step.js.map +1 -1
- package/src/lib/jobs/ai-job-theme-step.d.ts +1 -1
- package/src/lib/jobs/ai-job-theme-step.js +1 -1
- package/src/lib/jobs/ai-job-theme-step.js.map +1 -1
- package/src/lib/jobs/ai-job-workflow-step.js +17 -9
- package/src/lib/jobs/ai-job-workflow-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs.js +1 -1
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/jobs/ai-workflow-records.d.ts +20 -11
- package/src/lib/jobs/ai-workflow-records.js +80 -43
- package/src/lib/jobs/ai-workflow-records.js.map +1 -1
- package/src/lib/model/ai-allotments.d.ts +27 -6
- package/src/lib/model/ai-allotments.js +54 -14
- package/src/lib/model/ai-allotments.js.map +1 -1
- package/src/lib/model/ai-automation-outline.d.ts +21 -2
- package/src/lib/model/ai-automation-outline.js.map +1 -1
- package/src/lib/model/ai-build-plan.d.ts +57 -0
- package/src/lib/model/ai-build-plan.js +116 -7
- package/src/lib/model/ai-build-plan.js.map +1 -1
- package/src/lib/model/ai-job-creations.d.ts +55 -0
- package/src/lib/model/ai-job-creations.js +85 -0
- package/src/lib/model/ai-job-creations.js.map +1 -0
- package/src/lib/model/ai-page-job.d.ts +3 -2
- package/src/lib/model/ai-page-job.js +4 -3
- package/src/lib/model/ai-page-job.js.map +1 -1
- package/src/lib/model/ai-seo.d.ts +11 -51
- package/src/lib/model/ai-seo.js +0 -17
- package/src/lib/model/ai-seo.js.map +1 -1
- package/src/lib/model/ai-site-inventory.js +1 -1
- package/src/lib/model/ai-site-inventory.js.map +1 -1
- package/src/lib/model/ai-site-job.d.ts +4 -2
- package/src/lib/model/ai-site-job.js +8 -4
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-template-subjects.d.ts +10 -0
- package/src/lib/model/ai-template-subjects.js +16 -2
- package/src/lib/model/ai-template-subjects.js.map +1 -1
- package/src/lib/model/ai-theme-proposal.d.ts +1 -1
- package/src/lib/model/ai-theme-proposal.js +1 -1
- package/src/lib/model/ai-theme-proposal.js.map +1 -1
- package/src/lib/model/ai-tokens.js +1 -1
- package/src/lib/model/ai-tokens.js.map +1 -1
- package/src/lib/model/ai-usage-by-user.js +1 -1
- package/src/lib/model/ai-usage-by-user.js.map +1 -1
- package/src/lib/model/ai-workflow-job.d.ts +1 -3
- package/src/lib/model/ai-workflow-job.js +1 -2
- package/src/lib/model/ai-workflow-job.js.map +1 -1
- package/src/lib/plan-entitlements.d.ts +149 -0
- package/src/lib/plan-entitlements.js +168 -0
- package/src/lib/plan-entitlements.js.map +1 -0
- package/src/lib/plugin.js +1 -1
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/providers/catalog.js +1 -1
- package/src/lib/providers/catalog.js.map +1 -1
- package/src/lib/providers/model-choice.js +1 -1
- package/src/lib/providers/model-choice.js.map +1 -1
- package/src/lib/runtime/ai-device-audit.d.ts +1 -1
- package/src/lib/runtime/ai-device-audit.js +1 -1
- package/src/lib/runtime/ai-device-audit.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +25 -4
- package/src/lib/runtime/ai-doctrine-validators.js +178 -21
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +5 -5
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-eval-live.js +30 -15
- package/src/lib/runtime/ai-eval-live.js.map +1 -1
- package/src/lib/runtime/ai-eval.d.ts +9 -2
- package/src/lib/runtime/ai-eval.js +29 -2
- package/src/lib/runtime/ai-eval.js.map +1 -1
- package/src/lib/runtime/ai-gate.js +1 -1
- package/src/lib/runtime/ai-gate.js.map +1 -1
- package/src/lib/runtime/ai-node-tree.js +3 -3
- package/src/lib/runtime/ai-node-tree.js.map +1 -1
- package/src/lib/runtime/ai-palette.d.ts +7 -1
- package/src/lib/runtime/ai-palette.generated.d.ts +1 -1
- package/src/lib/runtime/ai-palette.generated.js +16 -13
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/runtime/ai-palette.js +7 -1
- package/src/lib/runtime/ai-palette.js.map +1 -1
- package/src/lib/runtime/seo-audit.d.ts +13 -63
- package/src/lib/runtime/seo-audit.js +22 -207
- package/src/lib/runtime/seo-audit.js.map +1 -1
- package/src/lib/runtime/site-inventory.js +29 -23
- package/src/lib/runtime/site-inventory.js.map +1 -1
- package/src/lib/server/ai-admin-org.js +1 -1
- package/src/lib/server/ai-admin-org.js.map +1 -1
- package/src/lib/server/ai-admin-orgs-spend.js +1 -1
- package/src/lib/server/ai-admin-orgs-spend.js.map +1 -1
- package/src/lib/server/ai-admin-signals.js +1 -1
- package/src/lib/server/ai-admin-signals.js.map +1 -1
- package/src/lib/server/ai-allotments.js +1 -1
- package/src/lib/server/ai-allotments.js.map +1 -1
- package/src/lib/server/ai-assist-prompts.js +3 -3
- package/src/lib/server/ai-assist-prompts.js.map +1 -1
- package/src/lib/server/ai-assist.js +2 -2
- package/src/lib/server/ai-assist.js.map +1 -1
- package/src/lib/server/assist-chat.d.ts +1 -1
- package/src/lib/server/assist-chat.js +13 -12
- package/src/lib/server/assist-chat.js.map +1 -1
- package/src/lib/server/assist-view-context.js +23 -23
- package/src/lib/server/assist-view-context.js.map +1 -1
- package/src/lib/server/billing-credits.d.ts +5 -0
- package/src/lib/server/billing-credits.js +8 -2
- package/src/lib/server/billing-credits.js.map +1 -1
- package/src/lib/server/billing-overage.js +2 -2
- package/src/lib/server/billing-overage.js.map +1 -1
- package/src/lib/server.d.ts +1 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/stand-in-automation-index.d.ts +20 -0
- package/src/lib/testing/stand-in-automation-index.js +102 -0
- package/src/lib/testing/stand-in-automation-index.js.map +1 -0
- package/src/lib/testing/stand-in-dataset-index.d.ts +20 -0
- package/src/lib/testing/stand-in-dataset-index.js +80 -0
- package/src/lib/testing/stand-in-dataset-index.js.map +1 -0
- package/src/lib/testing/stand-in-list-send-time.d.ts +4 -0
- package/src/lib/testing/stand-in-list-send-time.js +77 -0
- package/src/lib/testing/stand-in-list-send-time.js.map +1 -0
- package/src/lib/testing/stand-in-pipeline-index.d.ts +32 -0
- package/src/lib/testing/stand-in-pipeline-index.js +59 -0
- package/src/lib/testing/stand-in-pipeline-index.js.map +1 -0
- package/src/lib/testing/stand-in-product-index.d.ts +20 -0
- package/src/lib/testing/stand-in-product-index.js +105 -0
- package/src/lib/testing/stand-in-product-index.js.map +1 -0
- package/src/lib/tools/ai-component-tool.js +2 -2
- package/src/lib/tools/ai-component-tool.js.map +1 -1
- package/src/lib/tools/ai-duplicate-resource-tool.js +1 -1
- package/src/lib/tools/ai-duplicate-resource-tool.js.map +1 -1
- package/src/lib/tools/ai-seo-tool.d.ts +6 -18
- package/src/lib/tools/ai-seo-tool.js +7 -38
- package/src/lib/tools/ai-seo-tool.js.map +1 -1
- package/src/lib/tools/ai-workflow-tool.d.ts +1 -1
- package/src/lib/tools/ai-workflow-tool.js +1 -1
- package/src/lib/tools/ai-workflow-tool.js.map +1 -1
- package/src/lib/usage/ai-allotment-alerts.d.ts +10 -7
- package/src/lib/usage/ai-allotment-alerts.js +39 -15
- package/src/lib/usage/ai-allotment-alerts.js.map +1 -1
- package/src/lib/usage/ai-quota-checks.d.ts +37 -0
- package/src/lib/usage/ai-quota-checks.js +69 -0
- package/src/lib/usage/ai-quota-checks.js.map +1 -0
- package/src/lib/usage/ai-usage-alerts.js +22 -10
- package/src/lib/usage/ai-usage-alerts.js.map +1 -1
- package/src/lib/usage/ai-usage-by-user.js +1 -1
- package/src/lib/usage/ai-usage-by-user.js.map +1 -1
- package/src/lib/usage/assist-ceiling.d.ts +2 -2
- package/src/lib/usage/assist-ceiling.js +4 -4
- package/src/lib/usage/assist-ceiling.js.map +1 -1
- package/src/lib/usage/assist-credits.d.ts +494 -0
- package/src/lib/usage/assist-credits.js +524 -0
- package/src/lib/usage/assist-credits.js.map +1 -0
- package/src/lib/usage/assist-free-taste.d.ts +1 -1
- package/src/lib/usage/assist-free-taste.js +3 -2
- package/src/lib/usage/assist-free-taste.js.map +1 -1
- package/src/lib/usage/assist-signal-mining.js +1 -1
- package/src/lib/usage/assist-signal-mining.js.map +1 -1
- package/src/lib/usage/assist-spend-guards.d.ts +153 -0
- package/src/lib/usage/assist-spend-guards.js +182 -0
- package/src/lib/usage/assist-spend-guards.js.map +1 -0
- package/src/lib/usage/assist-usage.d.ts +1 -1
- package/src/lib/usage/assist-usage.js +1 -1
- package/src/lib/usage/assist-usage.js.map +1 -1
- package/src/lib/usage/staff-org-ai.js +1 -1
- package/src/lib/usage/staff-org-ai.js.map +1 -1
- package/src/lib/usage-axes.d.ts +49 -0
- package/src/lib/usage-axes.js +108 -0
- package/src/lib/usage-axes.js.map +1 -0
- package/src/lib/runtime/seo-page-facts.d.ts +0 -61
- package/src/lib/runtime/seo-page-facts.js +0 -150
- package/src/lib/runtime/seo-page-facts.js.map +0 -1
|
@@ -13,11 +13,12 @@
|
|
|
13
13
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
|
-
*/ import {
|
|
17
|
-
import {
|
|
16
|
+
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
+
import { actionRunResult } from "@aglyn/aglyn/app-utils/activity-presenter";
|
|
18
18
|
import { isFormArchived } from "@aglyn/aglyn/app-utils/forms";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
19
|
+
import { pluginRecordIndex } from "@aglyn/aglyn/plugin-manager/plugin-record-index";
|
|
20
|
+
import { visibleToHost } from "@aglyn/aglyn/app-utils/scope-tokens";
|
|
21
|
+
import { listOrgContainers } from "@aglyn/tenant-data-admin/server/org-containers";
|
|
21
22
|
/** Records of one kind a lookup reads: the Actions editor's picker window. */ export const AI_WORKFLOW_RECORDS_WINDOW = 100;
|
|
22
23
|
/** Pipelines a stage lookup reads. */ export const AI_WORKFLOW_PIPELINES_WINDOW = 20;
|
|
23
24
|
function text(value) {
|
|
@@ -37,6 +38,27 @@ async function named(query, fields, keep, nameOf) {
|
|
|
37
38
|
})).filter((record)=>record.name);
|
|
38
39
|
}
|
|
39
40
|
const live = (data)=>data['deletedAt'] == null;
|
|
41
|
+
/** The org's live campaigns placed on the site, named, in the editor's window. */ async function placedCampaigns(firestore, orgId, hostId) {
|
|
42
|
+
const containers = await listOrgContainers(firestore, 'campaign', orgId, AI_WORKFLOW_RECORDS_WINDOW);
|
|
43
|
+
return containers.filter((container)=>container.live && container.name && visibleToHost(container.visibleTo, hostId)).map((container)=>({
|
|
44
|
+
id: container.id,
|
|
45
|
+
name: container.name
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The live records of a kind another plugin keeps, through its index, in the
|
|
50
|
+
* editor's window; none where no plugin keeps the kind here.
|
|
51
|
+
*/ async function indexed(kind, scope, keep = ()=>true) {
|
|
52
|
+
const owner = pluginRecordIndex(kind);
|
|
53
|
+
if (!owner) return [];
|
|
54
|
+
const { records } = await owner.index.list(_extends({}, scope, {
|
|
55
|
+
limit: AI_WORKFLOW_RECORDS_WINDOW
|
|
56
|
+
}));
|
|
57
|
+
return records.filter(keep).map(({ id, name })=>({
|
|
58
|
+
id,
|
|
59
|
+
name
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
40
62
|
/** The site's forms that are not archived, with the field names their submissions carry. */ async function readForms(host) {
|
|
41
63
|
const snapshot = await host.collection('forms').select('displayName', 'slug', 'fields', 'archivedAt').limit(AI_WORKFLOW_RECORDS_WINDOW).get();
|
|
42
64
|
return snapshot.docs.filter((doc)=>{
|
|
@@ -64,37 +86,28 @@ const live = (data)=>data['deletedAt'] == null;
|
|
|
64
86
|
const none = Promise.resolve([]);
|
|
65
87
|
const [forms, datasets, lists, campaigns, workflows, webhooks, stages] = await Promise.all([
|
|
66
88
|
!wanted('forms') ? Promise.resolve([]) : readForms(host),
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
89
|
+
// The org's datasets shared with this site, as the data plugin indexes them.
|
|
90
|
+
!wanted('datasets') ? none : indexed('dataset', {
|
|
91
|
+
orgId: input.orgId,
|
|
92
|
+
hostId: input.hostId
|
|
93
|
+
}),
|
|
72
94
|
!wanted('lists') ? none : named(org.collection('lists'), [
|
|
73
95
|
'name',
|
|
74
96
|
'deletedAt'
|
|
75
97
|
], live, (data)=>text(data['name'])),
|
|
76
98
|
// The org's campaigns placed on this site: the set the automation's
|
|
77
|
-
// `assignCampaign` step accepts when it runs here.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
!wanted('
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
!wanted('webhooks') ? none : named(host.collection('webhooks'), [
|
|
90
|
-
'name',
|
|
91
|
-
'direction',
|
|
92
|
-
'deletedAt'
|
|
93
|
-
], (data)=>live(data) && data['direction'] === 'outbound', (data)=>text(data['name'])),
|
|
94
|
-
input.crm && wanted('stages') ? scopedToHost(org.collection('pipelines'), input.hostId).select('stages', 'archivedAt').limit(AI_WORKFLOW_PIPELINES_WINDOW).get().then((snapshot)=>snapshot.docs.filter((doc)=>!(Number(doc.get('archivedAt')) > 0)).flatMap((doc)=>Array.isArray(doc.get('stages')) ? doc.get('stages') : []).map((stage)=>({
|
|
95
|
-
id: text(stage == null ? void 0 : stage['id']),
|
|
96
|
-
name: text(stage == null ? void 0 : stage['name'])
|
|
97
|
-
})).filter((stage)=>stage.id && stage.name)) : none
|
|
99
|
+
// `assignCampaign` step accepts when it runs here. Read as the `campaign`
|
|
100
|
+
// container kind, where its declaration says the containers are stored.
|
|
101
|
+
!wanted('campaigns') ? none : placedCampaigns(firestore, input.orgId, input.hostId),
|
|
102
|
+
!wanted('workflows') ? none : indexed('workflow', {
|
|
103
|
+
hostId: input.hostId
|
|
104
|
+
}),
|
|
105
|
+
// Only a webhook that posts OUT is a step's target; an inbound one is an
|
|
106
|
+
// endpoint that runs a workflow.
|
|
107
|
+
!wanted('webhooks') ? none : indexed('webhook', {
|
|
108
|
+
hostId: input.hostId
|
|
109
|
+
}, (record)=>record.facts['direction'] === 'outbound'),
|
|
110
|
+
input.crm && wanted('stages') ? readStages(input.orgId, input.hostId) : none
|
|
98
111
|
]);
|
|
99
112
|
return {
|
|
100
113
|
forms,
|
|
@@ -106,29 +119,53 @@ const live = (data)=>data['deletedAt'] == null;
|
|
|
106
119
|
stages
|
|
107
120
|
};
|
|
108
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* The stages of the pipelines the site can see, asked of the plugin that keeps
|
|
124
|
+
* pipelines (its `pipeline` record index), never read from its collection. No
|
|
125
|
+
* index means no plugin keeps pipelines in this process: no stage to name.
|
|
126
|
+
*/ async function readStages(orgId, hostId) {
|
|
127
|
+
const pipelines = pluginRecordIndex('pipeline');
|
|
128
|
+
if (!pipelines) return [];
|
|
129
|
+
const { records } = await pipelines.index.list({
|
|
130
|
+
orgId,
|
|
131
|
+
hostId,
|
|
132
|
+
limit: AI_WORKFLOW_PIPELINES_WINDOW
|
|
133
|
+
});
|
|
134
|
+
return records.flatMap((record)=>Array.isArray(record.facts['stages']) ? record.facts['stages'] : []).map((stage)=>({
|
|
135
|
+
id: text(stage == null ? void 0 : stage['id']),
|
|
136
|
+
name: text(stage == null ? void 0 : stage['name'])
|
|
137
|
+
})).filter((stage)=>stage.id && stage.name);
|
|
138
|
+
}
|
|
109
139
|
/** The site's functions, for a workflow outline that says which of its calls still resolve. */ export async function readAiWorkflowFunctions(firestore, hostId) {
|
|
110
140
|
return named(firestore.collection('hosts').doc(hostId).collection('functions'), [
|
|
111
141
|
'name',
|
|
112
142
|
'deletedAt'
|
|
113
143
|
], live, (data)=>text(data['name']));
|
|
114
144
|
}
|
|
115
|
-
/**
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
145
|
+
/**
|
|
146
|
+
* A saved automation of the site, read through the index of the plugin that
|
|
147
|
+
* keeps it, or `null` for one that does not exist, was deleted, or that no
|
|
148
|
+
* plugin here keeps.
|
|
149
|
+
*/ export async function readAiWorkflowTarget(input) {
|
|
150
|
+
const owner = pluginRecordIndex(input.type);
|
|
151
|
+
const record = owner ? await owner.index.get({
|
|
152
|
+
hostId: input.hostId,
|
|
153
|
+
id: input.id
|
|
154
|
+
}) : null;
|
|
155
|
+
if (!record) return null;
|
|
156
|
+
const stored = _extends({
|
|
157
|
+
name: record.name
|
|
158
|
+
}, record.facts);
|
|
122
159
|
return input.type === 'action' ? {
|
|
123
160
|
type: 'action',
|
|
124
|
-
id:
|
|
125
|
-
name,
|
|
126
|
-
action:
|
|
161
|
+
id: record.id,
|
|
162
|
+
name: record.name,
|
|
163
|
+
action: stored
|
|
127
164
|
} : {
|
|
128
165
|
type: 'workflow',
|
|
129
|
-
id:
|
|
130
|
-
name,
|
|
131
|
-
workflow:
|
|
166
|
+
id: record.id,
|
|
167
|
+
name: record.name,
|
|
168
|
+
workflow: stored
|
|
132
169
|
};
|
|
133
170
|
}
|
|
134
171
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-workflow-records.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { HostAction } from '@aglyn/aglyn/app-utils/actions'\nimport { actionRunResult } from '@aglyn/aglyn/app-utils/activity-presenter'\nimport { datasetDisplayName } from '@aglyn/aglyn/app-utils/datasets'\nimport { isFormArchived } from '@aglyn/aglyn/app-utils/forms'\nimport type { HostWorkflow } from '@aglyn/aglyn/app-utils/workflows'\nimport { campaignPlacedOnHost } from '@aglyn/shared-ui-email-campaigns/model/campaign-container'\nimport { scopedToHost } from '@aglyn/tenant-data-admin/server/organizations'\nimport type {\n AiAutomationForm,\n AiAutomationNamedRecord,\n AiAutomationRecords,\n} from '../model/ai-automation-draft'\nimport type { AiRunRecord } from '../model/ai-automation-outline'\nimport type { AiWorkflowTargetType } from '../model/ai-workflow-job'\n\n/**\n * What a `workflow` job reads (AGL-2919), through the Admin SDK and scoped to\n * the job's own site and org: the records a drafted automation's words are\n * looked up among, the automation an explanation reads, and the run it\n * explains. Every read is a projection of the fields named here, windowed the\n * way the Actions editor's pickers are, so a lookup never reads more than the\n * editor would offer.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\ntype Data = Record<string, unknown>\n\n/** Records of one kind a lookup reads: the Actions editor's picker window. */\nexport const AI_WORKFLOW_RECORDS_WINDOW = 100\n\n/** Pipelines a stage lookup reads. */\nexport const AI_WORKFLOW_PIPELINES_WINDOW = 20\n\nfunction text(value: unknown): string {\n return typeof value === 'string' ? value.trim() : ''\n}\n\nasync function named(\n query: FirebaseFirestore.Query,\n fields: string[],\n keep: (data: Data) => boolean,\n nameOf: (data: Data, id: string) => string,\n): Promise<AiAutomationNamedRecord[]> {\n const snapshot = await query\n .select(...fields)\n .limit(AI_WORKFLOW_RECORDS_WINDOW)\n .get()\n return snapshot.docs\n .map((doc) => ({ data: (doc.data() ?? {}) as Data, id: doc.id }))\n .filter(({ data }) => keep(data))\n .map(({ data, id }) => ({ id, name: nameOf(data, id) }))\n .filter((record) => record.name)\n}\n\nconst live = (data: Data) => data['deletedAt'] == null\n\n/** The site's forms that are not archived, with the field names their submissions carry. */\nasync function readForms(host: FirebaseFirestore.DocumentReference): Promise<AiAutomationForm[]> {\n const snapshot = await host\n .collection('forms')\n .select('displayName', 'slug', 'fields', 'archivedAt')\n .limit(AI_WORKFLOW_RECORDS_WINDOW)\n .get()\n return snapshot.docs\n .filter((doc) => !isFormArchived((doc.data() ?? {}) as { archivedAt?: unknown }))\n .map((doc) => {\n const data = (doc.data() ?? {}) as Data\n return {\n id: doc.id,\n name: text(data['displayName']) || text(data['slug']) || doc.id,\n fields: (Array.isArray(data['fields']) ? (data['fields'] as Data[]) : [])\n .map((field) => text(field?.['fieldName']))\n .filter(Boolean),\n }\n })\n}\n\n/**\n * The site's records a drafted automation may name. Stages are read only for\n * a workspace with the CRM, whose pipelines they are; a site without it has\n * no stage a condition could name. `only`, when given, names the kinds to\n * read, and every other kind is answered empty without a read.\n */\nexport async function readAiAutomationRecords(\n firestore: Firestore,\n input: { orgId: string; hostId: string; crm: boolean; only?: ReadonlyArray<keyof AiAutomationRecords> },\n): Promise<AiAutomationRecords> {\n const host = firestore.collection('hosts').doc(input.hostId)\n const org = firestore.collection('orgs').doc(input.orgId)\n const wanted = (kind: keyof AiAutomationRecords) => !input.only || input.only.includes(kind)\n const none = Promise.resolve([] as AiAutomationNamedRecord[])\n const [forms, datasets, lists, campaigns, workflows, webhooks, stages] = await Promise.all([\n !wanted('forms') ? Promise.resolve([] as AiAutomationForm[]) : readForms(host),\n !wanted('datasets')\n ? none\n : named(\n scopedToHost(org.collection('datasets'), input.hostId),\n ['displayName', 'name', 'deletedAt'],\n live,\n (data, id) => datasetDisplayName(data) || id,\n ),\n !wanted('lists') ? none : named(org.collection('lists'), ['name', 'deletedAt'], live, (data) => text(data['name'])),\n // The org's campaigns placed on this site: the set the automation's\n // `assignCampaign` step accepts when it runs here.\n !wanted('campaigns')\n ? none\n : named(\n org.collection('emailCampaigns'),\n ['name', 'deletedAt', 'visibleTo'],\n (data) =>\n live(data) &&\n campaignPlacedOnHost({ visibleTo: data['visibleTo'] as string[] | undefined }, input.hostId),\n (data) => text(data['name']),\n ),\n !wanted('workflows')\n ? none\n : named(host.collection('workflows'), ['name', 'deletedAt'], live, (data) => text(data['name'])),\n !wanted('webhooks')\n ? none\n : named(\n host.collection('webhooks'),\n ['name', 'direction', 'deletedAt'],\n (data) => live(data) && data['direction'] === 'outbound',\n (data) => text(data['name']),\n ),\n input.crm && wanted('stages')\n ? scopedToHost(org.collection('pipelines'), input.hostId)\n .select('stages', 'archivedAt')\n .limit(AI_WORKFLOW_PIPELINES_WINDOW)\n .get()\n .then((snapshot) =>\n snapshot.docs\n .filter((doc) => !(Number(doc.get('archivedAt')) > 0))\n .flatMap((doc) => (Array.isArray(doc.get('stages')) ? (doc.get('stages') as Data[]) : []))\n .map((stage) => ({ id: text(stage?.['id']), name: text(stage?.['name']) }))\n .filter((stage) => stage.id && stage.name),\n )\n : none,\n ])\n return { forms, datasets, lists, campaigns, workflows, webhooks, stages }\n}\n\n/** The site's functions, for a workflow outline that says which of its calls still resolve. */\nexport async function readAiWorkflowFunctions(\n firestore: Firestore,\n hostId: string,\n): Promise<AiAutomationNamedRecord[]> {\n return named(\n firestore.collection('hosts').doc(hostId).collection('functions'),\n ['name', 'deletedAt'],\n live,\n (data) => text(data['name']),\n )\n}\n\nexport type AiWorkflowTarget =\n | { type: 'action'; id: string; name: string; action: HostAction }\n | { type: 'workflow'; id: string; name: string; workflow: HostWorkflow }\n\n/** A saved automation of the site, or `null` for one that does not exist or was deleted. */\nexport async function readAiWorkflowTarget(\n firestore: Firestore,\n input: { hostId: string; type: AiWorkflowTargetType; id: string },\n): Promise<AiWorkflowTarget | null> {\n const collection = input.type === 'action' ? 'actions' : 'workflows'\n const snapshot = await firestore.collection('hosts').doc(input.hostId).collection(collection).doc(input.id).get()\n const data = snapshot.exists ? ((snapshot.data() ?? {}) as Data) : null\n if (!data || data['deletedAt'] != null) return null\n const name = text(data['name']) || input.id\n return input.type === 'action'\n ? { type: 'action', id: snapshot.id, name, action: data as unknown as HostAction }\n : { type: 'workflow', id: snapshot.id, name, workflow: data as unknown as HostWorkflow }\n}\n\nexport type AiWorkflowRunRead =\n | { ok: true; run: AiRunRecord }\n | { ok: false; reason: 'gone' | 'not-failed' }\n\n/**\n * One run of an automation, as the site's activity log recorded it: gone when\n * there is no such entry or it belongs to another automation, and refused\n * when it did not fail. Only what the run history shows is read — never the\n * event's payload.\n */\nexport async function readAiWorkflowRun(\n firestore: Firestore,\n input: { hostId: string; targetId: string; runId: string },\n): Promise<AiWorkflowRunRead> {\n const snapshot = await firestore\n .collection('hosts')\n .doc(input.hostId)\n .collection('activity')\n .doc(input.runId)\n .get()\n const data = snapshot.exists ? ((snapshot.data() ?? {}) as Data) : null\n const target = data?.['target'] as { id?: unknown } | undefined\n if (!data || target?.id !== input.targetId) return { ok: false, reason: 'gone' }\n const run: AiRunRecord = {\n result: data['result'],\n trigger: data['trigger'],\n summary: data['summary'],\n action: data['action'],\n createdAt: data['createdAt'],\n }\n return actionRunResult(run as never) === 'failed' ? { ok: true, run } : { ok: false, reason: 'not-failed' }\n}\n"],"names":["actionRunResult","datasetDisplayName","isFormArchived","campaignPlacedOnHost","scopedToHost","AI_WORKFLOW_RECORDS_WINDOW","AI_WORKFLOW_PIPELINES_WINDOW","text","value","trim","named","query","fields","keep","nameOf","snapshot","select","limit","get","docs","map","doc","data","id","filter","name","record","live","readForms","host","collection","Array","isArray","field","Boolean","readAiAutomationRecords","firestore","input","hostId","org","orgId","wanted","kind","only","includes","none","Promise","resolve","forms","datasets","lists","campaigns","workflows","webhooks","stages","all","visibleTo","crm","then","Number","flatMap","stage","readAiWorkflowFunctions","readAiWorkflowTarget","type","exists","action","workflow","readAiWorkflowRun","runId","target","targetId","ok","reason","run","result","trigger","summary","createdAt"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-workflow-records.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { HostAction } from '@aglyn/aglyn/app-utils/actions'\nimport { actionRunResult } from '@aglyn/aglyn/app-utils/activity-presenter'\nimport { isFormArchived } from '@aglyn/aglyn/app-utils/forms'\nimport {\n pluginRecordIndex,\n type PluginIndexedRecord,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-index'\nimport { visibleToHost } from '@aglyn/aglyn/app-utils/scope-tokens'\nimport { listOrgContainers } from '@aglyn/tenant-data-admin/server/org-containers'\nimport type {\n AiAutomationForm,\n AiAutomationNamedRecord,\n AiAutomationRecords,\n} from '../model/ai-automation-draft'\nimport type { AiIndexedWorkflow, AiRunRecord } from '../model/ai-automation-outline'\nimport type { AiWorkflowTargetType } from '../model/ai-workflow-job'\n\n/**\n * What a `workflow` job reads (AGL-2919), scoped to the job's own site and\n * org: the records a drafted automation's words are looked up among, the\n * automation an explanation reads, and the run it explains. Every read is\n * windowed the way the Actions editor's pickers are, so a lookup never reads\n * more than the editor would offer.\n *\n * The site's workflows, webhooks and saved actions are the workflows plugin's\n * records, and the org's datasets the data plugin's, so they are read through\n * the indexes those plugins publish (`workflow`, `webhook`, `action`,\n * `dataset` — AGL-3080), never from their collections: where no plugin keeps a\n * kind, there are none to name. The rest are read through the Admin SDK as\n * projections of the fields named here.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\ntype Data = Record<string, unknown>\n\n/** Records of one kind a lookup reads: the Actions editor's picker window. */\nexport const AI_WORKFLOW_RECORDS_WINDOW = 100\n\n/** Pipelines a stage lookup reads. */\nexport const AI_WORKFLOW_PIPELINES_WINDOW = 20\n\nfunction text(value: unknown): string {\n return typeof value === 'string' ? value.trim() : ''\n}\n\nasync function named(\n query: FirebaseFirestore.Query,\n fields: string[],\n keep: (data: Data) => boolean,\n nameOf: (data: Data, id: string) => string,\n): Promise<AiAutomationNamedRecord[]> {\n const snapshot = await query\n .select(...fields)\n .limit(AI_WORKFLOW_RECORDS_WINDOW)\n .get()\n return snapshot.docs\n .map((doc) => ({ data: (doc.data() ?? {}) as Data, id: doc.id }))\n .filter(({ data }) => keep(data))\n .map(({ data, id }) => ({ id, name: nameOf(data, id) }))\n .filter((record) => record.name)\n}\n\nconst live = (data: Data) => data['deletedAt'] == null\n\n/** The org's live campaigns placed on the site, named, in the editor's window. */\nasync function placedCampaigns(\n firestore: Firestore,\n orgId: string,\n hostId: string,\n): Promise<AiAutomationNamedRecord[]> {\n const containers = await listOrgContainers(firestore, 'campaign', orgId, AI_WORKFLOW_RECORDS_WINDOW)\n return containers\n .filter((container) => container.live && container.name && visibleToHost(container.visibleTo, hostId))\n .map((container) => ({ id: container.id, name: container.name }))\n}\n\n/**\n * The live records of a kind another plugin keeps, through its index, in the\n * editor's window; none where no plugin keeps the kind here.\n */\nasync function indexed(\n kind: string,\n scope: { orgId?: string; hostId: string },\n keep: (record: PluginIndexedRecord) => boolean = () => true,\n): Promise<AiAutomationNamedRecord[]> {\n const owner = pluginRecordIndex(kind)\n if (!owner) return []\n const { records } = await owner.index.list({ ...scope, limit: AI_WORKFLOW_RECORDS_WINDOW })\n return records.filter(keep).map(({ id, name }) => ({ id, name }))\n}\n\n/** The site's forms that are not archived, with the field names their submissions carry. */\nasync function readForms(host: FirebaseFirestore.DocumentReference): Promise<AiAutomationForm[]> {\n const snapshot = await host\n .collection('forms')\n .select('displayName', 'slug', 'fields', 'archivedAt')\n .limit(AI_WORKFLOW_RECORDS_WINDOW)\n .get()\n return snapshot.docs\n .filter((doc) => !isFormArchived((doc.data() ?? {}) as { archivedAt?: unknown }))\n .map((doc) => {\n const data = (doc.data() ?? {}) as Data\n return {\n id: doc.id,\n name: text(data['displayName']) || text(data['slug']) || doc.id,\n fields: (Array.isArray(data['fields']) ? (data['fields'] as Data[]) : [])\n .map((field) => text(field?.['fieldName']))\n .filter(Boolean),\n }\n })\n}\n\n/**\n * The site's records a drafted automation may name. Stages are read only for\n * a workspace with the CRM, whose pipelines they are; a site without it has\n * no stage a condition could name. `only`, when given, names the kinds to\n * read, and every other kind is answered empty without a read.\n */\nexport async function readAiAutomationRecords(\n firestore: Firestore,\n input: { orgId: string; hostId: string; crm: boolean; only?: ReadonlyArray<keyof AiAutomationRecords> },\n): Promise<AiAutomationRecords> {\n const host = firestore.collection('hosts').doc(input.hostId)\n const org = firestore.collection('orgs').doc(input.orgId)\n const wanted = (kind: keyof AiAutomationRecords) => !input.only || input.only.includes(kind)\n const none = Promise.resolve([] as AiAutomationNamedRecord[])\n const [forms, datasets, lists, campaigns, workflows, webhooks, stages] = await Promise.all([\n !wanted('forms') ? Promise.resolve([] as AiAutomationForm[]) : readForms(host),\n // The org's datasets shared with this site, as the data plugin indexes them.\n !wanted('datasets') ? none : indexed('dataset', { orgId: input.orgId, hostId: input.hostId }),\n !wanted('lists') ? none : named(org.collection('lists'), ['name', 'deletedAt'], live, (data) => text(data['name'])),\n // The org's campaigns placed on this site: the set the automation's\n // `assignCampaign` step accepts when it runs here. Read as the `campaign`\n // container kind, where its declaration says the containers are stored.\n !wanted('campaigns') ? none : placedCampaigns(firestore, input.orgId, input.hostId),\n !wanted('workflows') ? none : indexed('workflow', { hostId: input.hostId }),\n // Only a webhook that posts OUT is a step's target; an inbound one is an\n // endpoint that runs a workflow.\n !wanted('webhooks')\n ? none\n : indexed('webhook', { hostId: input.hostId }, (record) => record.facts['direction'] === 'outbound'),\n input.crm && wanted('stages') ? readStages(input.orgId, input.hostId) : none,\n ])\n return { forms, datasets, lists, campaigns, workflows, webhooks, stages }\n}\n\n/**\n * The stages of the pipelines the site can see, asked of the plugin that keeps\n * pipelines (its `pipeline` record index), never read from its collection. No\n * index means no plugin keeps pipelines in this process: no stage to name.\n */\nasync function readStages(orgId: string, hostId: string): Promise<AiAutomationNamedRecord[]> {\n const pipelines = pluginRecordIndex('pipeline')\n if (!pipelines) return []\n const { records } = await pipelines.index.list({ orgId, hostId, limit: AI_WORKFLOW_PIPELINES_WINDOW })\n return records\n .flatMap((record) => (Array.isArray(record.facts['stages']) ? (record.facts['stages'] as Data[]) : []))\n .map((stage) => ({ id: text(stage?.['id']), name: text(stage?.['name']) }))\n .filter((stage) => stage.id && stage.name)\n}\n\n/** The site's functions, for a workflow outline that says which of its calls still resolve. */\nexport async function readAiWorkflowFunctions(\n firestore: Firestore,\n hostId: string,\n): Promise<AiAutomationNamedRecord[]> {\n return named(\n firestore.collection('hosts').doc(hostId).collection('functions'),\n ['name', 'deletedAt'],\n live,\n (data) => text(data['name']),\n )\n}\n\nexport type AiWorkflowTarget =\n | { type: 'action'; id: string; name: string; action: HostAction }\n | { type: 'workflow'; id: string; name: string; workflow: AiIndexedWorkflow }\n\n/**\n * A saved automation of the site, read through the index of the plugin that\n * keeps it, or `null` for one that does not exist, was deleted, or that no\n * plugin here keeps.\n */\nexport async function readAiWorkflowTarget(\n input: { hostId: string; type: AiWorkflowTargetType; id: string },\n): Promise<AiWorkflowTarget | null> {\n const owner = pluginRecordIndex(input.type)\n const record = owner ? await owner.index.get({ hostId: input.hostId, id: input.id }) : null\n if (!record) return null\n const stored = { name: record.name, ...record.facts }\n return input.type === 'action'\n ? { type: 'action', id: record.id, name: record.name, action: stored as unknown as HostAction }\n : { type: 'workflow', id: record.id, name: record.name, workflow: stored as unknown as AiIndexedWorkflow }\n}\n\nexport type AiWorkflowRunRead =\n | { ok: true; run: AiRunRecord }\n | { ok: false; reason: 'gone' | 'not-failed' }\n\n/**\n * One run of an automation, as the site's activity log recorded it: gone when\n * there is no such entry or it belongs to another automation, and refused\n * when it did not fail. Only what the run history shows is read — never the\n * event's payload.\n */\nexport async function readAiWorkflowRun(\n firestore: Firestore,\n input: { hostId: string; targetId: string; runId: string },\n): Promise<AiWorkflowRunRead> {\n const snapshot = await firestore\n .collection('hosts')\n .doc(input.hostId)\n .collection('activity')\n .doc(input.runId)\n .get()\n const data = snapshot.exists ? ((snapshot.data() ?? {}) as Data) : null\n const target = data?.['target'] as { id?: unknown } | undefined\n if (!data || target?.id !== input.targetId) return { ok: false, reason: 'gone' }\n const run: AiRunRecord = {\n result: data['result'],\n trigger: data['trigger'],\n summary: data['summary'],\n action: data['action'],\n createdAt: data['createdAt'],\n }\n return actionRunResult(run as never) === 'failed' ? { ok: true, run } : { ok: false, reason: 'not-failed' }\n}\n"],"names":["actionRunResult","isFormArchived","pluginRecordIndex","visibleToHost","listOrgContainers","AI_WORKFLOW_RECORDS_WINDOW","AI_WORKFLOW_PIPELINES_WINDOW","text","value","trim","named","query","fields","keep","nameOf","snapshot","select","limit","get","docs","map","doc","data","id","filter","name","record","live","placedCampaigns","firestore","orgId","hostId","containers","container","visibleTo","indexed","kind","scope","owner","records","index","list","readForms","host","collection","Array","isArray","field","Boolean","readAiAutomationRecords","input","org","wanted","only","includes","none","Promise","resolve","forms","datasets","lists","campaigns","workflows","webhooks","stages","all","facts","crm","readStages","pipelines","flatMap","stage","readAiWorkflowFunctions","readAiWorkflowTarget","type","stored","action","workflow","readAiWorkflowRun","runId","exists","target","targetId","ok","reason","run","result","trigger","summary","createdAt"],"mappings":"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"}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import { type AssistRefusedBy } from '
|
|
17
|
+
import { type AssistRefusedBy } from '../usage/assist-credits';
|
|
18
18
|
/**
|
|
19
19
|
* AI ALLOTMENTS (AGL-2942): the pure half.
|
|
20
20
|
*
|
|
@@ -45,7 +45,7 @@ import { type AssistRefusedBy } from '@aglyn/aglyn/app-utils/assist-credits';
|
|
|
45
45
|
*
|
|
46
46
|
* A HARD allotment refuses the request that finds the subject at or past it.
|
|
47
47
|
* A SOFT one admits it and tells the subject and the workspace's billing
|
|
48
|
-
* contacts at 80
|
|
48
|
+
* contacts at 75, 80, 90 and 100%. Both are measured against what the
|
|
49
49
|
* subject already drew this month, before the request — the same shape the
|
|
50
50
|
* band is measured in, so a request that starts under the line may finish
|
|
51
51
|
* past it, and the next one is the one refused.
|
|
@@ -68,8 +68,13 @@ export type AiCreditAllotmentScope = Exclude<AiAllotmentScope, 'org'>;
|
|
|
68
68
|
* is the plugin's rung: core names the ceilings core's billing sells.
|
|
69
69
|
*/
|
|
70
70
|
export type AiRefusedBy = AssistRefusedBy | 'allotment';
|
|
71
|
-
/**
|
|
72
|
-
|
|
71
|
+
/**
|
|
72
|
+
* The steps a soft allotment announces — the same 75, 80, 90 and 100 every
|
|
73
|
+
* usage quota steps through (AGL-3431). Only the highest step reached is
|
|
74
|
+
* sent, each once a month.
|
|
75
|
+
*/
|
|
76
|
+
export declare const AI_ALLOTMENT_ALERT_BANDS: readonly [75, 80, 90, 100];
|
|
77
|
+
export type AiAllotmentThreshold = (typeof AI_ALLOTMENT_ALERT_BANDS)[number];
|
|
73
78
|
/** The share of an allotment at which the strip warns and a soft one alerts. */
|
|
74
79
|
export declare const AI_ALLOTMENT_WARN_SHARE = 0.8;
|
|
75
80
|
/**
|
|
@@ -152,7 +157,10 @@ export interface AiAllotmentStanding {
|
|
|
152
157
|
export type AiAllotmentState = 'ok' | 'warn' | 'reached';
|
|
153
158
|
/** `reached` at or past the allotment, `warn` from 80% of it. */
|
|
154
159
|
export declare function aiAllotmentState(used: number, credits: number): AiAllotmentState;
|
|
155
|
-
/**
|
|
160
|
+
/**
|
|
161
|
+
* The highest step a standing has reached, for the alert pipeline — compared
|
|
162
|
+
* as `used × 100 ≥ step × credits`, so 9 of 10 is exactly 90%.
|
|
163
|
+
*/
|
|
156
164
|
export declare function aiAllotmentThreshold(used: number, credits: number): AiAllotmentThreshold | null;
|
|
157
165
|
/**
|
|
158
166
|
* Whether a standing admits a request.
|
|
@@ -194,14 +202,27 @@ export declare function aiAllotmentRefusalText(scope: AiCreditAllotmentScope | n
|
|
|
194
202
|
/**
|
|
195
203
|
* The words a soft allotment's alert carries, for BOTH channels — the
|
|
196
204
|
* console notification and the email — so the two cannot say different
|
|
197
|
-
* things about one crossing.
|
|
205
|
+
* things about one crossing.
|
|
206
|
+
*
|
|
207
|
+
* The email is plain text with no heading, so the body opens by saying whose
|
|
208
|
+
* AI credits, how many of how many, and in which workspace (and on which site,
|
|
209
|
+
* for a collaborator): a figure on its own reads as a bill. The managers'
|
|
210
|
+
* copy says how to stop the subject at the line; the person's own copy does
|
|
211
|
+
* not, because they cannot open Billing.
|
|
198
212
|
*/
|
|
199
213
|
export declare function aiAllotmentAlertCopy(input: {
|
|
200
214
|
scope: AiCreditAllotmentScope;
|
|
201
215
|
threshold: AiAllotmentThreshold;
|
|
202
216
|
used: number;
|
|
203
217
|
credits: number;
|
|
218
|
+
/** The member's name, or the site's for a site allotment. */
|
|
204
219
|
name: string;
|
|
220
|
+
/** The workspace the allotment is drawn from. */
|
|
221
|
+
workspace: string;
|
|
222
|
+
/** The site a collaborator's allotment is on. */
|
|
223
|
+
site?: string;
|
|
224
|
+
/** The workspace's managers (the default), or the person it is for. */
|
|
225
|
+
reader?: 'managers' | 'subject';
|
|
205
226
|
}): {
|
|
206
227
|
title: string;
|
|
207
228
|
body: string;
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
|
-
*/ import { ASSIST_HARD_CAP_CONTROL_LOCATION } from "
|
|
16
|
+
*/ import { ASSIST_HARD_CAP_CONTROL_LOCATION } from "../usage/assist-credits.js";
|
|
17
17
|
import { ORG_PERMISSIONS } from "@aglyn/aglyn/app-utils/org-permissions";
|
|
18
18
|
/**
|
|
19
19
|
* AI ALLOTMENTS (AGL-2942): the pure half.
|
|
@@ -45,7 +45,7 @@ import { ORG_PERMISSIONS } from "@aglyn/aglyn/app-utils/org-permissions";
|
|
|
45
45
|
*
|
|
46
46
|
* A HARD allotment refuses the request that finds the subject at or past it.
|
|
47
47
|
* A SOFT one admits it and tells the subject and the workspace's billing
|
|
48
|
-
* contacts at 80
|
|
48
|
+
* contacts at 75, 80, 90 and 100%. Both are measured against what the
|
|
49
49
|
* subject already drew this month, before the request — the same shape the
|
|
50
50
|
* band is measured in, so a request that starts under the line may finish
|
|
51
51
|
* past it, and the next one is the one refused.
|
|
@@ -54,6 +54,16 @@ import { ORG_PERMISSIONS } from "@aglyn/aglyn/app-utils/org-permissions";
|
|
|
54
54
|
* cards read one definition of a subject, a standing and a refusal sentence.
|
|
55
55
|
*/ /** `orgs/{orgId}/{this}/{subject}`. */ export const AI_ALLOTMENTS_COLLECTION = 'aiAllotments';
|
|
56
56
|
/** The subject id of the org-wide model restriction. */ export const AI_ALLOTMENT_ORG_SUBJECT = 'org';
|
|
57
|
+
/**
|
|
58
|
+
* The steps a soft allotment announces — the same 75, 80, 90 and 100 every
|
|
59
|
+
* usage quota steps through (AGL-3431). Only the highest step reached is
|
|
60
|
+
* sent, each once a month.
|
|
61
|
+
*/ export const AI_ALLOTMENT_ALERT_BANDS = [
|
|
62
|
+
75,
|
|
63
|
+
80,
|
|
64
|
+
90,
|
|
65
|
+
100
|
|
66
|
+
];
|
|
57
67
|
/** The share of an allotment at which the strip warns and a soft one alerts. */ export const AI_ALLOTMENT_WARN_SHARE = 0.8;
|
|
58
68
|
/**
|
|
59
69
|
* The largest monthly allotment a write may carry. A bound on a typing
|
|
@@ -162,9 +172,16 @@ const ID_SEGMENT = /^[A-Za-z0-9_-]{1,128}$/;
|
|
|
162
172
|
if (used >= credits * AI_ALLOTMENT_WARN_SHARE) return 'warn';
|
|
163
173
|
return 'ok';
|
|
164
174
|
}
|
|
165
|
-
/**
|
|
166
|
-
|
|
167
|
-
|
|
175
|
+
/**
|
|
176
|
+
* The highest step a standing has reached, for the alert pipeline — compared
|
|
177
|
+
* as `used × 100 ≥ step × credits`, so 9 of 10 is exactly 90%.
|
|
178
|
+
*/ export function aiAllotmentThreshold(used, credits) {
|
|
179
|
+
if (!(credits > 0) || !Number.isFinite(used) || used < 0) return null;
|
|
180
|
+
for(let index = AI_ALLOTMENT_ALERT_BANDS.length - 1; index >= 0; index -= 1){
|
|
181
|
+
const band = AI_ALLOTMENT_ALERT_BANDS[index];
|
|
182
|
+
if (used * 100 >= band * credits) return band;
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
168
185
|
}
|
|
169
186
|
/**
|
|
170
187
|
* Whether a standing admits a request.
|
|
@@ -248,21 +265,44 @@ const scopeRank = (scope)=>SCOPE_ORDER.indexOf(scope);
|
|
|
248
265
|
/**
|
|
249
266
|
* The words a soft allotment's alert carries, for BOTH channels — the
|
|
250
267
|
* console notification and the email — so the two cannot say different
|
|
251
|
-
* things about one crossing.
|
|
268
|
+
* things about one crossing.
|
|
269
|
+
*
|
|
270
|
+
* The email is plain text with no heading, so the body opens by saying whose
|
|
271
|
+
* AI credits, how many of how many, and in which workspace (and on which site,
|
|
272
|
+
* for a collaborator): a figure on its own reads as a bill. The managers'
|
|
273
|
+
* copy says how to stop the subject at the line; the person's own copy does
|
|
274
|
+
* not, because they cannot open Billing.
|
|
252
275
|
*/ export function aiAllotmentAlertCopy(input) {
|
|
253
|
-
const
|
|
254
|
-
const
|
|
255
|
-
const
|
|
256
|
-
const
|
|
257
|
-
|
|
276
|
+
const { scope, threshold, name, workspace } = input;
|
|
277
|
+
const used = input.used.toLocaleString('en-US');
|
|
278
|
+
const credits = input.credits.toLocaleString('en-US');
|
|
279
|
+
const reached = threshold >= 100;
|
|
280
|
+
const onSite = scope === 'collab' && input.site ? ` on the ${input.site} site` : '';
|
|
281
|
+
const amount = reached ? `all ${credits} AI credits` : `${used} of the ${credits} AI credits`;
|
|
282
|
+
const tail = reached ? ` this month (${used} so far).` : ` this month, past the ${threshold}% mark.`;
|
|
283
|
+
if (input.reader === 'subject' && scope !== 'host') {
|
|
284
|
+
const where = scope === 'collab' ? onSite : ` in ${workspace}`;
|
|
285
|
+
const whoCanChange = scope === 'collab' ? 'The site’s admin or a workspace admin can raise or change it.' : 'A workspace admin can raise or change it.';
|
|
286
|
+
return {
|
|
287
|
+
title: reached ? `You’ve used your whole AI allotment${where}` : `You’re past ${threshold}% of your AI allotment${where}`,
|
|
288
|
+
body: `You’ve used ${amount} ${workspace} allotted you${onSite}${tail} ` + 'It’s a soft allotment, so it does not stop your AI requests.' + (reached ? ` ${whoCanChange}` : '')
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
const who = scope === 'host' ? `The ${name} site` : name;
|
|
292
|
+
const whose = scope === 'host' ? 'its' : 'their';
|
|
293
|
+
const them = scope === 'host' ? 'it' : 'them';
|
|
294
|
+
const requests = scope === 'host' ? 'AI requests on the site' : `${name}’s AI requests`;
|
|
295
|
+
const where = scope === 'collab' ? onSite : scope === 'member' ? ` in ${workspace}` : '';
|
|
296
|
+
const opening = `${who} has used ${amount} allotted to ${them}${onSite} in ${workspace}${tail}`;
|
|
297
|
+
if (reached) {
|
|
258
298
|
return {
|
|
259
299
|
title: `${who} has used ${whose} whole AI allotment${where}`,
|
|
260
|
-
body: `${
|
|
300
|
+
body: `${opening} It’s a soft allotment, so it does not stop ${requests}; ` + 'they keep drawing on the workspace’s AI credits, and crossing it ' + `charges nothing by itself. To stop ${them} at the line, make it hard ` + `under ${ASSIST_HARD_CAP_CONTROL_LOCATION}; to give ${them} more, ` + 'raise it there.'
|
|
261
301
|
};
|
|
262
302
|
}
|
|
263
303
|
return {
|
|
264
|
-
title: `${who} is past ${
|
|
265
|
-
body: `${
|
|
304
|
+
title: `${who} is past ${threshold}% of ${whose} AI allotment${where}`,
|
|
305
|
+
body: `${opening} It’s a soft allotment, so it does not stop ${requests}; ` + 'they draw on the workspace’s AI credits like everyone else’s.'
|
|
266
306
|
};
|
|
267
307
|
}
|
|
268
308
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-allotments.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n ASSIST_HARD_CAP_CONTROL_LOCATION,\n type AssistRefusedBy,\n} from '@aglyn/aglyn/app-utils/assist-credits'\nimport { ORG_PERMISSIONS } from '@aglyn/aglyn/app-utils/org-permissions'\n\n/**\n * AI ALLOTMENTS (AGL-2942): the pure half.\n *\n * The workspace's AI pool is one band. An allotment is a manager's decision\n * about how much of that band one SUBJECT may draw in a calendar month (UTC),\n * so one client site, one contractor or one teammate cannot spend what the\n * others were counting on:\n *\n * orgs/{orgId}/aiAllotments/{subject}\n * { subject, scope, uid, hostId, credits, mode, models, setBy,\n * updatedAt, alerted }\n *\n * where `subject` is one of\n *\n * member:{uid} a team member, across every site\n * collab:{hostId}:{uid} a site collaborator, on that site\n * host:{hostId} a whole site, across everyone who works on it\n * org the org-wide model restriction (no credits)\n *\n * ## An allotment never grants\n *\n * The org band stays the outer wall: the reservation decides the workspace's\n * own ceilings first and consults an allotment only once they admitted the\n * request. So an allotment larger than the band is not an error and not a\n * grant — it is a line the workspace reaches after the band.\n *\n * ## Hard and soft\n *\n * A HARD allotment refuses the request that finds the subject at or past it.\n * A SOFT one admits it and tells the subject and the workspace's billing\n * contacts at 80% and again at 100%. Both are measured against what the\n * subject already drew this month, before the request — the same shape the\n * band is measured in, so a request that starts under the line may finish\n * past it, and the next one is the one refused.\n *\n * Everything here is dependency-light so the gate, the routes and the console\n * cards read one definition of a subject, a standing and a refusal sentence.\n */\n\n/** `orgs/{orgId}/{this}/{subject}`. */\nexport const AI_ALLOTMENTS_COLLECTION = 'aiAllotments'\n\n/** The subject id of the org-wide model restriction. */\nexport const AI_ALLOTMENT_ORG_SUBJECT = 'org'\n\nexport type AiAllotmentMode = 'hard' | 'soft'\n\nexport type AiAllotmentScope = 'member' | 'collab' | 'host' | 'org'\n\n/** The scopes an allotment of CREDITS can have; `org` carries models only. */\nexport type AiCreditAllotmentScope = Exclude<AiAllotmentScope, 'org'>\n\n/**\n * Why a reservation refused: the meter's own ceilings, and an allotment.\n *\n * Widened here rather than in core's `AssistRefusedBy`, because an allotment\n * is the plugin's rung: core names the ceilings core's billing sells.\n */\nexport type AiRefusedBy = AssistRefusedBy | 'allotment'\n\n/** The two thresholds a soft allotment announces. */\nexport type AiAllotmentThreshold = 80 | 100\n\n/** The share of an allotment at which the strip warns and a soft one alerts. */\nexport const AI_ALLOTMENT_WARN_SHARE = 0.8\n\n/**\n * The largest monthly allotment a write may carry. A bound on a typing\n * mistake, not a product limit: a billion credits is a million dollars of\n * provider spend, far past any band the platform sells.\n */\nexport const AI_ALLOTMENT_MAX_CREDITS = 1_000_000_000\n\n/** The most catalog ids one allowlist may name. */\nexport const AI_ALLOTMENT_MAX_MODELS = 20\n\nconst ID_SEGMENT = /^[A-Za-z0-9_-]{1,128}$/\n\nexport interface AiAllotmentSubject {\n id: string\n scope: AiAllotmentScope\n uid: string | null\n hostId: string | null\n}\n\nexport type AiAllotmentSubjectInput =\n | { scope: 'member'; uid: string }\n | { scope: 'collab'; hostId: string; uid: string }\n | { scope: 'host'; hostId: string }\n | { scope: 'org' }\n\n/** The document id for a subject. Throws on a segment that is not an id. */\nexport function aiAllotmentSubjectId(input: AiAllotmentSubjectInput): string {\n const segment = (value: string): string => {\n if (!ID_SEGMENT.test(value)) throw new Error(`not an id segment: ${value}`)\n return value\n }\n switch (input.scope) {\n case 'member':\n return `member:${segment(input.uid)}`\n case 'collab':\n return `collab:${segment(input.hostId)}:${segment(input.uid)}`\n case 'host':\n return `host:${segment(input.hostId)}`\n default:\n return AI_ALLOTMENT_ORG_SUBJECT\n }\n}\n\n/** A subject id read back, or `null` for anything that is not one. */\nexport function parseAiAllotmentSubject(value: unknown): AiAllotmentSubject | null {\n if (typeof value !== 'string') return null\n if (value === AI_ALLOTMENT_ORG_SUBJECT) {\n return { id: value, scope: 'org', uid: null, hostId: null }\n }\n const parts = value.split(':')\n if (parts.some((part) => !ID_SEGMENT.test(part))) return null\n if (parts.length === 2 && parts[0] === 'member') {\n return { id: value, scope: 'member', uid: parts[1], hostId: null }\n }\n if (parts.length === 2 && parts[0] === 'host') {\n return { id: value, scope: 'host', uid: null, hostId: parts[1] }\n }\n if (parts.length === 3 && parts[0] === 'collab') {\n return { id: value, scope: 'collab', uid: parts[2], hostId: parts[1] }\n }\n return null\n}\n\n/** One allotment as every reader hands it on. */\nexport interface AiAllotment {\n subject: string\n scope: AiAllotmentScope\n uid: string | null\n hostId: string | null\n /**\n * Credits a month, or `null` for an allotment that bounds models alone —\n * and always `null` on the org-wide restriction.\n */\n credits: number | null\n mode: AiAllotmentMode\n /** Catalog ids the subject may run on, or `null` for no restriction. */\n models: string[] | null\n setBy: string | null\n /** The last threshold announced, and the month it was announced in. */\n alerted: { month: string; threshold: AiAllotmentThreshold } | null\n}\n\n/** A finite positive whole number of credits, else `null`. */\nexport function aiAllotmentCredits(value: unknown): number | null {\n const parsed = typeof value === 'number' ? value : Number.NaN\n if (!Number.isFinite(parsed) || parsed <= 0) return null\n return Math.min(AI_ALLOTMENT_MAX_CREDITS, Math.floor(parsed))\n}\n\n/** An allowlist read back: unique strings, bounded; empty reads as none. */\nexport function aiAllotmentModels(value: unknown): string[] | null {\n if (!Array.isArray(value)) return null\n const models = [\n ...new Set(\n value\n .filter((entry): entry is string => typeof entry === 'string')\n .map((entry) => entry.trim())\n .filter(Boolean),\n ),\n ].slice(0, AI_ALLOTMENT_MAX_MODELS)\n return models.length ? models : null\n}\n\n/**\n * A stored allotment as a complete record, or `null` when the id is not a\n * subject. The scope, uid and site are read off the ID, never off the\n * fields: the id is what the gate addresses, so it is the only claim about\n * who the document is for that can be trusted.\n *\n * `mode` defaults to `hard`: a document whose mode was lost is a cap, which\n * is the reading that cannot spend more than a manager set.\n */\nexport function aiAllotmentFrom(\n raw: Record<string, unknown> | null | undefined,\n id: string,\n): AiAllotment | null {\n const subject = parseAiAllotmentSubject(id)\n if (!subject || !raw) return null\n const alertedRaw = raw['alerted'] as { month?: unknown; threshold?: unknown } | null\n const threshold = Number(alertedRaw?.threshold)\n return {\n subject: subject.id,\n scope: subject.scope,\n uid: subject.uid,\n hostId: subject.hostId,\n credits: subject.scope === 'org' ? null : aiAllotmentCredits(raw['credits']),\n mode: raw['mode'] === 'soft' ? 'soft' : 'hard',\n models: aiAllotmentModels(raw['models']),\n setBy: typeof raw['setBy'] === 'string' ? (raw['setBy'] as string) : null,\n alerted:\n typeof alertedRaw?.month === 'string' && (threshold === 80 || threshold === 100)\n ? { month: alertedRaw.month, threshold }\n : null,\n }\n}\n\n/** One allotment of credits measured against what its subject drew. */\nexport interface AiAllotmentStanding {\n subject: string\n scope: AiCreditAllotmentScope\n uid: string | null\n hostId: string | null\n credits: number\n /** Credits drawn this month before the request being decided. */\n used: number\n mode: AiAllotmentMode\n}\n\nexport type AiAllotmentState = 'ok' | 'warn' | 'reached'\n\n/** `reached` at or past the allotment, `warn` from 80% of it. */\nexport function aiAllotmentState(used: number, credits: number): AiAllotmentState {\n if (!(credits > 0)) return 'ok'\n if (used >= credits) return 'reached'\n if (used >= credits * AI_ALLOTMENT_WARN_SHARE) return 'warn'\n return 'ok'\n}\n\n/** The threshold a standing sits at, for the alert pipeline. */\nexport function aiAllotmentThreshold(\n used: number,\n credits: number,\n): AiAllotmentThreshold | null {\n const state = aiAllotmentState(used, credits)\n return state === 'reached' ? 100 : state === 'warn' ? 80 : null\n}\n\n/**\n * Whether a standing admits a request.\n *\n * A soft allotment always does. A hard one admits while the subject is under\n * it — and, for a caller that knows what the request will cost before it\n * starts (a job's estimate), while the estimate still fits: a 100-credit job\n * at 0 of 100 is admitted, one at 1 of 100 is not.\n */\nexport function aiAllotmentAdmits(\n standing: Pick<AiAllotmentStanding, 'credits' | 'used' | 'mode'>,\n estimateCredits = 0,\n): boolean {\n if (standing.mode === 'soft') return true\n const estimate = Number.isFinite(estimateCredits) && estimateCredits > 0 ? estimateCredits : 0\n return estimate > 0\n ? standing.used + estimate <= standing.credits\n : standing.used < standing.credits\n}\n\n/**\n * Which subject a refusal names when more than one allotment is spent: the\n * narrowest first — the person on this site, then the person, then the site.\n */\nconst SCOPE_ORDER: readonly AiCreditAllotmentScope[] = ['collab', 'member', 'host']\n\nconst scopeRank = (scope: AiCreditAllotmentScope): number => SCOPE_ORDER.indexOf(scope)\n\nexport interface AiAllotmentVerdict {\n /** The hard allotment that refuses the request, or `null`. */\n refusal: AiAllotmentStanding | null\n /** The allotment with the least left, for the usage strip; `null` for none. */\n binding: AiAllotmentStanding | null\n /** Soft allotments at a threshold, for the alert pipeline. */\n alerts: Array<{ standing: AiAllotmentStanding; threshold: AiAllotmentThreshold }>\n}\n\n/** The verdict over every allotment that applies to one request. */\nexport function aiAllotmentVerdict(\n standings: readonly AiAllotmentStanding[],\n estimateCredits = 0,\n): AiAllotmentVerdict {\n const ordered = [...standings].sort((a, b) => scopeRank(a.scope) - scopeRank(b.scope))\n const refusal =\n ordered.find((standing) => !aiAllotmentAdmits(standing, estimateCredits)) ?? null\n let binding: AiAllotmentStanding | null = null\n for (const standing of ordered) {\n if (!binding || standing.credits - standing.used < binding.credits - binding.used) {\n binding = standing\n }\n }\n const alerts = ordered.flatMap((standing) => {\n if (standing.mode !== 'soft') return []\n const threshold = aiAllotmentThreshold(standing.used, standing.credits)\n return threshold ? [{ standing, threshold }] : []\n })\n return { refusal, binding, alerts }\n}\n\n/**\n * The allowlists that apply to a request, intersected — `null` when none of\n * the allotments names one. An empty result is a real answer: two lists that\n * share no model leave nothing to pick, and the model choice falls back to\n * the routing table rather than turning AI off for a mistake in a list.\n */\nexport function aiAllotmentModelsAllowed(\n allotments: ReadonlyArray<Pick<AiAllotment, 'models'> | null | undefined>,\n): string[] | null {\n const lists = allotments\n .map((allotment) => allotment?.models)\n .filter((models): models is string[] => Array.isArray(models) && models.length > 0)\n if (!lists.length) return null\n return lists.reduce((kept, list) => kept.filter((id) => list.includes(id)))\n}\n\n/** The catalog label for `billing.manage`, the word a sentence names it by. */\nexport function billingManageLabel(): string {\n return (\n ORG_PERMISSIONS.find((definition) => definition.key === 'billing.manage')?.label ??\n 'Manage billing'\n )\n}\n\n/**\n * The sentence a refused request is told, naming who can raise the\n * allotment and where. No figures: the strip beside it carries those, and a\n * refusal that quoted a count would be stale the moment someone raised it.\n */\nexport function aiAllotmentRefusalText(\n scope: AiCreditAllotmentScope | null | undefined,\n): string {\n const managers = `an organization admin with the \"${billingManageLabel()}\" permission`\n switch (scope) {\n case 'collab':\n return (\n 'You have used your AI allotment on this site for this month. ' +\n `The site's admin, or ${managers}, can raise it on the site's Users card.`\n )\n case 'host':\n return (\n 'This site has used its AI allotment for this month. ' +\n `Ask ${managers} to raise it under ${ASSIST_HARD_CAP_CONTROL_LOCATION}.`\n )\n default:\n return (\n 'You have used your AI allotment for this month. ' +\n `Ask ${managers} to raise it under ${ASSIST_HARD_CAP_CONTROL_LOCATION}.`\n )\n }\n}\n\n/**\n * The words a soft allotment's alert carries, for BOTH channels — the\n * console notification and the email — so the two cannot say different\n * things about one crossing. `name` is the member's name or the site's.\n */\nexport function aiAllotmentAlertCopy(input: {\n scope: AiCreditAllotmentScope\n threshold: AiAllotmentThreshold\n used: number\n credits: number\n name: string\n}): { title: string; body: string } {\n const who =\n input.scope === 'host' ? `The ${input.name} site` : input.name\n const whose = input.scope === 'host' ? 'its' : 'their'\n const where = input.scope === 'collab' ? ' on its site' : ''\n const figures =\n `${input.used.toLocaleString('en-US')} of ` +\n `${input.credits.toLocaleString('en-US')} credits used this month.`\n if (input.threshold >= 100) {\n return {\n title: `${who} has used ${whose} whole AI allotment${where}`,\n body:\n `${figures} The allotment is soft, so requests keep working and ` +\n 'draw on the workspace’s pool. Raise it or make it hard under ' +\n `${ASSIST_HARD_CAP_CONTROL_LOCATION}.`,\n }\n }\n return {\n title: `${who} is past ${Math.round(AI_ALLOTMENT_WARN_SHARE * 100)}% of ${whose} AI allotment${where}`,\n body: `${figures} The allotment is soft, so nothing stops at the line.`,\n }\n}\n"],"names":["ASSIST_HARD_CAP_CONTROL_LOCATION","ORG_PERMISSIONS","AI_ALLOTMENTS_COLLECTION","AI_ALLOTMENT_ORG_SUBJECT","AI_ALLOTMENT_WARN_SHARE","AI_ALLOTMENT_MAX_CREDITS","AI_ALLOTMENT_MAX_MODELS","ID_SEGMENT","aiAllotmentSubjectId","input","segment","value","test","Error","scope","uid","hostId","parseAiAllotmentSubject","id","parts","split","some","part","length","aiAllotmentCredits","parsed","Number","NaN","isFinite","Math","min","floor","aiAllotmentModels","Array","isArray","models","Set","filter","entry","map","trim","Boolean","slice","aiAllotmentFrom","raw","subject","alertedRaw","threshold","credits","mode","setBy","alerted","month","aiAllotmentState","used","aiAllotmentThreshold","state","aiAllotmentAdmits","standing","estimateCredits","estimate","SCOPE_ORDER","scopeRank","indexOf","aiAllotmentVerdict","standings","ordered","sort","a","b","refusal","find","binding","alerts","flatMap","aiAllotmentModelsAllowed","allotments","lists","allotment","reduce","kept","list","includes","billingManageLabel","definition","key","label","aiAllotmentRefusalText","managers","aiAllotmentAlertCopy","who","name","whose","where","figures","toLocaleString","title","body","round"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,gCAAgC,QAE3B,wCAAuC;AAC9C,SAASC,eAAe,QAAQ,yCAAwC;AAExE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqCC,GAED,qCAAqC,GACrC,OAAO,MAAMC,2BAA2B,eAAc;AAEtD,sDAAsD,GACtD,OAAO,MAAMC,2BAA2B,MAAK;AAoB7C,8EAA8E,GAC9E,OAAO,MAAMC,0BAA0B,IAAG;AAE1C;;;;CAIC,GACD,OAAO,MAAMC,2BAA2B,WAAa;AAErD,iDAAiD,GACjD,OAAO,MAAMC,0BAA0B,GAAE;AAEzC,MAAMC,aAAa;AAenB,0EAA0E,GAC1E,OAAO,SAASC,qBAAqBC,KAA8B;IACjE,MAAMC,UAAU,CAACC;QACf,IAAI,CAACJ,WAAWK,IAAI,CAACD,QAAQ,MAAM,IAAIE,MAAM,CAAC,mBAAmB,EAAEF,OAAO;QAC1E,OAAOA;IACT;IACA,OAAQF,MAAMK,KAAK;QACjB,KAAK;YACH,OAAO,CAAC,OAAO,EAAEJ,QAAQD,MAAMM,GAAG,GAAG;QACvC,KAAK;YACH,OAAO,CAAC,OAAO,EAAEL,QAAQD,MAAMO,MAAM,EAAE,CAAC,EAAEN,QAAQD,MAAMM,GAAG,GAAG;QAChE,KAAK;YACH,OAAO,CAAC,KAAK,EAAEL,QAAQD,MAAMO,MAAM,GAAG;QACxC;YACE,OAAOb;IACX;AACF;AAEA,oEAAoE,GACpE,OAAO,SAASc,wBAAwBN,KAAc;IACpD,IAAI,OAAOA,UAAU,UAAU,OAAO;IACtC,IAAIA,UAAUR,0BAA0B;QACtC,OAAO;YAAEe,IAAIP;YAAOG,OAAO;YAAOC,KAAK;YAAMC,QAAQ;QAAK;IAC5D;IACA,MAAMG,QAAQR,MAAMS,KAAK,CAAC;IAC1B,IAAID,MAAME,IAAI,CAAC,CAACC,OAAS,CAACf,WAAWK,IAAI,CAACU,QAAQ,OAAO;IACzD,IAAIH,MAAMI,MAAM,KAAK,KAAKJ,KAAK,CAAC,EAAE,KAAK,UAAU;QAC/C,OAAO;YAAED,IAAIP;YAAOG,OAAO;YAAUC,KAAKI,KAAK,CAAC,EAAE;YAAEH,QAAQ;QAAK;IACnE;IACA,IAAIG,MAAMI,MAAM,KAAK,KAAKJ,KAAK,CAAC,EAAE,KAAK,QAAQ;QAC7C,OAAO;YAAED,IAAIP;YAAOG,OAAO;YAAQC,KAAK;YAAMC,QAAQG,KAAK,CAAC,EAAE;QAAC;IACjE;IACA,IAAIA,MAAMI,MAAM,KAAK,KAAKJ,KAAK,CAAC,EAAE,KAAK,UAAU;QAC/C,OAAO;YAAED,IAAIP;YAAOG,OAAO;YAAUC,KAAKI,KAAK,CAAC,EAAE;YAAEH,QAAQG,KAAK,CAAC,EAAE;QAAC;IACvE;IACA,OAAO;AACT;AAqBA,4DAA4D,GAC5D,OAAO,SAASK,mBAAmBb,KAAc;IAC/C,MAAMc,SAAS,OAAOd,UAAU,WAAWA,QAAQe,OAAOC,GAAG;IAC7D,IAAI,CAACD,OAAOE,QAAQ,CAACH,WAAWA,UAAU,GAAG,OAAO;IACpD,OAAOI,KAAKC,GAAG,CAACzB,0BAA0BwB,KAAKE,KAAK,CAACN;AACvD;AAEA,0EAA0E,GAC1E,OAAO,SAASO,kBAAkBrB,KAAc;IAC9C,IAAI,CAACsB,MAAMC,OAAO,CAACvB,QAAQ,OAAO;IAClC,MAAMwB,SAAS;WACV,IAAIC,IACLzB,MACG0B,MAAM,CAAC,CAACC,QAA2B,OAAOA,UAAU,UACpDC,GAAG,CAAC,CAACD,QAAUA,MAAME,IAAI,IACzBH,MAAM,CAACI;KAEb,CAACC,KAAK,CAAC,GAAGpC;IACX,OAAO6B,OAAOZ,MAAM,GAAGY,SAAS;AAClC;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASQ,gBACdC,GAA+C,EAC/C1B,EAAU;IAEV,MAAM2B,UAAU5B,wBAAwBC;IACxC,IAAI,CAAC2B,WAAW,CAACD,KAAK,OAAO;IAC7B,MAAME,aAAaF,GAAG,CAAC,UAAU;IACjC,MAAMG,YAAYrB,OAAOoB,8BAAAA,WAAYC,SAAS;IAC9C,OAAO;QACLF,SAASA,QAAQ3B,EAAE;QACnBJ,OAAO+B,QAAQ/B,KAAK;QACpBC,KAAK8B,QAAQ9B,GAAG;QAChBC,QAAQ6B,QAAQ7B,MAAM;QACtBgC,SAASH,QAAQ/B,KAAK,KAAK,QAAQ,OAAOU,mBAAmBoB,GAAG,CAAC,UAAU;QAC3EK,MAAML,GAAG,CAAC,OAAO,KAAK,SAAS,SAAS;QACxCT,QAAQH,kBAAkBY,GAAG,CAAC,SAAS;QACvCM,OAAO,OAAON,GAAG,CAAC,QAAQ,KAAK,WAAYA,GAAG,CAAC,QAAQ,GAAc;QACrEO,SACE,QAAOL,8BAAAA,WAAYM,KAAK,MAAK,YAAaL,CAAAA,cAAc,MAAMA,cAAc,GAAE,IAC1E;YAAEK,OAAON,WAAWM,KAAK;YAAEL;QAAU,IACrC;IACR;AACF;AAgBA,+DAA+D,GAC/D,OAAO,SAASM,iBAAiBC,IAAY,EAAEN,OAAe;IAC5D,IAAI,CAAEA,CAAAA,UAAU,CAAA,GAAI,OAAO;IAC3B,IAAIM,QAAQN,SAAS,OAAO;IAC5B,IAAIM,QAAQN,UAAU5C,yBAAyB,OAAO;IACtD,OAAO;AACT;AAEA,8DAA8D,GAC9D,OAAO,SAASmD,qBACdD,IAAY,EACZN,OAAe;IAEf,MAAMQ,QAAQH,iBAAiBC,MAAMN;IACrC,OAAOQ,UAAU,YAAY,MAAMA,UAAU,SAAS,KAAK;AAC7D;AAEA;;;;;;;CAOC,GACD,OAAO,SAASC,kBACdC,QAAgE,EAChEC,kBAAkB,CAAC;IAEnB,IAAID,SAAST,IAAI,KAAK,QAAQ,OAAO;IACrC,MAAMW,WAAWlC,OAAOE,QAAQ,CAAC+B,oBAAoBA,kBAAkB,IAAIA,kBAAkB;IAC7F,OAAOC,WAAW,IACdF,SAASJ,IAAI,GAAGM,YAAYF,SAASV,OAAO,GAC5CU,SAASJ,IAAI,GAAGI,SAASV,OAAO;AACtC;AAEA;;;CAGC,GACD,MAAMa,cAAiD;IAAC;IAAU;IAAU;CAAO;AAEnF,MAAMC,YAAY,CAAChD,QAA0C+C,YAAYE,OAAO,CAACjD;AAWjF,kEAAkE,GAClE,OAAO,SAASkD,mBACdC,SAAyC,EACzCN,kBAAkB,CAAC;QAIjBO;IAFF,MAAMA,UAAU;WAAID;KAAU,CAACE,IAAI,CAAC,CAACC,GAAGC,IAAMP,UAAUM,EAAEtD,KAAK,IAAIgD,UAAUO,EAAEvD,KAAK;IACpF,MAAMwD,WACJJ,gBAAAA,QAAQK,IAAI,CAAC,CAACb,WAAa,CAACD,kBAAkBC,UAAUC,6BAAxDO,gBAA6E;IAC/E,IAAIM,UAAsC;IAC1C,KAAK,MAAMd,YAAYQ,QAAS;QAC9B,IAAI,CAACM,WAAWd,SAASV,OAAO,GAAGU,SAASJ,IAAI,GAAGkB,QAAQxB,OAAO,GAAGwB,QAAQlB,IAAI,EAAE;YACjFkB,UAAUd;QACZ;IACF;IACA,MAAMe,SAASP,QAAQQ,OAAO,CAAC,CAAChB;QAC9B,IAAIA,SAAST,IAAI,KAAK,QAAQ,OAAO,EAAE;QACvC,MAAMF,YAAYQ,qBAAqBG,SAASJ,IAAI,EAAEI,SAASV,OAAO;QACtE,OAAOD,YAAY;YAAC;gBAAEW;gBAAUX;YAAU;SAAE,GAAG,EAAE;IACnD;IACA,OAAO;QAAEuB;QAASE;QAASC;IAAO;AACpC;AAEA;;;;;CAKC,GACD,OAAO,SAASE,yBACdC,UAAyE;IAEzE,MAAMC,QAAQD,WACXrC,GAAG,CAAC,CAACuC,YAAcA,6BAAAA,UAAW3C,MAAM,EACpCE,MAAM,CAAC,CAACF,SAA+BF,MAAMC,OAAO,CAACC,WAAWA,OAAOZ,MAAM,GAAG;IACnF,IAAI,CAACsD,MAAMtD,MAAM,EAAE,OAAO;IAC1B,OAAOsD,MAAME,MAAM,CAAC,CAACC,MAAMC,OAASD,KAAK3C,MAAM,CAAC,CAACnB,KAAO+D,KAAKC,QAAQ,CAAChE;AACxE;AAEA,6EAA6E,GAC7E,OAAO,SAASiE;;QAEZlF;IADF,gBACEA,wBAAAA,gBAAgBsE,IAAI,CAAC,CAACa,aAAeA,WAAWC,GAAG,KAAK,sCAAxDpF,sBAA2EqF,KAAK,mBAChF;AAEJ;AAEA;;;;CAIC,GACD,OAAO,SAASC,uBACdzE,KAAgD;IAEhD,MAAM0E,WAAW,CAAC,gCAAgC,EAAEL,qBAAqB,YAAY,CAAC;IACtF,OAAQrE;QACN,KAAK;YACH,OACE,kEACA,CAAC,qBAAqB,EAAE0E,SAAS,wCAAwC,CAAC;QAE9E,KAAK;YACH,OACE,yDACA,CAAC,IAAI,EAAEA,SAAS,mBAAmB,EAAExF,iCAAiC,CAAC,CAAC;QAE5E;YACE,OACE,qDACA,CAAC,IAAI,EAAEwF,SAAS,mBAAmB,EAAExF,iCAAiC,CAAC,CAAC;IAE9E;AACF;AAEA;;;;CAIC,GACD,OAAO,SAASyF,qBAAqBhF,KAMpC;IACC,MAAMiF,MACJjF,MAAMK,KAAK,KAAK,SAAS,CAAC,IAAI,EAAEL,MAAMkF,IAAI,CAAC,KAAK,CAAC,GAAGlF,MAAMkF,IAAI;IAChE,MAAMC,QAAQnF,MAAMK,KAAK,KAAK,SAAS,QAAQ;IAC/C,MAAM+E,QAAQpF,MAAMK,KAAK,KAAK,WAAW,iBAAiB;IAC1D,MAAMgF,UACJ,GAAGrF,MAAM6C,IAAI,CAACyC,cAAc,CAAC,SAAS,IAAI,CAAC,GAC3C,GAAGtF,MAAMuC,OAAO,CAAC+C,cAAc,CAAC,SAAS,yBAAyB,CAAC;IACrE,IAAItF,MAAMsC,SAAS,IAAI,KAAK;QAC1B,OAAO;YACLiD,OAAO,GAAGN,IAAI,UAAU,EAAEE,MAAM,mBAAmB,EAAEC,OAAO;YAC5DI,MACE,GAAGH,QAAQ,qDAAqD,CAAC,GACjE,kEACA,GAAG9F,iCAAiC,CAAC,CAAC;QAC1C;IACF;IACA,OAAO;QACLgG,OAAO,GAAGN,IAAI,SAAS,EAAE7D,KAAKqE,KAAK,CAAC9F,0BAA0B,KAAK,KAAK,EAAEwF,MAAM,aAAa,EAAEC,OAAO;QACtGI,MAAM,GAAGH,QAAQ,qDAAqD,CAAC;IACzE;AACF"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/model/ai-allotments.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n ASSIST_HARD_CAP_CONTROL_LOCATION,\n type AssistRefusedBy,\n} from '../usage/assist-credits'\nimport { ORG_PERMISSIONS } from '@aglyn/aglyn/app-utils/org-permissions'\n\n/**\n * AI ALLOTMENTS (AGL-2942): the pure half.\n *\n * The workspace's AI pool is one band. An allotment is a manager's decision\n * about how much of that band one SUBJECT may draw in a calendar month (UTC),\n * so one client site, one contractor or one teammate cannot spend what the\n * others were counting on:\n *\n * orgs/{orgId}/aiAllotments/{subject}\n * { subject, scope, uid, hostId, credits, mode, models, setBy,\n * updatedAt, alerted }\n *\n * where `subject` is one of\n *\n * member:{uid} a team member, across every site\n * collab:{hostId}:{uid} a site collaborator, on that site\n * host:{hostId} a whole site, across everyone who works on it\n * org the org-wide model restriction (no credits)\n *\n * ## An allotment never grants\n *\n * The org band stays the outer wall: the reservation decides the workspace's\n * own ceilings first and consults an allotment only once they admitted the\n * request. So an allotment larger than the band is not an error and not a\n * grant — it is a line the workspace reaches after the band.\n *\n * ## Hard and soft\n *\n * A HARD allotment refuses the request that finds the subject at or past it.\n * A SOFT one admits it and tells the subject and the workspace's billing\n * contacts at 75, 80, 90 and 100%. Both are measured against what the\n * subject already drew this month, before the request — the same shape the\n * band is measured in, so a request that starts under the line may finish\n * past it, and the next one is the one refused.\n *\n * Everything here is dependency-light so the gate, the routes and the console\n * cards read one definition of a subject, a standing and a refusal sentence.\n */\n\n/** `orgs/{orgId}/{this}/{subject}`. */\nexport const AI_ALLOTMENTS_COLLECTION = 'aiAllotments'\n\n/** The subject id of the org-wide model restriction. */\nexport const AI_ALLOTMENT_ORG_SUBJECT = 'org'\n\nexport type AiAllotmentMode = 'hard' | 'soft'\n\nexport type AiAllotmentScope = 'member' | 'collab' | 'host' | 'org'\n\n/** The scopes an allotment of CREDITS can have; `org` carries models only. */\nexport type AiCreditAllotmentScope = Exclude<AiAllotmentScope, 'org'>\n\n/**\n * Why a reservation refused: the meter's own ceilings, and an allotment.\n *\n * Widened here rather than in core's `AssistRefusedBy`, because an allotment\n * is the plugin's rung: core names the ceilings core's billing sells.\n */\nexport type AiRefusedBy = AssistRefusedBy | 'allotment'\n\n/**\n * The steps a soft allotment announces — the same 75, 80, 90 and 100 every\n * usage quota steps through (AGL-3431). Only the highest step reached is\n * sent, each once a month.\n */\nexport const AI_ALLOTMENT_ALERT_BANDS = [75, 80, 90, 100] as const\nexport type AiAllotmentThreshold = (typeof AI_ALLOTMENT_ALERT_BANDS)[number]\n\n/** The share of an allotment at which the strip warns and a soft one alerts. */\nexport const AI_ALLOTMENT_WARN_SHARE = 0.8\n\n/**\n * The largest monthly allotment a write may carry. A bound on a typing\n * mistake, not a product limit: a billion credits is a million dollars of\n * provider spend, far past any band the platform sells.\n */\nexport const AI_ALLOTMENT_MAX_CREDITS = 1_000_000_000\n\n/** The most catalog ids one allowlist may name. */\nexport const AI_ALLOTMENT_MAX_MODELS = 20\n\nconst ID_SEGMENT = /^[A-Za-z0-9_-]{1,128}$/\n\nexport interface AiAllotmentSubject {\n id: string\n scope: AiAllotmentScope\n uid: string | null\n hostId: string | null\n}\n\nexport type AiAllotmentSubjectInput =\n | { scope: 'member'; uid: string }\n | { scope: 'collab'; hostId: string; uid: string }\n | { scope: 'host'; hostId: string }\n | { scope: 'org' }\n\n/** The document id for a subject. Throws on a segment that is not an id. */\nexport function aiAllotmentSubjectId(input: AiAllotmentSubjectInput): string {\n const segment = (value: string): string => {\n if (!ID_SEGMENT.test(value)) throw new Error(`not an id segment: ${value}`)\n return value\n }\n switch (input.scope) {\n case 'member':\n return `member:${segment(input.uid)}`\n case 'collab':\n return `collab:${segment(input.hostId)}:${segment(input.uid)}`\n case 'host':\n return `host:${segment(input.hostId)}`\n default:\n return AI_ALLOTMENT_ORG_SUBJECT\n }\n}\n\n/** A subject id read back, or `null` for anything that is not one. */\nexport function parseAiAllotmentSubject(value: unknown): AiAllotmentSubject | null {\n if (typeof value !== 'string') return null\n if (value === AI_ALLOTMENT_ORG_SUBJECT) {\n return { id: value, scope: 'org', uid: null, hostId: null }\n }\n const parts = value.split(':')\n if (parts.some((part) => !ID_SEGMENT.test(part))) return null\n if (parts.length === 2 && parts[0] === 'member') {\n return { id: value, scope: 'member', uid: parts[1], hostId: null }\n }\n if (parts.length === 2 && parts[0] === 'host') {\n return { id: value, scope: 'host', uid: null, hostId: parts[1] }\n }\n if (parts.length === 3 && parts[0] === 'collab') {\n return { id: value, scope: 'collab', uid: parts[2], hostId: parts[1] }\n }\n return null\n}\n\n/** One allotment as every reader hands it on. */\nexport interface AiAllotment {\n subject: string\n scope: AiAllotmentScope\n uid: string | null\n hostId: string | null\n /**\n * Credits a month, or `null` for an allotment that bounds models alone —\n * and always `null` on the org-wide restriction.\n */\n credits: number | null\n mode: AiAllotmentMode\n /** Catalog ids the subject may run on, or `null` for no restriction. */\n models: string[] | null\n setBy: string | null\n /** The last threshold announced, and the month it was announced in. */\n alerted: { month: string; threshold: AiAllotmentThreshold } | null\n}\n\n/** A finite positive whole number of credits, else `null`. */\nexport function aiAllotmentCredits(value: unknown): number | null {\n const parsed = typeof value === 'number' ? value : Number.NaN\n if (!Number.isFinite(parsed) || parsed <= 0) return null\n return Math.min(AI_ALLOTMENT_MAX_CREDITS, Math.floor(parsed))\n}\n\n/** An allowlist read back: unique strings, bounded; empty reads as none. */\nexport function aiAllotmentModels(value: unknown): string[] | null {\n if (!Array.isArray(value)) return null\n const models = [\n ...new Set(\n value\n .filter((entry): entry is string => typeof entry === 'string')\n .map((entry) => entry.trim())\n .filter(Boolean),\n ),\n ].slice(0, AI_ALLOTMENT_MAX_MODELS)\n return models.length ? models : null\n}\n\n/**\n * A stored allotment as a complete record, or `null` when the id is not a\n * subject. The scope, uid and site are read off the ID, never off the\n * fields: the id is what the gate addresses, so it is the only claim about\n * who the document is for that can be trusted.\n *\n * `mode` defaults to `hard`: a document whose mode was lost is a cap, which\n * is the reading that cannot spend more than a manager set.\n */\nexport function aiAllotmentFrom(\n raw: Record<string, unknown> | null | undefined,\n id: string,\n): AiAllotment | null {\n const subject = parseAiAllotmentSubject(id)\n if (!subject || !raw) return null\n const alertedRaw = raw['alerted'] as { month?: unknown; threshold?: unknown } | null\n const threshold = Number(alertedRaw?.threshold)\n return {\n subject: subject.id,\n scope: subject.scope,\n uid: subject.uid,\n hostId: subject.hostId,\n credits: subject.scope === 'org' ? null : aiAllotmentCredits(raw['credits']),\n mode: raw['mode'] === 'soft' ? 'soft' : 'hard',\n models: aiAllotmentModels(raw['models']),\n setBy: typeof raw['setBy'] === 'string' ? (raw['setBy'] as string) : null,\n alerted:\n typeof alertedRaw?.month === 'string' && (threshold === 80 || threshold === 100)\n ? { month: alertedRaw.month, threshold }\n : null,\n }\n}\n\n/** One allotment of credits measured against what its subject drew. */\nexport interface AiAllotmentStanding {\n subject: string\n scope: AiCreditAllotmentScope\n uid: string | null\n hostId: string | null\n credits: number\n /** Credits drawn this month before the request being decided. */\n used: number\n mode: AiAllotmentMode\n}\n\nexport type AiAllotmentState = 'ok' | 'warn' | 'reached'\n\n/** `reached` at or past the allotment, `warn` from 80% of it. */\nexport function aiAllotmentState(used: number, credits: number): AiAllotmentState {\n if (!(credits > 0)) return 'ok'\n if (used >= credits) return 'reached'\n if (used >= credits * AI_ALLOTMENT_WARN_SHARE) return 'warn'\n return 'ok'\n}\n\n/**\n * The highest step a standing has reached, for the alert pipeline — compared\n * as `used × 100 ≥ step × credits`, so 9 of 10 is exactly 90%.\n */\nexport function aiAllotmentThreshold(\n used: number,\n credits: number,\n): AiAllotmentThreshold | null {\n if (!(credits > 0) || !Number.isFinite(used) || used < 0) return null\n for (let index = AI_ALLOTMENT_ALERT_BANDS.length - 1; index >= 0; index -= 1) {\n const band = AI_ALLOTMENT_ALERT_BANDS[index]\n if (used * 100 >= band * credits) return band\n }\n return null\n}\n\n/**\n * Whether a standing admits a request.\n *\n * A soft allotment always does. A hard one admits while the subject is under\n * it — and, for a caller that knows what the request will cost before it\n * starts (a job's estimate), while the estimate still fits: a 100-credit job\n * at 0 of 100 is admitted, one at 1 of 100 is not.\n */\nexport function aiAllotmentAdmits(\n standing: Pick<AiAllotmentStanding, 'credits' | 'used' | 'mode'>,\n estimateCredits = 0,\n): boolean {\n if (standing.mode === 'soft') return true\n const estimate = Number.isFinite(estimateCredits) && estimateCredits > 0 ? estimateCredits : 0\n return estimate > 0\n ? standing.used + estimate <= standing.credits\n : standing.used < standing.credits\n}\n\n/**\n * Which subject a refusal names when more than one allotment is spent: the\n * narrowest first — the person on this site, then the person, then the site.\n */\nconst SCOPE_ORDER: readonly AiCreditAllotmentScope[] = ['collab', 'member', 'host']\n\nconst scopeRank = (scope: AiCreditAllotmentScope): number => SCOPE_ORDER.indexOf(scope)\n\nexport interface AiAllotmentVerdict {\n /** The hard allotment that refuses the request, or `null`. */\n refusal: AiAllotmentStanding | null\n /** The allotment with the least left, for the usage strip; `null` for none. */\n binding: AiAllotmentStanding | null\n /** Soft allotments at a threshold, for the alert pipeline. */\n alerts: Array<{ standing: AiAllotmentStanding; threshold: AiAllotmentThreshold }>\n}\n\n/** The verdict over every allotment that applies to one request. */\nexport function aiAllotmentVerdict(\n standings: readonly AiAllotmentStanding[],\n estimateCredits = 0,\n): AiAllotmentVerdict {\n const ordered = [...standings].sort((a, b) => scopeRank(a.scope) - scopeRank(b.scope))\n const refusal =\n ordered.find((standing) => !aiAllotmentAdmits(standing, estimateCredits)) ?? null\n let binding: AiAllotmentStanding | null = null\n for (const standing of ordered) {\n if (!binding || standing.credits - standing.used < binding.credits - binding.used) {\n binding = standing\n }\n }\n const alerts = ordered.flatMap((standing) => {\n if (standing.mode !== 'soft') return []\n const threshold = aiAllotmentThreshold(standing.used, standing.credits)\n return threshold ? [{ standing, threshold }] : []\n })\n return { refusal, binding, alerts }\n}\n\n/**\n * The allowlists that apply to a request, intersected — `null` when none of\n * the allotments names one. An empty result is a real answer: two lists that\n * share no model leave nothing to pick, and the model choice falls back to\n * the routing table rather than turning AI off for a mistake in a list.\n */\nexport function aiAllotmentModelsAllowed(\n allotments: ReadonlyArray<Pick<AiAllotment, 'models'> | null | undefined>,\n): string[] | null {\n const lists = allotments\n .map((allotment) => allotment?.models)\n .filter((models): models is string[] => Array.isArray(models) && models.length > 0)\n if (!lists.length) return null\n return lists.reduce((kept, list) => kept.filter((id) => list.includes(id)))\n}\n\n/** The catalog label for `billing.manage`, the word a sentence names it by. */\nexport function billingManageLabel(): string {\n return (\n ORG_PERMISSIONS.find((definition) => definition.key === 'billing.manage')?.label ??\n 'Manage billing'\n )\n}\n\n/**\n * The sentence a refused request is told, naming who can raise the\n * allotment and where. No figures: the strip beside it carries those, and a\n * refusal that quoted a count would be stale the moment someone raised it.\n */\nexport function aiAllotmentRefusalText(\n scope: AiCreditAllotmentScope | null | undefined,\n): string {\n const managers = `an organization admin with the \"${billingManageLabel()}\" permission`\n switch (scope) {\n case 'collab':\n return (\n 'You have used your AI allotment on this site for this month. ' +\n `The site's admin, or ${managers}, can raise it on the site's Users card.`\n )\n case 'host':\n return (\n 'This site has used its AI allotment for this month. ' +\n `Ask ${managers} to raise it under ${ASSIST_HARD_CAP_CONTROL_LOCATION}.`\n )\n default:\n return (\n 'You have used your AI allotment for this month. ' +\n `Ask ${managers} to raise it under ${ASSIST_HARD_CAP_CONTROL_LOCATION}.`\n )\n }\n}\n\n/**\n * The words a soft allotment's alert carries, for BOTH channels — the\n * console notification and the email — so the two cannot say different\n * things about one crossing.\n *\n * The email is plain text with no heading, so the body opens by saying whose\n * AI credits, how many of how many, and in which workspace (and on which site,\n * for a collaborator): a figure on its own reads as a bill. The managers'\n * copy says how to stop the subject at the line; the person's own copy does\n * not, because they cannot open Billing.\n */\nexport function aiAllotmentAlertCopy(input: {\n scope: AiCreditAllotmentScope\n threshold: AiAllotmentThreshold\n used: number\n credits: number\n /** The member's name, or the site's for a site allotment. */\n name: string\n /** The workspace the allotment is drawn from. */\n workspace: string\n /** The site a collaborator's allotment is on. */\n site?: string\n /** The workspace's managers (the default), or the person it is for. */\n reader?: 'managers' | 'subject'\n}): { title: string; body: string } {\n const { scope, threshold, name, workspace } = input\n const used = input.used.toLocaleString('en-US')\n const credits = input.credits.toLocaleString('en-US')\n const reached = threshold >= 100\n const onSite = scope === 'collab' && input.site ? ` on the ${input.site} site` : ''\n const amount = reached\n ? `all ${credits} AI credits`\n : `${used} of the ${credits} AI credits`\n const tail = reached ? ` this month (${used} so far).` : ` this month, past the ${threshold}% mark.`\n\n if (input.reader === 'subject' && scope !== 'host') {\n const where = scope === 'collab' ? onSite : ` in ${workspace}`\n const whoCanChange =\n scope === 'collab'\n ? 'The site’s admin or a workspace admin can raise or change it.'\n : 'A workspace admin can raise or change it.'\n return {\n title: reached\n ? `You’ve used your whole AI allotment${where}`\n : `You’re past ${threshold}% of your AI allotment${where}`,\n body:\n `You’ve used ${amount} ${workspace} allotted you${onSite}${tail} ` +\n 'It’s a soft allotment, so it does not stop your AI requests.' +\n (reached ? ` ${whoCanChange}` : ''),\n }\n }\n\n const who = scope === 'host' ? `The ${name} site` : name\n const whose = scope === 'host' ? 'its' : 'their'\n const them = scope === 'host' ? 'it' : 'them'\n const requests = scope === 'host' ? 'AI requests on the site' : `${name}’s AI requests`\n const where = scope === 'collab' ? onSite : scope === 'member' ? ` in ${workspace}` : ''\n const opening =\n `${who} has used ${amount} allotted to ${them}${onSite} in ${workspace}${tail}`\n if (reached) {\n return {\n title: `${who} has used ${whose} whole AI allotment${where}`,\n body:\n `${opening} It’s a soft allotment, so it does not stop ${requests}; ` +\n 'they keep drawing on the workspace’s AI credits, and crossing it ' +\n `charges nothing by itself. To stop ${them} at the line, make it hard ` +\n `under ${ASSIST_HARD_CAP_CONTROL_LOCATION}; to give ${them} more, ` +\n 'raise it there.',\n }\n }\n return {\n title: `${who} is past ${threshold}% of ${whose} AI allotment${where}`,\n body:\n `${opening} It’s a soft allotment, so it does not stop ${requests}; ` +\n 'they draw on the workspace’s AI credits like everyone else’s.',\n }\n}\n"],"names":["ASSIST_HARD_CAP_CONTROL_LOCATION","ORG_PERMISSIONS","AI_ALLOTMENTS_COLLECTION","AI_ALLOTMENT_ORG_SUBJECT","AI_ALLOTMENT_ALERT_BANDS","AI_ALLOTMENT_WARN_SHARE","AI_ALLOTMENT_MAX_CREDITS","AI_ALLOTMENT_MAX_MODELS","ID_SEGMENT","aiAllotmentSubjectId","input","segment","value","test","Error","scope","uid","hostId","parseAiAllotmentSubject","id","parts","split","some","part","length","aiAllotmentCredits","parsed","Number","NaN","isFinite","Math","min","floor","aiAllotmentModels","Array","isArray","models","Set","filter","entry","map","trim","Boolean","slice","aiAllotmentFrom","raw","subject","alertedRaw","threshold","credits","mode","setBy","alerted","month","aiAllotmentState","used","aiAllotmentThreshold","index","band","aiAllotmentAdmits","standing","estimateCredits","estimate","SCOPE_ORDER","scopeRank","indexOf","aiAllotmentVerdict","standings","ordered","sort","a","b","refusal","find","binding","alerts","flatMap","aiAllotmentModelsAllowed","allotments","lists","allotment","reduce","kept","list","includes","billingManageLabel","definition","key","label","aiAllotmentRefusalText","managers","aiAllotmentAlertCopy","name","workspace","toLocaleString","reached","onSite","site","amount","tail","reader","where","whoCanChange","title","body","who","whose","them","requests","opening"],"mappings":"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"}
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
17
|
import { type HostAction, type HostActionStepType } from '@aglyn/aglyn/app-utils/actions';
|
|
18
|
-
import type { HostWorkflow } from '@aglyn/aglyn/app-utils/workflows';
|
|
19
18
|
import type { AiAutomationNamedRecord, AiAutomationRecords } from './ai-automation-draft';
|
|
20
19
|
import type { AiWorkflowExplanation } from '../tools/ai-workflow-tool';
|
|
21
20
|
/** How much of an email's text an outline quotes. */
|
|
@@ -32,8 +31,28 @@ export declare const AI_OUTLINE_RECORD_KINDS: Readonly<Partial<Record<HostAction
|
|
|
32
31
|
export declare function aiActionRecordKinds(action: Pick<HostAction, 'steps'>): Array<keyof AiAutomationRecords>;
|
|
33
32
|
/** A saved action as an outline. */
|
|
34
33
|
export declare function aiActionOutline(action: HostAction, records: AiAutomationRecords | null): string;
|
|
34
|
+
/**
|
|
35
|
+
* A saved workflow as the workflows plugin's `workflow` record index shares
|
|
36
|
+
* it: its name, and the facts that index documents — the stored step list
|
|
37
|
+
* (each a function call by `functionId`, `functionName`, `args` and
|
|
38
|
+
* `resultName`), the scope name it returns, and its trigger.
|
|
39
|
+
*/
|
|
40
|
+
export interface AiIndexedWorkflow {
|
|
41
|
+
name: string;
|
|
42
|
+
steps?: Array<{
|
|
43
|
+
functionId?: string;
|
|
44
|
+
functionName?: string;
|
|
45
|
+
args?: string[];
|
|
46
|
+
resultName?: string;
|
|
47
|
+
}>;
|
|
48
|
+
returnValue?: string | null;
|
|
49
|
+
trigger?: {
|
|
50
|
+
event?: string;
|
|
51
|
+
filter?: string;
|
|
52
|
+
} | null;
|
|
53
|
+
}
|
|
35
54
|
/** A saved workflow — a pipeline of function calls — as an outline. */
|
|
36
|
-
export declare function aiWorkflowOutline(workflow:
|
|
55
|
+
export declare function aiWorkflowOutline(workflow: AiIndexedWorkflow, functions: readonly AiAutomationNamedRecord[] | null): string;
|
|
37
56
|
/** What the run history recorded about one run, as far as an outline reads it. */
|
|
38
57
|
export interface AiRunRecord {
|
|
39
58
|
result?: unknown;
|