@aglyn/plugins-ai 1.0.0-beta.226 → 1.0.0-beta.228
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 +3 -2
- package/package.json +17 -17
- package/src/lib/billing/ai-month-meter.js +5 -3
- package/src/lib/billing/ai-month-meter.js.map +1 -1
- package/src/lib/billing/ai-overage-ledger.js +3 -1
- package/src/lib/billing/ai-overage-ledger.js.map +1 -1
- package/src/lib/components/ai-brief-dialog.component.d.ts +6 -2
- package/src/lib/components/ai-brief-dialog.component.js +36 -18
- package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
- package/src/lib/components/ai-describe-automation.component.js +24 -4
- package/src/lib/components/ai-describe-automation.component.js.map +1 -1
- package/src/lib/components/ai-describe-button.component.js +6 -1
- package/src/lib/components/ai-describe-button.component.js.map +1 -1
- package/src/lib/components/ai-describe-page.component.d.ts +1 -1
- package/src/lib/components/ai-describe-page.component.js +1 -1
- package/src/lib/components/ai-describe-page.component.js.map +1 -1
- package/src/lib/components/ai-insight-dialog.component.js +24 -2
- package/src/lib/components/ai-insight-dialog.component.js.map +1 -1
- package/src/lib/components/ai-job-events.d.ts +20 -0
- package/src/lib/components/ai-job-events.js +78 -0
- package/src/lib/components/ai-job-events.js.map +1 -0
- package/src/lib/components/ai-job-follow.component.d.ts +49 -0
- package/src/lib/components/ai-job-follow.component.js +210 -0
- package/src/lib/components/ai-job-follow.component.js.map +1 -0
- package/src/lib/components/ai-job-links.d.ts +42 -0
- package/src/lib/components/ai-job-links.js +140 -0
- package/src/lib/components/ai-job-links.js.map +1 -0
- package/src/lib/components/ai-job-plan.component.d.ts +13 -6
- package/src/lib/components/ai-job-plan.component.js +29 -3
- package/src/lib/components/ai-job-plan.component.js.map +1 -1
- package/src/lib/components/ai-job-requests.d.ts +24 -0
- package/src/lib/components/ai-job-requests.js +76 -0
- package/src/lib/components/ai-job-requests.js.map +1 -0
- package/src/lib/components/ai-job-site.d.ts +22 -0
- package/src/lib/components/ai-job-site.js +73 -0
- package/src/lib/components/ai-job-site.js.map +1 -0
- package/src/lib/components/ai-jobs-indicator.component.d.ts +25 -0
- package/src/lib/components/ai-jobs-indicator.component.js +80 -0
- package/src/lib/components/ai-jobs-indicator.component.js.map +1 -0
- package/src/lib/components/ai-jobs-list-page.component.d.ts +23 -0
- package/src/lib/components/ai-jobs-list-page.component.js +303 -0
- package/src/lib/components/ai-jobs-list-page.component.js.map +1 -0
- package/src/lib/components/ai-jobs-page.component.d.ts +7 -0
- package/src/lib/components/ai-jobs-page.component.js +30 -0
- package/src/lib/components/ai-jobs-page.component.js.map +1 -0
- package/src/lib/components/ai-jobs-store.d.ts +81 -0
- package/src/lib/components/ai-jobs-store.js +218 -0
- package/src/lib/components/ai-jobs-store.js.map +1 -0
- package/src/lib/components/ai-page-brief-dialog.component.d.ts +4 -4
- package/src/lib/components/ai-page-brief-dialog.component.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.d.ts +51 -0
- package/src/lib/components/ai-site-build-page.component.js +495 -0
- package/src/lib/components/ai-site-build-page.component.js.map +1 -0
- package/src/lib/components/ai-site-start-card.component.d.ts +9 -1
- package/src/lib/components/ai-site-start-card.component.js +533 -132
- package/src/lib/components/ai-site-start-card.component.js.map +1 -1
- package/src/lib/components/ai-site-starter-fallback.component.d.ts +24 -0
- package/src/lib/components/ai-site-starter-fallback.component.js +112 -0
- package/src/lib/components/ai-site-starter-fallback.component.js.map +1 -0
- package/src/lib/components/assist-jobs-drawer.component.d.ts +18 -20
- package/src/lib/components/assist-jobs-drawer.component.js +152 -172
- package/src/lib/components/assist-jobs-drawer.component.js.map +1 -1
- package/src/lib/components/assist-panel.component.js +69 -8
- package/src/lib/components/assist-panel.component.js.map +1 -1
- package/src/lib/components/staff-ai-credit-return.component.d.ts +55 -0
- package/src/lib/components/staff-ai-credit-return.component.js +346 -0
- package/src/lib/components/staff-ai-credit-return.component.js.map +1 -0
- package/src/lib/components/staff-org-ai-card.component.js +364 -279
- package/src/lib/components/staff-org-ai-card.component.js.map +1 -1
- package/src/lib/components/staff-user-ai-usage-card.component.d.ts +4 -0
- package/src/lib/components/staff-user-ai-usage-card.component.js +83 -28
- package/src/lib/components/staff-user-ai-usage-card.component.js.map +1 -1
- package/src/lib/components/use-ai-job-run.js +5 -18
- package/src/lib/components/use-ai-job-run.js.map +1 -1
- package/src/lib/jobs/ai-job-auto-confirm.d.ts +52 -0
- package/src/lib/jobs/ai-job-auto-confirm.js +62 -0
- package/src/lib/jobs/ai-job-auto-confirm.js.map +1 -0
- package/src/lib/jobs/ai-job-draft-ids.d.ts +6 -0
- package/src/lib/jobs/ai-job-draft-ids.js +9 -0
- package/src/lib/jobs/ai-job-draft-ids.js.map +1 -1
- package/src/lib/jobs/ai-job-drafts.d.ts +7 -0
- package/src/lib/jobs/ai-job-drafts.js +16 -7
- package/src/lib/jobs/ai-job-drafts.js.map +1 -1
- package/src/lib/jobs/ai-job-layout-step.js +2 -1
- package/src/lib/jobs/ai-job-layout-step.js.map +1 -1
- package/src/lib/jobs/ai-job-page-sections.d.ts +6 -5
- package/src/lib/jobs/ai-job-page-sections.js +3 -3
- package/src/lib/jobs/ai-job-page-sections.js.map +1 -1
- package/src/lib/jobs/ai-job-page-step.d.ts +19 -1
- package/src/lib/jobs/ai-job-page-step.js +50 -8
- package/src/lib/jobs/ai-job-page-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +33 -2
- package/src/lib/jobs/ai-job-plan-step.js +167 -18
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.d.ts +28 -0
- package/src/lib/jobs/ai-job-site-step.js +130 -21
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-text-step.d.ts +14 -1
- package/src/lib/jobs/ai-job-text-step.js.map +1 -1
- package/src/lib/jobs/ai-jobs-notify.d.ts +37 -0
- package/src/lib/jobs/ai-jobs-notify.js +51 -0
- package/src/lib/jobs/ai-jobs-notify.js.map +1 -0
- package/src/lib/jobs/ai-jobs.d.ts +39 -4
- package/src/lib/jobs/ai-jobs.js +205 -34
- package/src/lib/jobs/ai-jobs.js.map +1 -1
- package/src/lib/jobs/ai-site-publish.d.ts +98 -0
- package/src/lib/jobs/ai-site-publish.js +372 -0
- package/src/lib/jobs/ai-site-publish.js.map +1 -0
- package/src/lib/jobs/fixtures/ai-page-briefs.d.ts +7 -6
- package/src/lib/jobs/fixtures/ai-page-briefs.js +23 -56
- package/src/lib/jobs/fixtures/ai-page-briefs.js.map +1 -1
- package/src/lib/model/ai-job-activity.d.ts +112 -0
- package/src/lib/model/ai-job-activity.js +144 -0
- package/src/lib/model/ai-job-activity.js.map +1 -0
- package/src/lib/model/ai-job-failure-copy.d.ts +69 -0
- package/src/lib/model/ai-job-failure-copy.js +78 -0
- package/src/lib/model/ai-job-failure-copy.js.map +1 -0
- package/src/lib/model/ai-job-notice.d.ts +63 -0
- package/src/lib/model/ai-job-notice.js +129 -0
- package/src/lib/model/ai-job-notice.js.map +1 -0
- package/src/lib/model/ai-jobs.types.d.ts +53 -0
- package/src/lib/model/ai-jobs.types.js +1 -0
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-plan-capabilities.d.ts +32 -10
- package/src/lib/model/ai-plan-capabilities.js +15 -2
- package/src/lib/model/ai-plan-capabilities.js.map +1 -1
- package/src/lib/model/ai-site-build-progress.d.ts +61 -0
- package/src/lib/model/ai-site-build-progress.js +136 -0
- package/src/lib/model/ai-site-build-progress.js.map +1 -0
- package/src/lib/model/ai-site-inventory.d.ts +9 -0
- package/src/lib/model/ai-site-inventory.js +7 -2
- package/src/lib/model/ai-site-inventory.js.map +1 -1
- package/src/lib/model/ai-site-job.d.ts +93 -2
- package/src/lib/model/ai-site-job.js +88 -7
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-site-start-seo.d.ts +19 -0
- package/src/lib/model/ai-site-start-seo.js +91 -3
- package/src/lib/model/ai-site-start-seo.js.map +1 -1
- package/src/lib/model/ai-site-start.d.ts +3 -1
- package/src/lib/model/ai-site-start.js +6 -4
- package/src/lib/model/ai-site-start.js.map +1 -1
- package/src/lib/plan-entitlements.d.ts +3 -3
- package/src/lib/plan-entitlements.js +3 -3
- package/src/lib/plan-entitlements.js.map +1 -1
- package/src/lib/plugin.js +38 -2
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/runtime/ai-abuse-guards.d.ts +9 -6
- package/src/lib/runtime/ai-abuse-guards.js +10 -7
- package/src/lib/runtime/ai-abuse-guards.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.d.ts +16 -12
- package/src/lib/runtime/ai-doctrine-validators.js +50 -40
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +1 -1
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-eval-live.d.ts +1 -1
- package/src/lib/runtime/ai-eval-live.js +1 -1
- package/src/lib/runtime/ai-eval-live.js.map +1 -1
- package/src/lib/runtime/ai-gate.d.ts +3 -2
- package/src/lib/runtime/ai-gate.js +3 -2
- package/src/lib/runtime/ai-gate.js.map +1 -1
- package/src/lib/runtime/site-inventory.d.ts +16 -1
- package/src/lib/runtime/site-inventory.js +43 -3
- package/src/lib/runtime/site-inventory.js.map +1 -1
- package/src/lib/server/ai-admin-credits.d.ts +21 -0
- package/src/lib/server/ai-admin-credits.js +213 -0
- package/src/lib/server/ai-admin-credits.js.map +1 -0
- package/src/lib/server/ai-admin-org.js +12 -5
- package/src/lib/server/ai-admin-org.js.map +1 -1
- package/src/lib/server/ai-admin-signals.js +3 -1
- package/src/lib/server/ai-admin-signals.js.map +1 -1
- package/src/lib/server/ai-admin-user.js +12 -1
- package/src/lib/server/ai-admin-user.js.map +1 -1
- package/src/lib/server/ai-allotments.js +3 -2
- package/src/lib/server/ai-allotments.js.map +1 -1
- package/src/lib/server/ai-jobs-live-sites.d.ts +26 -0
- package/src/lib/server/ai-jobs-live-sites.js +58 -0
- package/src/lib/server/ai-jobs-live-sites.js.map +1 -0
- package/src/lib/server/ai-jobs-route.js +42 -5
- package/src/lib/server/ai-jobs-route.js.map +1 -1
- package/src/lib/server/assist-chat.js +3 -2
- package/src/lib/server/assist-chat.js.map +1 -1
- package/src/lib/server/billing-credits.js +4 -3
- package/src/lib/server/billing-credits.js.map +1 -1
- package/src/lib/server/billing-overage.js +3 -2
- package/src/lib/server/billing-overage.js.map +1 -1
- package/src/lib/server.js +11 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/usage/assist-credit-returns-write.d.ts +75 -0
- package/src/lib/usage/assist-credit-returns-write.js +150 -0
- package/src/lib/usage/assist-credit-returns-write.js.map +1 -0
- package/src/lib/usage/assist-credit-returns.d.ts +68 -0
- package/src/lib/usage/assist-credit-returns.js +79 -0
- package/src/lib/usage/assist-credit-returns.js.map +1 -0
- package/src/lib/usage/assist-free-taste.js +6 -3
- package/src/lib/usage/assist-free-taste.js.map +1 -1
- package/src/lib/usage/assist-job-refund.d.ts +100 -0
- package/src/lib/usage/assist-job-refund.js +109 -0
- package/src/lib/usage/assist-job-refund.js.map +1 -0
- package/src/lib/usage/assist-usage.js +5 -2
- package/src/lib/usage/assist-usage.js.map +1 -1
- package/src/lib/usage/staff-org-ai.d.ts +31 -1
- package/src/lib/usage/staff-org-ai.js +19 -2
- package/src/lib/usage/staff-org-ai.js.map +1 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { notifyUsers } from "@aglyn/tenant-data-admin/server/notifications";
|
|
17
|
+
import { aiJobNotice } from "../model/ai-job-notice.js";
|
|
18
|
+
import { aiJobAutoConfirms } from "./ai-job-auto-confirm.js";
|
|
19
|
+
import { registerAiJobTransitionListener } from "./ai-jobs.js";
|
|
20
|
+
/**
|
|
21
|
+
* The person who started an AI job is told when it needs them, when it
|
|
22
|
+
* finishes and when it stops (AGL-3593), through the console's notifications
|
|
23
|
+
* — the bell, and the generic notification email for a person who switched
|
|
24
|
+
* that channel on for the category. No other channel: there is no push.
|
|
25
|
+
*
|
|
26
|
+
* The machine calls this where the job's state changes, once per change
|
|
27
|
+
* (`registerAiJobTransitionListener`): the request that ran a step inline,
|
|
28
|
+
* or the beat that ran it with nobody watching — which is exactly when a
|
|
29
|
+
* person needs telling. The recipient is the job's creator, whose job it is
|
|
30
|
+
* and who alone confirms its plan; a teammate is not told about someone
|
|
31
|
+
* else's draft.
|
|
32
|
+
*
|
|
33
|
+
* `notifyUsers` never throws, and honors the person's per-category,
|
|
34
|
+
* per-workspace and per-site preferences.
|
|
35
|
+
*/ export function aiJobTransitionNotifier(notify = notifyUsers) {
|
|
36
|
+
return async ({ job, to })=>{
|
|
37
|
+
var _job_review;
|
|
38
|
+
if (!job.createdBy) return;
|
|
39
|
+
// A guided site start's plan is confirmed for it the moment it is kept
|
|
40
|
+
// (AGL-3594): there is nothing for the person to confirm.
|
|
41
|
+
if (to === 'needs-review' && ((_job_review = job.review) == null ? void 0 : _job_review.reason) === 'plan' && aiJobAutoConfirms(job)) return;
|
|
42
|
+
await notify([
|
|
43
|
+
job.createdBy
|
|
44
|
+
], aiJobNotice(job, to));
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Registers the notifier the jobs machine tells about each change. */ export function registerAiJobsNotify() {
|
|
48
|
+
registerAiJobTransitionListener(aiJobTransitionNotifier());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
//# sourceMappingURL=ai-jobs-notify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-jobs-notify.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 { notifyUsers } from '@aglyn/tenant-data-admin/server/notifications'\nimport { aiJobNotice } from '../model/ai-job-notice'\nimport { aiJobAutoConfirms } from './ai-job-auto-confirm'\nimport { registerAiJobTransitionListener, type AiJobTransitionListener } from './ai-jobs'\n\n/**\n * The person who started an AI job is told when it needs them, when it\n * finishes and when it stops (AGL-3593), through the console's notifications\n * — the bell, and the generic notification email for a person who switched\n * that channel on for the category. No other channel: there is no push.\n *\n * The machine calls this where the job's state changes, once per change\n * (`registerAiJobTransitionListener`): the request that ran a step inline,\n * or the beat that ran it with nobody watching — which is exactly when a\n * person needs telling. The recipient is the job's creator, whose job it is\n * and who alone confirms its plan; a teammate is not told about someone\n * else's draft.\n *\n * `notifyUsers` never throws, and honors the person's per-category,\n * per-workspace and per-site preferences.\n */\nexport function aiJobTransitionNotifier(\n notify: typeof notifyUsers = notifyUsers,\n): AiJobTransitionListener {\n return async ({ job, to }) => {\n if (!job.createdBy) return\n // A guided site start's plan is confirmed for it the moment it is kept\n // (AGL-3594): there is nothing for the person to confirm.\n if (to === 'needs-review' && job.review?.reason === 'plan' && aiJobAutoConfirms(job)) return\n await notify([job.createdBy], aiJobNotice(job, to))\n }\n}\n\n/** Registers the notifier the jobs machine tells about each change. */\nexport function registerAiJobsNotify(): void {\n registerAiJobTransitionListener(aiJobTransitionNotifier())\n}\n"],"names":["notifyUsers","aiJobNotice","aiJobAutoConfirms","registerAiJobTransitionListener","aiJobTransitionNotifier","notify","job","to","createdBy","review","reason","registerAiJobsNotify"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,WAAW,QAAQ,gDAA+C;AAC3E,SAASC,WAAW,QAAQ,4BAAwB;AACpD,SAASC,iBAAiB,QAAQ,2BAAuB;AACzD,SAASC,+BAA+B,QAAsC,eAAW;AAEzF;;;;;;;;;;;;;;;CAeC,GACD,OAAO,SAASC,wBACdC,SAA6BL,WAAW;IAExC,OAAO,OAAO,EAAEM,GAAG,EAAEC,EAAE,EAAE;YAIMD;QAH7B,IAAI,CAACA,IAAIE,SAAS,EAAE;QACpB,uEAAuE;QACvE,0DAA0D;QAC1D,IAAID,OAAO,kBAAkBD,EAAAA,cAAAA,IAAIG,MAAM,qBAAVH,YAAYI,MAAM,MAAK,UAAUR,kBAAkBI,MAAM;QACtF,MAAMD,OAAO;YAACC,IAAIE,SAAS;SAAC,EAAEP,YAAYK,KAAKC;IACjD;AACF;AAEA,qEAAqE,GACrE,OAAO,SAASI;IACdR,gCAAgCC;AAClC"}
|
|
@@ -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 AiJob, type AiJobApplied, type AiJobKind, type AiJobOutput, type AiJobPlan, type AiJobReview, type AiJobStatus, type AiJobStepTokens, type AiJobSummary } from '../model/ai-jobs.types';
|
|
17
|
+
import { AI_JOB_PLAN_STEP, type AiJob, type AiJobApplied, type AiJobKind, type AiJobOutput, type AiJobPlan, type AiJobReview, type AiJobSitePublish, type AiJobStatus, type AiJobStepTokens, type AiJobSummary } from '../model/ai-jobs.types';
|
|
18
18
|
import type { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types';
|
|
19
19
|
import { type AiActivityActor } from '../activity/ai-activity';
|
|
20
20
|
import { type AiEffort, type AiUsage } from '../runtime/ai-runtime';
|
|
@@ -107,8 +107,7 @@ export declare const AI_JOB_NOT_AVAILABLE_COPY = "This kind of AI job is not ava
|
|
|
107
107
|
export declare const AI_JOB_REFUSED_COPY = "The model declined this brief. Try rephrasing it.";
|
|
108
108
|
type Firestore = FirebaseFirestore.Firestore;
|
|
109
109
|
export declare function isAiJobTerminal(status: AiJobStatus): boolean;
|
|
110
|
-
|
|
111
|
-
export declare const AI_JOB_PLAN_STEP = "plan";
|
|
110
|
+
export { AI_JOB_PLAN_STEP };
|
|
112
111
|
/**
|
|
113
112
|
* The kinds that build site structure, and so plan before they generate
|
|
114
113
|
* (AGL-2935): the plan their first step proposes is what their generation
|
|
@@ -196,6 +195,26 @@ export type AiJobPauseReader = (input: {
|
|
|
196
195
|
* runs every job, as a process with no lockdown carrier would.
|
|
197
196
|
*/
|
|
198
197
|
export declare function registerAiJobPauseReader(reader: AiJobPauseReader | null): void;
|
|
198
|
+
/**
|
|
199
|
+
* A change a person is told about: the job's plan waits for them (or a step
|
|
200
|
+
* stopped for their decision), the job finished, or it stopped.
|
|
201
|
+
*/
|
|
202
|
+
export type AiJobTransition = 'needs-review' | 'done' | 'failed';
|
|
203
|
+
export type AiJobTransitionListener = (input: {
|
|
204
|
+
job: AiJob;
|
|
205
|
+
to: AiJobTransition;
|
|
206
|
+
}) => Promise<void> | void;
|
|
207
|
+
/**
|
|
208
|
+
* The listener told each time a job ENTERS `needs_review`, `done` or `failed`
|
|
209
|
+
* — once per entry, from the write that made it, so a stream that re-reads
|
|
210
|
+
* the job or a beat that sees it again tells nobody twice. Registered by its
|
|
211
|
+
* own module (`ai-jobs-notify.ts`) from the console's server surface, as the
|
|
212
|
+
* pause reader is, so this machine never loads the notification fan-out.
|
|
213
|
+
* `null` unregisters it.
|
|
214
|
+
*/
|
|
215
|
+
export declare function registerAiJobTransitionListener(listener: AiJobTransitionListener | null): void;
|
|
216
|
+
/** Whether a transition listener is registered: what a surface that runs jobs must have. */
|
|
217
|
+
export declare function aiJobTransitionListenerRegistered(): boolean;
|
|
199
218
|
/** Whether a pause reader is registered: what a surface that runs jobs must have. */
|
|
200
219
|
export declare function aiJobPauseReaderRegistered(): boolean;
|
|
201
220
|
/** The runner for one step of one job: the plan step by its name, else the kind's own. */
|
|
@@ -230,9 +249,23 @@ export declare function createAiJob(firestore: Firestore, input: CreateAiJobInpu
|
|
|
230
249
|
export declare function getAiJob(firestore: Firestore, orgId: string, jobId: string): Promise<AiJob | null>;
|
|
231
250
|
export interface ListAiJobsOptions {
|
|
232
251
|
status?: AiJobStatus;
|
|
252
|
+
/**
|
|
253
|
+
* Any of these statuses (AGL-3593): the console's indicator asks for every
|
|
254
|
+
* job not yet settled in one read. Ignored when `status` is given.
|
|
255
|
+
*/
|
|
256
|
+
statuses?: readonly AiJobStatus[];
|
|
257
|
+
/**
|
|
258
|
+
* One site's jobs (AGL-3596), for the site's AI jobs page; on the
|
|
259
|
+
* (hostId, createdAt) index. Not combined with a status filter, which no
|
|
260
|
+
* reader asks for and no index answers.
|
|
261
|
+
*/
|
|
262
|
+
hostId?: string;
|
|
233
263
|
limit?: number;
|
|
234
264
|
}
|
|
235
|
-
/**
|
|
265
|
+
/**
|
|
266
|
+
* Newest first. Needs the (status, createdAt) index when filtered, which
|
|
267
|
+
* answers an `in` over statuses as it answers one status.
|
|
268
|
+
*/
|
|
236
269
|
export declare function listAiJobs(firestore: Firestore, orgId: string, options?: ListAiJobsOptions): Promise<AiJob[]>;
|
|
237
270
|
/** The wire form. `running` is the lease, read against `now`. */
|
|
238
271
|
export declare function aiJobSummary(job: AiJob, now?: Date): AiJobSummary;
|
|
@@ -288,6 +321,8 @@ export interface RecordStepInput {
|
|
|
288
321
|
continued?: boolean;
|
|
289
322
|
/** What the run cost in tokens and time (AGL-2937), added to the step's totals. */
|
|
290
323
|
tokens?: AiJobStepTokenRun;
|
|
324
|
+
/** What a guided site start put live (AGL-3596), kept on the job. */
|
|
325
|
+
sitePublish?: AiJobSitePublish;
|
|
291
326
|
}
|
|
292
327
|
/** One run of a step's runner, as the machine measured it (AGL-2937). */
|
|
293
328
|
export interface AiJobStepTokenRun {
|
package/src/lib/jobs/ai-jobs.js
CHANGED
|
@@ -23,7 +23,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
23
23
|
import { createResourceUid } from "@aglyn/aglyn/app-utils/create-resource-uid";
|
|
24
24
|
import { checkEntitlement } from "@aglyn/aglyn/server";
|
|
25
25
|
import { assistCreditsFromUsd, assistFreeTasteRefusalText, assistHardCapRefusalText, assistRefusedByHardCap } from "../usage/assist-credits.js";
|
|
26
|
-
import { AI_JOB_TERMINAL_STATUSES } from "../model/ai-jobs.types.js";
|
|
26
|
+
import { AI_JOB_PLAN_STEP, AI_JOB_TERMINAL_STATUSES } from "../model/ai-jobs.types.js";
|
|
27
27
|
import { resolveEffectivePlan } from "@aglyn/aglyn/app-utils/plan-entitlements";
|
|
28
28
|
import { aiOverageReservationRefusal } from "../billing/ai-overage-gate.js";
|
|
29
29
|
import { aiAllotmentRefusalText } from "../model/ai-allotments.js";
|
|
@@ -39,7 +39,10 @@ import { aiMintJobDraftIds } from "./ai-job-draft-ids.js";
|
|
|
39
39
|
import { AI_JOB_TEXT_STEP_MINIMUM_MS, runAiJobTextStep } from "./ai-job-text-step.js";
|
|
40
40
|
import { AI_JOB_SEO_STEP_MINIMUM_MS } from "./ai-job-seo-budget.js";
|
|
41
41
|
import { AI_JOB_THEME_STEP_MINIMUM_MS } from "./ai-job-theme-budget.js";
|
|
42
|
-
import { assistExchangeExpiry, recordAssistCost, releaseAssistMessage, reserveAssistMessage } from "../usage/assist-usage.js";
|
|
42
|
+
import { assistExchangeExpiry, assistUsageDay, assistUsageMonth, recordAssistCost, releaseAssistMessage, reserveAssistMessage } from "../usage/assist-usage.js";
|
|
43
|
+
import { aiJobRefundCredits, refundJobCredits } from "../usage/assist-job-refund.js";
|
|
44
|
+
import { freeAssistAccount } from "../usage/assist-free-taste.js";
|
|
45
|
+
import { aiJobAutoConfirms } from "./ai-job-auto-confirm.js";
|
|
43
46
|
import { addAdminAudit } from "@aglyn/tenant-data-admin/server/admin-audit-write";
|
|
44
47
|
/**
|
|
45
48
|
* AI generation jobs — the Firestore state machine (AGL-2904).
|
|
@@ -134,7 +137,9 @@ export function isAiJobTerminal(status) {
|
|
|
134
137
|
return AI_JOB_TERMINAL_STATUSES.includes(status);
|
|
135
138
|
}
|
|
136
139
|
// ── The step runner registry ──────────────────────────────────────────────
|
|
137
|
-
|
|
140
|
+
// The step a planned kind runs first (AGL-2935), named in the model so the
|
|
141
|
+
// console reads a job's phase off its steps without loading this machine.
|
|
142
|
+
export { AI_JOB_PLAN_STEP };
|
|
138
143
|
/**
|
|
139
144
|
* The kinds that build site structure, and so plan before they generate
|
|
140
145
|
* (AGL-2935): the plan their first step proposes is what their generation
|
|
@@ -232,6 +237,36 @@ let pauseReader = null;
|
|
|
232
237
|
*/ export function registerAiJobPauseReader(reader) {
|
|
233
238
|
pauseReader = reader;
|
|
234
239
|
}
|
|
240
|
+
let transitionListener = null;
|
|
241
|
+
/**
|
|
242
|
+
* The listener told each time a job ENTERS `needs_review`, `done` or `failed`
|
|
243
|
+
* — once per entry, from the write that made it, so a stream that re-reads
|
|
244
|
+
* the job or a beat that sees it again tells nobody twice. Registered by its
|
|
245
|
+
* own module (`ai-jobs-notify.ts`) from the console's server surface, as the
|
|
246
|
+
* pause reader is, so this machine never loads the notification fan-out.
|
|
247
|
+
* `null` unregisters it.
|
|
248
|
+
*/ export function registerAiJobTransitionListener(listener) {
|
|
249
|
+
transitionListener = listener;
|
|
250
|
+
}
|
|
251
|
+
/** Whether a transition listener is registered: what a surface that runs jobs must have. */ export function aiJobTransitionListenerRegistered() {
|
|
252
|
+
return transitionListener !== null;
|
|
253
|
+
}
|
|
254
|
+
/** Tells the listener; a listener that throws never fails the write it follows. */ async function announceAiJobTransition(job, to) {
|
|
255
|
+
if (!transitionListener) return;
|
|
256
|
+
try {
|
|
257
|
+
await transitionListener({
|
|
258
|
+
job,
|
|
259
|
+
to
|
|
260
|
+
});
|
|
261
|
+
} catch (error) {
|
|
262
|
+
console.error('ai job transition listener failed', {
|
|
263
|
+
orgId: job.orgId,
|
|
264
|
+
jobId: job.$id,
|
|
265
|
+
to,
|
|
266
|
+
error
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
235
270
|
/** Whether a pause reader is registered: what a surface that runs jobs must have. */ export function aiJobPauseReaderRegistered() {
|
|
236
271
|
return pauseReader !== null;
|
|
237
272
|
}
|
|
@@ -378,19 +413,27 @@ function jobFrom(snapshot) {
|
|
|
378
413
|
export async function getAiJob(firestore, orgId, jobId) {
|
|
379
414
|
return jobFrom(await jobsCollection(firestore, orgId).doc(jobId).get());
|
|
380
415
|
}
|
|
381
|
-
/**
|
|
416
|
+
/**
|
|
417
|
+
* Newest first. Needs the (status, createdAt) index when filtered, which
|
|
418
|
+
* answers an `in` over statuses as it answers one status.
|
|
419
|
+
*/ export async function listAiJobs(firestore, orgId, options = {}) {
|
|
382
420
|
var _options_limit;
|
|
421
|
+
var _options_statuses;
|
|
383
422
|
const limit = Math.min(Math.max(1, Math.floor((_options_limit = options.limit) != null ? _options_limit : 20)), 100);
|
|
384
423
|
let query = jobsCollection(firestore, orgId);
|
|
385
424
|
if (options.status) query = query.where('status', '==', options.status);
|
|
425
|
+
else if ((_options_statuses = options.statuses) == null ? void 0 : _options_statuses.length) query = query.where('status', 'in', [
|
|
426
|
+
...options.statuses
|
|
427
|
+
]);
|
|
428
|
+
else if (options.hostId) query = query.where('hostId', '==', options.hostId);
|
|
386
429
|
const snapshot = await query.orderBy('createdAt', 'desc').limit(limit).get();
|
|
387
430
|
return snapshot.docs.map((doc)=>jobFrom(doc)).filter((job)=>job !== null);
|
|
388
431
|
}
|
|
389
432
|
/** The wire form. `running` is the lease, read against `now`. */ export function aiJobSummary(job, now = new Date()) {
|
|
390
|
-
var _job_hostId, _job_steps, _job_outputs, _job_creditsReserved, _job_creditsSpent, _toIso, _toIso1, _job_error, _job_plan_labels, _job_plan_confirmedBy, _job_review, _job_applied_versions, _job_applied_staged;
|
|
433
|
+
var _job_hostId, _job_steps, _job_outputs, _job_creditsReserved, _job_creditsSpent, _job_refundedCredits, _toIso, _toIso1, _job_error, _job_plan_labels, _job_plan_confirmedBy, _job_review, _job_applied_versions, _job_applied_staged;
|
|
391
434
|
var _job_lease, _job_inputs;
|
|
392
435
|
const leaseUntil = toMillis((_job_lease = job.lease) == null ? void 0 : _job_lease.until);
|
|
393
|
-
return {
|
|
436
|
+
return _extends({
|
|
394
437
|
id: job.$id,
|
|
395
438
|
orgId: job.orgId,
|
|
396
439
|
hostId: (_job_hostId = job.hostId) != null ? _job_hostId : null,
|
|
@@ -412,6 +455,7 @@ export async function getAiJob(firestore, orgId, jobId) {
|
|
|
412
455
|
outputs: (_job_outputs = job.outputs) != null ? _job_outputs : [],
|
|
413
456
|
creditsReserved: (_job_creditsReserved = job.creditsReserved) != null ? _job_creditsReserved : 0,
|
|
414
457
|
creditsSpent: (_job_creditsSpent = job.creditsSpent) != null ? _job_creditsSpent : 0,
|
|
458
|
+
refundedCredits: (_job_refundedCredits = job.refundedCredits) != null ? _job_refundedCredits : 0,
|
|
415
459
|
createdBy: job.createdBy,
|
|
416
460
|
createdAt: (_toIso = toIso(job.createdAt)) != null ? _toIso : new Date(0).toISOString(),
|
|
417
461
|
updatedAt: (_toIso1 = toIso(job.updatedAt)) != null ? _toIso1 : new Date(0).toISOString(),
|
|
@@ -430,7 +474,9 @@ export async function getAiJob(firestore, orgId, jobId) {
|
|
|
430
474
|
versions: (_job_applied_versions = job.applied.versions) != null ? _job_applied_versions : {},
|
|
431
475
|
staged: (_job_applied_staged = job.applied.staged) != null ? _job_applied_staged : []
|
|
432
476
|
} : null
|
|
433
|
-
}
|
|
477
|
+
}, job.sitePublish ? {
|
|
478
|
+
sitePublish: job.sitePublish
|
|
479
|
+
} : {});
|
|
434
480
|
}
|
|
435
481
|
function leaseIsLive(job, owner, nowMs) {
|
|
436
482
|
const lease = job.lease;
|
|
@@ -605,7 +651,7 @@ function nextStepIndex(job) {
|
|
|
605
651
|
* in the same transaction, so no beat can claim the next step in between.
|
|
606
652
|
*/ export async function recordStep(firestore, orgId, jobId, owner, stepIndex, input, now = new Date()) {
|
|
607
653
|
const ref = jobsCollection(firestore, orgId).doc(jobId);
|
|
608
|
-
|
|
654
|
+
const recorded = await firestore.runTransaction(async (tx)=>{
|
|
609
655
|
var _job_outputs, _input_outputs, _job_creditsSpent, _job_creditsReserved, _job_creditsReserved1;
|
|
610
656
|
var _job_lease;
|
|
611
657
|
const job = jobFrom(await tx.get(ref));
|
|
@@ -645,6 +691,8 @@ function nextStepIndex(job) {
|
|
|
645
691
|
lease: null
|
|
646
692
|
} : {}, input.plan ? {
|
|
647
693
|
plan: input.plan
|
|
694
|
+
} : {}, input.sitePublish ? {
|
|
695
|
+
sitePublish: input.sitePublish
|
|
648
696
|
} : {}, parksForReview && input.review ? {
|
|
649
697
|
review: input.review,
|
|
650
698
|
error: input.review.reason === 'plan' ? null : input.review.message
|
|
@@ -654,9 +702,16 @@ function nextStepIndex(job) {
|
|
|
654
702
|
});
|
|
655
703
|
return {
|
|
656
704
|
job: _extends({}, job, patch),
|
|
657
|
-
remaining
|
|
705
|
+
remaining,
|
|
706
|
+
// Entered `needs_review` in this write, rather than parked again.
|
|
707
|
+
parked: parksForReview && job.status !== 'needs_review'
|
|
658
708
|
};
|
|
659
709
|
});
|
|
710
|
+
if (recorded.parked) await announceAiJobTransition(recorded.job, 'needs-review');
|
|
711
|
+
return {
|
|
712
|
+
job: recorded.job,
|
|
713
|
+
remaining: recorded.remaining
|
|
714
|
+
};
|
|
660
715
|
}
|
|
661
716
|
async function transition(firestore, orgId, jobId, apply) {
|
|
662
717
|
const ref = jobsCollection(firestore, orgId).doc(jobId);
|
|
@@ -678,13 +733,14 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
678
733
|
});
|
|
679
734
|
}
|
|
680
735
|
/** Every step done: the job is `done` and holds nothing. */ export async function completeAiJob(firestore, orgId, jobId, now = new Date()) {
|
|
681
|
-
const { job } = await transition(firestore, orgId, jobId, (current)=>isAiJobTerminal(current.status) ? null : {
|
|
736
|
+
const { job, changed } = await transition(firestore, orgId, jobId, (current)=>isAiJobTerminal(current.status) ? null : {
|
|
682
737
|
status: 'done',
|
|
683
738
|
creditsReserved: 0,
|
|
684
739
|
lease: null,
|
|
685
740
|
error: null,
|
|
686
741
|
updatedAt: now
|
|
687
742
|
});
|
|
743
|
+
if (changed) await announceAiJobTransition(job, 'done');
|
|
688
744
|
return job;
|
|
689
745
|
}
|
|
690
746
|
/**
|
|
@@ -699,7 +755,7 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
699
755
|
stepIndex: (_ref = detail == null ? void 0 : detail.stepIndex) != null ? _ref : null,
|
|
700
756
|
error: (detail == null ? void 0 : detail.error) instanceof Error ? detail.error.message : (_ref1 = detail == null ? void 0 : detail.error) != null ? _ref1 : null
|
|
701
757
|
});
|
|
702
|
-
const { job } = await transition(firestore, orgId, jobId, (current)=>isAiJobTerminal(current.status) ? null : {
|
|
758
|
+
const { job, changed } = await transition(firestore, orgId, jobId, (current)=>isAiJobTerminal(current.status) ? null : {
|
|
703
759
|
status: 'failed',
|
|
704
760
|
error: message,
|
|
705
761
|
creditsReserved: 0,
|
|
@@ -711,6 +767,7 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
711
767
|
}) : step),
|
|
712
768
|
updatedAt: now
|
|
713
769
|
});
|
|
770
|
+
if (changed) await announceAiJobTransition(job, 'failed');
|
|
714
771
|
return job;
|
|
715
772
|
}
|
|
716
773
|
/**
|
|
@@ -760,6 +817,57 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
760
817
|
}
|
|
761
818
|
return result;
|
|
762
819
|
}
|
|
820
|
+
/**
|
|
821
|
+
* Gives back what a job spent when it failed on our side (AGL-3594), and
|
|
822
|
+
* records the give-back on the job — `refundedCredits`, `refundReason` and
|
|
823
|
+
* the count its next key is numbered by. Everything the job spent and has not
|
|
824
|
+
* given back where it delivered nothing; the failing step's own spend where
|
|
825
|
+
* it delivered drafts, which it keeps. `assist-job-refund.ts` has the rule
|
|
826
|
+
* and the bound. Never throws: a give-back that cannot be made leaves the job
|
|
827
|
+
* as the failure left it, metered as every other step.
|
|
828
|
+
*/ async function refundOurFailure(firestore, orgId, job, input) {
|
|
829
|
+
try {
|
|
830
|
+
var _job_outputs, _job_creditsSpent, _job_refundedCredits, _data, _job_refunds;
|
|
831
|
+
const delivered = ((_job_outputs = job.outputs) != null ? _job_outputs : []).some((output)=>output.resource !== 'seo');
|
|
832
|
+
const credits = aiJobRefundCredits({
|
|
833
|
+
delivered,
|
|
834
|
+
jobCredits: (_job_creditsSpent = job.creditsSpent) != null ? _job_creditsSpent : 0,
|
|
835
|
+
stepCredits: input.stepCredits,
|
|
836
|
+
alreadyRefunded: (_job_refundedCredits = job.refundedCredits) != null ? _job_refundedCredits : 0
|
|
837
|
+
});
|
|
838
|
+
if (credits <= 0) return job;
|
|
839
|
+
const free = input.free !== undefined ? input.free : freeAssistAccount((_data = (await firestore.collection('orgs').doc(orgId).get()).data()) != null ? _data : {});
|
|
840
|
+
const ordinal = (_job_refunds = job.refunds) != null ? _job_refunds : 0;
|
|
841
|
+
const refund = await refundJobCredits(firestore, {
|
|
842
|
+
orgId,
|
|
843
|
+
free,
|
|
844
|
+
jobId: job.$id,
|
|
845
|
+
ordinal,
|
|
846
|
+
credits,
|
|
847
|
+
month: assistUsageMonth(input.now),
|
|
848
|
+
day: assistUsageDay(input.now),
|
|
849
|
+
reason: input.reason
|
|
850
|
+
});
|
|
851
|
+
if (refund.status !== 'returned') return job;
|
|
852
|
+
const { job: refunded } = await transition(firestore, orgId, job.$id, (current)=>{
|
|
853
|
+
var _current_refundedCredits, _current_refunds;
|
|
854
|
+
return {
|
|
855
|
+
refundedCredits: ((_current_refundedCredits = current.refundedCredits) != null ? _current_refundedCredits : 0) + credits,
|
|
856
|
+
refundReason: input.reason,
|
|
857
|
+
refunds: ((_current_refunds = current.refunds) != null ? _current_refunds : 0) + 1,
|
|
858
|
+
updatedAt: input.now
|
|
859
|
+
};
|
|
860
|
+
});
|
|
861
|
+
return refunded;
|
|
862
|
+
} catch (error) {
|
|
863
|
+
console.error('ai job refund failed', {
|
|
864
|
+
orgId,
|
|
865
|
+
jobId: job.$id,
|
|
866
|
+
error
|
|
867
|
+
});
|
|
868
|
+
return job;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
763
871
|
/**
|
|
764
872
|
* Resume a job that stopped for a person (AGL-2935): confirm its plan, or
|
|
765
873
|
* try again the step whose answer broke a building rule. One transaction,
|
|
@@ -938,22 +1046,32 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
938
1046
|
const runner = aiJobRunnerForStep(job.kind, step.name);
|
|
939
1047
|
if (!runner) {
|
|
940
1048
|
await releaseHeld();
|
|
1049
|
+
const failed = await failAiJob(firestore, orgId, jobId, AI_JOB_NOT_AVAILABLE_COPY, {
|
|
1050
|
+
stepIndex,
|
|
1051
|
+
error: `no runner registered for kind ${job.kind}`
|
|
1052
|
+
}, now);
|
|
941
1053
|
return {
|
|
942
1054
|
outcome: 'failed',
|
|
943
|
-
job: await
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1055
|
+
job: await refundOurFailure(firestore, orgId, failed, {
|
|
1056
|
+
reason: 'unavailable',
|
|
1057
|
+
stepCredits: 0,
|
|
1058
|
+
now
|
|
1059
|
+
})
|
|
947
1060
|
};
|
|
948
1061
|
}
|
|
949
1062
|
if (((_step_attempts = step.attempts) != null ? _step_attempts : 0) > AI_JOB_STEP_MAX_ATTEMPTS) {
|
|
950
1063
|
await releaseHeld();
|
|
1064
|
+
const failed = await failAiJob(firestore, orgId, jobId, AI_UPSTREAM_FAILURE_COPY, {
|
|
1065
|
+
stepIndex,
|
|
1066
|
+
error: `step ${step.name} exhausted ${AI_JOB_STEP_MAX_ATTEMPTS} attempts`
|
|
1067
|
+
}, now);
|
|
951
1068
|
return {
|
|
952
1069
|
outcome: 'failed',
|
|
953
|
-
job: await
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1070
|
+
job: await refundOurFailure(firestore, orgId, failed, {
|
|
1071
|
+
reason: 'timeout',
|
|
1072
|
+
stepCredits: 0,
|
|
1073
|
+
now
|
|
1074
|
+
})
|
|
957
1075
|
};
|
|
958
1076
|
}
|
|
959
1077
|
const org = (_options_org = options.org) != null ? _options_org : (_data = (await firestore.collection('orgs').doc(orgId).get()).data()) != null ? _data : {};
|
|
@@ -1058,6 +1176,7 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
1058
1176
|
modelFor
|
|
1059
1177
|
});
|
|
1060
1178
|
} catch (error) {
|
|
1179
|
+
var _reservation_free;
|
|
1061
1180
|
// The provider refused the request or the budget ended before it
|
|
1062
1181
|
// answered: no usage came back, so nothing is metered and the message
|
|
1063
1182
|
// goes back to the org. The provider's own words are already in the
|
|
@@ -1078,12 +1197,18 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
1078
1197
|
job: requeued
|
|
1079
1198
|
};
|
|
1080
1199
|
}
|
|
1200
|
+
const failed = await failAiJob(firestore, orgId, jobId, AI_UPSTREAM_FAILURE_COPY, {
|
|
1201
|
+
stepIndex,
|
|
1202
|
+
error
|
|
1203
|
+
}, now);
|
|
1081
1204
|
return {
|
|
1082
1205
|
outcome: 'failed',
|
|
1083
|
-
job: await
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1206
|
+
job: await refundOurFailure(firestore, orgId, failed, {
|
|
1207
|
+
reason: 'provider',
|
|
1208
|
+
stepCredits: 0,
|
|
1209
|
+
free: (_reservation_free = reservation.free) != null ? _reservation_free : null,
|
|
1210
|
+
now
|
|
1211
|
+
})
|
|
1087
1212
|
};
|
|
1088
1213
|
}
|
|
1089
1214
|
const latencyMs = Math.max(0, Date.now() - runStarted);
|
|
@@ -1092,6 +1217,7 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
1092
1217
|
// back and nothing is metered, exactly as for a request the provider
|
|
1093
1218
|
// refused.
|
|
1094
1219
|
if (outcome.failure && aiJobStepSpentNothing(outcome)) {
|
|
1220
|
+
var _reservation_free1;
|
|
1095
1221
|
await releaseAssistMessage(firestore, orgId, reservation).catch((releaseError)=>console.error('ai job release failed', {
|
|
1096
1222
|
orgId,
|
|
1097
1223
|
jobId,
|
|
@@ -1102,12 +1228,18 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
1102
1228
|
creditsSpent: 0,
|
|
1103
1229
|
error: outcome.failure
|
|
1104
1230
|
}, now);
|
|
1231
|
+
const failed = await failAiJob(firestore, orgId, jobId, outcome.failure, {
|
|
1232
|
+
stepIndex,
|
|
1233
|
+
error: `step failure before the provider: ${outcome.failure}`
|
|
1234
|
+
}, now);
|
|
1105
1235
|
return {
|
|
1106
1236
|
outcome: 'failed',
|
|
1107
|
-
job: await
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1237
|
+
job: await refundOurFailure(firestore, orgId, failed, {
|
|
1238
|
+
reason: 'step-failure',
|
|
1239
|
+
stepCredits: 0,
|
|
1240
|
+
free: (_reservation_free1 = reservation.free) != null ? _reservation_free1 : null,
|
|
1241
|
+
now
|
|
1242
|
+
})
|
|
1111
1243
|
};
|
|
1112
1244
|
}
|
|
1113
1245
|
const tokens = {
|
|
@@ -1132,7 +1264,7 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
1132
1264
|
// Tokens were spent, so the bill is written first and its failure is a
|
|
1133
1265
|
// log line, never a failed job — the same rule the chat route keeps.
|
|
1134
1266
|
try {
|
|
1135
|
-
var _job_hostId1,
|
|
1267
|
+
var _job_hostId1, _reservation_free2;
|
|
1136
1268
|
await recordAssistCost(firestore, orgId, {
|
|
1137
1269
|
route: 'ai/jobs',
|
|
1138
1270
|
hostId: (_job_hostId1 = job.hostId) != null ? _job_hostId1 : null,
|
|
@@ -1145,7 +1277,7 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
1145
1277
|
// The account this job drew on, as the reservation decided it
|
|
1146
1278
|
// (AGL-2925): a Free job lands on the owner's allowance and the
|
|
1147
1279
|
// platform's day; a paid one on neither.
|
|
1148
|
-
free: (
|
|
1280
|
+
free: (_reservation_free2 = reservation.free) != null ? _reservation_free2 : null,
|
|
1149
1281
|
// The step is the creator's spend, under the job's kind (AGL-2928).
|
|
1150
1282
|
uid: job.createdBy,
|
|
1151
1283
|
kind: job.kind
|
|
@@ -1163,6 +1295,7 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
1163
1295
|
// model's answer (AGL-2938), fails the job with its own sentence. The
|
|
1164
1296
|
// tokens are already on the bill above.
|
|
1165
1297
|
if (outcome.refused || outcome.failure) {
|
|
1298
|
+
var _reservation_free3;
|
|
1166
1299
|
const message = outcome.refused ? AI_JOB_REFUSED_COPY : outcome.failure;
|
|
1167
1300
|
await recordStep(firestore, orgId, jobId, options.owner, stepIndex, {
|
|
1168
1301
|
status: 'failed',
|
|
@@ -1170,12 +1303,19 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
1170
1303
|
error: message,
|
|
1171
1304
|
tokens
|
|
1172
1305
|
}, now);
|
|
1306
|
+
const failed = await failAiJob(firestore, orgId, jobId, message, {
|
|
1307
|
+
stepIndex,
|
|
1308
|
+
error: outcome.refused ? 'stop_reason refusal' : `step failure: ${message}`
|
|
1309
|
+
}, now);
|
|
1310
|
+
// A model declining the brief is not our failure; a step's own is (AGL-3594).
|
|
1173
1311
|
return {
|
|
1174
1312
|
outcome: 'failed',
|
|
1175
|
-
job: await
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1313
|
+
job: outcome.refused ? failed : await refundOurFailure(firestore, orgId, failed, {
|
|
1314
|
+
reason: 'step-failure',
|
|
1315
|
+
stepCredits: credits,
|
|
1316
|
+
free: (_reservation_free3 = reservation.free) != null ? _reservation_free3 : null,
|
|
1317
|
+
now
|
|
1318
|
+
})
|
|
1179
1319
|
};
|
|
1180
1320
|
}
|
|
1181
1321
|
// A step that stopped for a person parks the job in the same write that
|
|
@@ -1212,6 +1352,8 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
1212
1352
|
plan: outcome.plan
|
|
1213
1353
|
} : {}, review ? {
|
|
1214
1354
|
review
|
|
1355
|
+
} : {}, outcome.sitePublish ? {
|
|
1356
|
+
sitePublish: outcome.sitePublish
|
|
1215
1357
|
} : {}), now);
|
|
1216
1358
|
const actor = (_options_actor = options.actor) != null ? _options_actor : {
|
|
1217
1359
|
uid: job.createdBy
|
|
@@ -1245,7 +1387,30 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
1245
1387
|
}
|
|
1246
1388
|
});
|
|
1247
1389
|
}
|
|
1390
|
+
if ((review == null ? void 0 : review.reason) === 'plan' && recorded.job.status === 'needs_review' && aiJobAutoConfirms(job)) {
|
|
1391
|
+
// A guided site start confirms its own plan (AGL-3594): the resume
|
|
1392
|
+
// door's confirmation, by the job's creator, and the build queued.
|
|
1393
|
+
const resumed = await resumeAiJob(firestore, orgId, jobId, {
|
|
1394
|
+
uid: job.createdBy
|
|
1395
|
+
}, now);
|
|
1396
|
+
await writeAiJobAudit(firestore, {
|
|
1397
|
+
action: 'ai.job.resume',
|
|
1398
|
+
actorUid: job.createdBy,
|
|
1399
|
+
orgId,
|
|
1400
|
+
jobId,
|
|
1401
|
+
after: {
|
|
1402
|
+
status: resumed.job.status,
|
|
1403
|
+
confirmedPlan: true,
|
|
1404
|
+
autoConfirm: true
|
|
1405
|
+
}
|
|
1406
|
+
});
|
|
1407
|
+
return {
|
|
1408
|
+
outcome: 'done',
|
|
1409
|
+
job: resumed.job
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1248
1412
|
if (review && recorded.job.status === 'needs_review') {
|
|
1413
|
+
var _reservation_free4;
|
|
1249
1414
|
// Nobody parked the job but its own step, so the row names no actor, as
|
|
1250
1415
|
// the meter's park does.
|
|
1251
1416
|
await logAiJobNeedsInput(orgId, {
|
|
@@ -1255,9 +1420,15 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
1255
1420
|
kind: job.kind,
|
|
1256
1421
|
reason: review.reason
|
|
1257
1422
|
});
|
|
1423
|
+
// A plan the plan rules still refused is our failure (AGL-3594).
|
|
1258
1424
|
return {
|
|
1259
1425
|
outcome: 'needs_review',
|
|
1260
|
-
job: recorded.job
|
|
1426
|
+
job: outcome.uncredited ? await refundOurFailure(firestore, orgId, recorded.job, {
|
|
1427
|
+
reason: 'plan-refused',
|
|
1428
|
+
stepCredits: credits,
|
|
1429
|
+
free: (_reservation_free4 = reservation.free) != null ? _reservation_free4 : null,
|
|
1430
|
+
now
|
|
1431
|
+
}) : recorded.job
|
|
1261
1432
|
};
|
|
1262
1433
|
}
|
|
1263
1434
|
if (recorded.remaining > 0 || isAiJobTerminal(recorded.job.status)) {
|