@aglyn/plugins-ai 1.0.0-beta.250 → 1.0.0-beta.252
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/package.json +17 -17
- package/src/lib/components/ai-brief-dialog.component.js +15 -1
- package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
- package/src/lib/components/ai-job-links.js +1 -1
- package/src/lib/components/ai-job-links.js.map +1 -1
- package/src/lib/components/ai-job-plan.component.js +20 -2
- package/src/lib/components/ai-job-plan.component.js.map +1 -1
- package/src/lib/components/ai-jobs-store.d.ts +11 -0
- package/src/lib/components/ai-jobs-store.js +22 -0
- package/src/lib/components/ai-jobs-store.js.map +1 -1
- package/src/lib/jobs/ai-job-crm-step.js +7 -4
- package/src/lib/jobs/ai-job-crm-step.js.map +1 -1
- package/src/lib/jobs/ai-job-drafts.d.ts +24 -0
- package/src/lib/jobs/ai-job-drafts.js +46 -0
- package/src/lib/jobs/ai-job-drafts.js.map +1 -1
- package/src/lib/jobs/ai-job-experiment-step.js +6 -1
- package/src/lib/jobs/ai-job-experiment-step.js.map +1 -1
- package/src/lib/jobs/ai-job-form-reuse.d.ts +78 -0
- package/src/lib/jobs/ai-job-form-reuse.js +136 -0
- package/src/lib/jobs/ai-job-form-reuse.js.map +1 -0
- package/src/lib/jobs/ai-job-form-step.d.ts +17 -0
- package/src/lib/jobs/ai-job-form-step.js +63 -11
- package/src/lib/jobs/ai-job-form-step.js.map +1 -1
- package/src/lib/jobs/ai-job-insight-step.js +6 -1
- package/src/lib/jobs/ai-job-insight-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.d.ts +19 -0
- package/src/lib/jobs/ai-job-plan-step.js +39 -1
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-seo-step.js +5 -3
- package/src/lib/jobs/ai-job-seo-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-content.d.ts +7 -5
- package/src/lib/jobs/ai-job-site-content.js +7 -5
- package/src/lib/jobs/ai-job-site-content.js.map +1 -1
- package/src/lib/jobs/ai-job-text-step.js +6 -3
- package/src/lib/jobs/ai-job-text-step.js.map +1 -1
- package/src/lib/jobs/ai-layout-stock-photos.d.ts +11 -7
- package/src/lib/jobs/ai-layout-stock-photos.js +25 -10
- package/src/lib/jobs/ai-layout-stock-photos.js.map +1 -1
- package/src/lib/jobs/ai-stock-business-nouns.d.ts +62 -0
- package/src/lib/jobs/ai-stock-business-nouns.js +264 -0
- package/src/lib/jobs/ai-stock-business-nouns.js.map +1 -0
- package/src/lib/model/ai-build-plan.d.ts +7 -3
- package/src/lib/model/ai-build-plan.js +11 -1
- package/src/lib/model/ai-build-plan.js.map +1 -1
- package/src/lib/model/ai-credit-estimate.d.ts +32 -18
- package/src/lib/model/ai-credit-estimate.js +36 -18
- package/src/lib/model/ai-credit-estimate.js.map +1 -1
- package/src/lib/model/ai-job-activity.d.ts +30 -0
- package/src/lib/model/ai-job-activity.js +35 -0
- package/src/lib/model/ai-job-activity.js.map +1 -1
- package/src/lib/model/ai-media-credits.d.ts +5 -3
- package/src/lib/model/ai-media-credits.js +7 -5
- package/src/lib/model/ai-media-credits.js.map +1 -1
- package/src/lib/model/ai-site-job.js +6 -3
- package/src/lib/model/ai-site-job.js.map +1 -1
- package/src/lib/model/ai-site-store-pages.d.ts +5 -0
- package/src/lib/model/ai-site-store-pages.js +2 -0
- package/src/lib/model/ai-site-store-pages.js.map +1 -1
- package/src/lib/plugin-config.js +2 -2
- package/src/lib/plugin-config.js.map +1 -1
- package/src/lib/providers/anthropic-crm-wording-superseded.d.ts +29 -0
- package/src/lib/providers/anthropic-crm-wording-superseded.js +31 -0
- package/src/lib/providers/anthropic-crm-wording-superseded.js.map +1 -0
- package/src/lib/providers/catalog.d.ts +24 -17
- package/src/lib/providers/catalog.js +63 -30
- package/src/lib/providers/catalog.js.map +1 -1
- package/src/lib/providers/model-choice.d.ts +1 -1
- package/src/lib/providers/model-choice.js +39 -30
- package/src/lib/providers/model-choice.js.map +1 -1
- package/src/lib/providers/routing.d.ts +20 -1
- package/src/lib/providers/routing.js +47 -8
- package/src/lib/providers/routing.js.map +1 -1
- package/src/lib/runtime/ai-dev-env.d.ts +37 -1
- package/src/lib/runtime/ai-dev-env.js +59 -6
- package/src/lib/runtime/ai-dev-env.js.map +1 -1
- package/src/lib/runtime/ai-doctrine-validators.js +3 -1
- package/src/lib/runtime/ai-doctrine-validators.js.map +1 -1
- package/src/lib/runtime/ai-doctrine.js +8 -4
- package/src/lib/runtime/ai-doctrine.js.map +1 -1
- package/src/lib/runtime/ai-eval-live.d.ts +8 -0
- package/src/lib/runtime/ai-eval-live.js +34 -8
- package/src/lib/runtime/ai-eval-live.js.map +1 -1
- package/src/lib/runtime/ai-palette.generated.js +4 -0
- package/src/lib/runtime/ai-palette.generated.js.map +1 -1
- package/src/lib/runtime/ai-runtime.js +17 -2
- package/src/lib/runtime/ai-runtime.js.map +1 -1
- package/src/lib/runtime/seo-fields.js +4 -3
- package/src/lib/runtime/seo-fields.js.map +1 -1
- package/src/lib/server/ai-assist.js +3 -2
- package/src/lib/server/ai-assist.js.map +1 -1
- package/src/lib/server/ai-media-svg.js +4 -0
- package/src/lib/server/ai-media-svg.js.map +1 -1
- package/src/lib/usage/ai-allotment-alerts.js +2 -5
- package/src/lib/usage/ai-allotment-alerts.js.map +1 -1
|
@@ -0,0 +1,78 @@
|
|
|
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
|
+
*/
|
|
17
|
+
import type { AiJob } from '../model/ai-jobs.types';
|
|
18
|
+
import { type AiDraftRecord } from './ai-job-drafts';
|
|
19
|
+
/**
|
|
20
|
+
* A FORM ANOTHER BUILD JUST MADE, PLACED INSTEAD OF REFUSED (AGL-3765).
|
|
21
|
+
*
|
|
22
|
+
* Two page jobs started from one brief on a Free site, minutes apart, were
|
|
23
|
+
* each planned while the site had no form, and each planned a new one. The
|
|
24
|
+
* first to reach its form step made the site's one Free form; the second
|
|
25
|
+
* reached its own ten seconds later, was refused at the band, and stopped
|
|
26
|
+
* without the page it was asked for. A third job, planned after the form
|
|
27
|
+
* existed, placed it by id and was built.
|
|
28
|
+
*
|
|
29
|
+
* So a form UNIT — the form a page, a guided start or a build makes before
|
|
30
|
+
* the page that places it — that the band refuses looks for a form another AI
|
|
31
|
+
* job made on the same site, and reports that form as what the unit built.
|
|
32
|
+
* The page then binds it by id, exactly as it binds a form the site always
|
|
33
|
+
* had. Another job's form qualifies when the site holds it now, in use, and it
|
|
34
|
+
* was made in the last {@link AI_FORM_REUSE_WINDOW_MS}, or by a job of the
|
|
35
|
+
* same brief. Which job made it is read from what the jobs record: the
|
|
36
|
+
* form's id is the id a job's plan minted for its form creation before the
|
|
37
|
+
* write (`AiBuildPlanCreate.id`), and the output the job recorded after it.
|
|
38
|
+
*
|
|
39
|
+
* A job the member asked for a form by itself never borrows one: it was asked
|
|
40
|
+
* for a new form, not for a page that needs one.
|
|
41
|
+
*/
|
|
42
|
+
/** How recently another job must have made a form for a refused unit to place it. */
|
|
43
|
+
export declare const AI_FORM_REUSE_WINDOW_MS: number;
|
|
44
|
+
/** What the unit's output says when it placed another build's form. */
|
|
45
|
+
export declare const AI_FORM_REUSED_NOTE = "Reused the form another build just made.";
|
|
46
|
+
/** How many of the site's newest jobs are read for the form's maker. */
|
|
47
|
+
export declare const AI_FORM_REUSE_JOBS_READ = 20;
|
|
48
|
+
/** The fields of a job this module reads. */
|
|
49
|
+
export type AiFormReuseJob = Pick<AiJob, '$id' | 'hostId' | 'brief' | 'outputs' | 'plan'>;
|
|
50
|
+
/**
|
|
51
|
+
* The forms other jobs on this site made or were about to make, each with
|
|
52
|
+
* whether its job had the same brief as the job the member started. Read off
|
|
53
|
+
* each job's recorded outputs and its plan's minted form ids; the job the
|
|
54
|
+
* member started is never another job.
|
|
55
|
+
*/
|
|
56
|
+
export declare function aiSiblingJobFormIds(jobs: readonly AiFormReuseJob[], input: {
|
|
57
|
+
hostId: string;
|
|
58
|
+
originJobId: string;
|
|
59
|
+
}): Map<string, {
|
|
60
|
+
sameBrief: boolean;
|
|
61
|
+
}>;
|
|
62
|
+
export interface AiSiblingJobFormDeps {
|
|
63
|
+
/** The site's newest jobs; the machine's list otherwise. */
|
|
64
|
+
listJobs?: (firestore: FirebaseFirestore.Firestore, orgId: string, options: {
|
|
65
|
+
hostId: string;
|
|
66
|
+
limit: number;
|
|
67
|
+
}) => Promise<readonly AiFormReuseJob[]>;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The form another AI job made on this site that a refused form unit places
|
|
71
|
+
* instead (AGL-3765), as the draft record the unit reports; `null` when the
|
|
72
|
+
* job is not a unit, or no other job's form qualifies. The newest qualifying
|
|
73
|
+
* form wins.
|
|
74
|
+
*/
|
|
75
|
+
export declare function aiSiblingJobForm(firestore: FirebaseFirestore.Firestore, input: {
|
|
76
|
+
job: Pick<AiJob, '$id' | 'orgId' | 'hostId' | 'inputs'>;
|
|
77
|
+
now: Date;
|
|
78
|
+
}, deps?: AiSiblingJobFormDeps): Promise<AiDraftRecord | null>;
|
|
@@ -0,0 +1,136 @@
|
|
|
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 { isFormArchived } from "@aglyn/aglyn/app-utils/forms";
|
|
17
|
+
import { aiOriginJobId } from "./ai-job-draft-ids.js";
|
|
18
|
+
import { aiDraftInstantMs, aiSiteSubdomain } from "./ai-job-drafts.js";
|
|
19
|
+
import { listAiJobs } from "./ai-jobs.js";
|
|
20
|
+
/**
|
|
21
|
+
* A FORM ANOTHER BUILD JUST MADE, PLACED INSTEAD OF REFUSED (AGL-3765).
|
|
22
|
+
*
|
|
23
|
+
* Two page jobs started from one brief on a Free site, minutes apart, were
|
|
24
|
+
* each planned while the site had no form, and each planned a new one. The
|
|
25
|
+
* first to reach its form step made the site's one Free form; the second
|
|
26
|
+
* reached its own ten seconds later, was refused at the band, and stopped
|
|
27
|
+
* without the page it was asked for. A third job, planned after the form
|
|
28
|
+
* existed, placed it by id and was built.
|
|
29
|
+
*
|
|
30
|
+
* So a form UNIT — the form a page, a guided start or a build makes before
|
|
31
|
+
* the page that places it — that the band refuses looks for a form another AI
|
|
32
|
+
* job made on the same site, and reports that form as what the unit built.
|
|
33
|
+
* The page then binds it by id, exactly as it binds a form the site always
|
|
34
|
+
* had. Another job's form qualifies when the site holds it now, in use, and it
|
|
35
|
+
* was made in the last {@link AI_FORM_REUSE_WINDOW_MS}, or by a job of the
|
|
36
|
+
* same brief. Which job made it is read from what the jobs record: the
|
|
37
|
+
* form's id is the id a job's plan minted for its form creation before the
|
|
38
|
+
* write (`AiBuildPlanCreate.id`), and the output the job recorded after it.
|
|
39
|
+
*
|
|
40
|
+
* A job the member asked for a form by itself never borrows one: it was asked
|
|
41
|
+
* for a new form, not for a page that needs one.
|
|
42
|
+
*/ /** How recently another job must have made a form for a refused unit to place it. */ export const AI_FORM_REUSE_WINDOW_MS = 15 * 60000;
|
|
43
|
+
/** What the unit's output says when it placed another build's form. */ export const AI_FORM_REUSED_NOTE = 'Reused the form another build just made.';
|
|
44
|
+
/** How many of the site's newest jobs are read for the form's maker. */ export const AI_FORM_REUSE_JOBS_READ = 20;
|
|
45
|
+
/** A brief as two jobs of the same brief share it: case and spacing aside. */ function briefKey(brief) {
|
|
46
|
+
return (brief != null ? brief : '').trim().replace(/\s+/g, ' ').toLowerCase();
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The forms other jobs on this site made or were about to make, each with
|
|
50
|
+
* whether its job had the same brief as the job the member started. Read off
|
|
51
|
+
* each job's recorded outputs and its plan's minted form ids; the job the
|
|
52
|
+
* member started is never another job.
|
|
53
|
+
*/ export function aiSiblingJobFormIds(jobs, input) {
|
|
54
|
+
const origin = jobs.find((job)=>job.$id === input.originJobId);
|
|
55
|
+
const brief = origin ? briefKey(origin.brief) : '';
|
|
56
|
+
const found = new Map();
|
|
57
|
+
for (const job of jobs){
|
|
58
|
+
var _job_outputs, _ref;
|
|
59
|
+
var _job_plan;
|
|
60
|
+
if (job.$id === input.originJobId || job.hostId !== input.hostId) continue;
|
|
61
|
+
const sameBrief = Boolean(brief) && briefKey(job.brief) === brief;
|
|
62
|
+
const ids = [
|
|
63
|
+
...((_job_outputs = job.outputs) != null ? _job_outputs : []).filter((output)=>{
|
|
64
|
+
var _output_hostId;
|
|
65
|
+
return output.resource === 'form' && ((_output_hostId = output.hostId) != null ? _output_hostId : input.hostId) === input.hostId;
|
|
66
|
+
}).map((output)=>output.id),
|
|
67
|
+
...((_ref = (_job_plan = job.plan) == null ? void 0 : _job_plan.create) != null ? _ref : []).filter((entry)=>entry.kind === 'form').map((entry)=>entry.id)
|
|
68
|
+
];
|
|
69
|
+
for (const id of ids){
|
|
70
|
+
var _ref1;
|
|
71
|
+
var _found_get;
|
|
72
|
+
if (!id) continue;
|
|
73
|
+
found.set(id, {
|
|
74
|
+
sameBrief: sameBrief || ((_ref1 = (_found_get = found.get(id)) == null ? void 0 : _found_get.sameBrief) != null ? _ref1 : false)
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return found;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The form another AI job made on this site that a refused form unit places
|
|
82
|
+
* instead (AGL-3765), as the draft record the unit reports; `null` when the
|
|
83
|
+
* job is not a unit, or no other job's form qualifies. The newest qualifying
|
|
84
|
+
* form wins.
|
|
85
|
+
*/ export async function aiSiblingJobForm(firestore, input, deps = {}) {
|
|
86
|
+
var _deps_listJobs;
|
|
87
|
+
const { job } = input;
|
|
88
|
+
const hostId = job.hostId;
|
|
89
|
+
const originJobId = aiOriginJobId(job);
|
|
90
|
+
// Only a unit of a job that places the form; a form job builds its own.
|
|
91
|
+
if (!hostId || originJobId === job.$id) return null;
|
|
92
|
+
const listJobs = (_deps_listJobs = deps.listJobs) != null ? _deps_listJobs : listAiJobs;
|
|
93
|
+
const jobs = await listJobs(firestore, job.orgId, {
|
|
94
|
+
hostId,
|
|
95
|
+
limit: AI_FORM_REUSE_JOBS_READ
|
|
96
|
+
});
|
|
97
|
+
const candidates = aiSiblingJobFormIds(jobs, {
|
|
98
|
+
hostId,
|
|
99
|
+
originJobId
|
|
100
|
+
});
|
|
101
|
+
candidates.delete(job.$id);
|
|
102
|
+
if (!candidates.size) return null;
|
|
103
|
+
const forms = firestore.collection('hosts').doc(hostId).collection('forms');
|
|
104
|
+
const read = await Promise.all([
|
|
105
|
+
...candidates.entries()
|
|
106
|
+
].map(async ([id, { sameBrief }])=>({
|
|
107
|
+
id,
|
|
108
|
+
sameBrief,
|
|
109
|
+
doc: await forms.doc(id).get()
|
|
110
|
+
})));
|
|
111
|
+
const now = input.now.getTime();
|
|
112
|
+
const qualifying = read.filter(({ doc })=>doc.exists && !isFormArchived({
|
|
113
|
+
archivedAt: doc.get('archivedAt')
|
|
114
|
+
})).map(({ id, sameBrief, doc })=>{
|
|
115
|
+
var _doc_get;
|
|
116
|
+
return {
|
|
117
|
+
id,
|
|
118
|
+
sameBrief,
|
|
119
|
+
name: String((_doc_get = doc.get('displayName')) != null ? _doc_get : ''),
|
|
120
|
+
createdAt: aiDraftInstantMs(doc.get('createdAt'))
|
|
121
|
+
};
|
|
122
|
+
}).filter((form)=>form.sameBrief || form.createdAt !== null && now - form.createdAt <= AI_FORM_REUSE_WINDOW_MS).sort((a, b)=>{
|
|
123
|
+
var _b_createdAt, _a_createdAt;
|
|
124
|
+
return ((_b_createdAt = b.createdAt) != null ? _b_createdAt : 0) - ((_a_createdAt = a.createdAt) != null ? _a_createdAt : 0);
|
|
125
|
+
});
|
|
126
|
+
const [form] = qualifying;
|
|
127
|
+
if (!form) return null;
|
|
128
|
+
return {
|
|
129
|
+
id: form.id,
|
|
130
|
+
versionId: null,
|
|
131
|
+
name: form.name,
|
|
132
|
+
hostSubdomain: await aiSiteSubdomain(firestore, hostId)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
//# sourceMappingURL=ai-job-form-reuse.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-form-reuse.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 { isFormArchived } from '@aglyn/aglyn/app-utils/forms'\nimport type { AiJob } from '../model/ai-jobs.types'\nimport { aiOriginJobId } from './ai-job-draft-ids'\nimport { aiDraftInstantMs, aiSiteSubdomain, type AiDraftRecord } from './ai-job-drafts'\nimport { listAiJobs } from './ai-jobs'\n\n/**\n * A FORM ANOTHER BUILD JUST MADE, PLACED INSTEAD OF REFUSED (AGL-3765).\n *\n * Two page jobs started from one brief on a Free site, minutes apart, were\n * each planned while the site had no form, and each planned a new one. The\n * first to reach its form step made the site's one Free form; the second\n * reached its own ten seconds later, was refused at the band, and stopped\n * without the page it was asked for. A third job, planned after the form\n * existed, placed it by id and was built.\n *\n * So a form UNIT — the form a page, a guided start or a build makes before\n * the page that places it — that the band refuses looks for a form another AI\n * job made on the same site, and reports that form as what the unit built.\n * The page then binds it by id, exactly as it binds a form the site always\n * had. Another job's form qualifies when the site holds it now, in use, and it\n * was made in the last {@link AI_FORM_REUSE_WINDOW_MS}, or by a job of the\n * same brief. Which job made it is read from what the jobs record: the\n * form's id is the id a job's plan minted for its form creation before the\n * write (`AiBuildPlanCreate.id`), and the output the job recorded after it.\n *\n * A job the member asked for a form by itself never borrows one: it was asked\n * for a new form, not for a page that needs one.\n */\n\n/** How recently another job must have made a form for a refused unit to place it. */\nexport const AI_FORM_REUSE_WINDOW_MS = 15 * 60_000\n\n/** What the unit's output says when it placed another build's form. */\nexport const AI_FORM_REUSED_NOTE = 'Reused the form another build just made.'\n\n/** How many of the site's newest jobs are read for the form's maker. */\nexport const AI_FORM_REUSE_JOBS_READ = 20\n\n/** The fields of a job this module reads. */\nexport type AiFormReuseJob = Pick<AiJob, '$id' | 'hostId' | 'brief' | 'outputs' | 'plan'>\n\n/** A brief as two jobs of the same brief share it: case and spacing aside. */\nfunction briefKey(brief: string | null | undefined): string {\n return (brief ?? '').trim().replace(/\\s+/g, ' ').toLowerCase()\n}\n\n/**\n * The forms other jobs on this site made or were about to make, each with\n * whether its job had the same brief as the job the member started. Read off\n * each job's recorded outputs and its plan's minted form ids; the job the\n * member started is never another job.\n */\nexport function aiSiblingJobFormIds(\n jobs: readonly AiFormReuseJob[],\n input: { hostId: string; originJobId: string },\n): Map<string, { sameBrief: boolean }> {\n const origin = jobs.find((job) => job.$id === input.originJobId)\n const brief = origin ? briefKey(origin.brief) : ''\n const found = new Map<string, { sameBrief: boolean }>()\n for (const job of jobs) {\n if (job.$id === input.originJobId || job.hostId !== input.hostId) continue\n const sameBrief = Boolean(brief) && briefKey(job.brief) === brief\n const ids = [\n ...(job.outputs ?? [])\n .filter((output) => output.resource === 'form' && (output.hostId ?? input.hostId) === input.hostId)\n .map((output) => output.id),\n ...(job.plan?.create ?? []).filter((entry) => entry.kind === 'form').map((entry) => entry.id),\n ]\n for (const id of ids) {\n if (!id) continue\n found.set(id, { sameBrief: sameBrief || (found.get(id)?.sameBrief ?? false) })\n }\n }\n return found\n}\n\nexport interface AiSiblingJobFormDeps {\n /** The site's newest jobs; the machine's list otherwise. */\n listJobs?: (\n firestore: FirebaseFirestore.Firestore,\n orgId: string,\n options: { hostId: string; limit: number },\n ) => Promise<readonly AiFormReuseJob[]>\n}\n\n/**\n * The form another AI job made on this site that a refused form unit places\n * instead (AGL-3765), as the draft record the unit reports; `null` when the\n * job is not a unit, or no other job's form qualifies. The newest qualifying\n * form wins.\n */\nexport async function aiSiblingJobForm(\n firestore: FirebaseFirestore.Firestore,\n input: { job: Pick<AiJob, '$id' | 'orgId' | 'hostId' | 'inputs'>; now: Date },\n deps: AiSiblingJobFormDeps = {},\n): Promise<AiDraftRecord | null> {\n const { job } = input\n const hostId = job.hostId\n const originJobId = aiOriginJobId(job)\n // Only a unit of a job that places the form; a form job builds its own.\n if (!hostId || originJobId === job.$id) return null\n const listJobs = deps.listJobs ?? listAiJobs\n const jobs = await listJobs(firestore, job.orgId, { hostId, limit: AI_FORM_REUSE_JOBS_READ })\n const candidates = aiSiblingJobFormIds(jobs, { hostId, originJobId })\n candidates.delete(job.$id)\n if (!candidates.size) return null\n const forms = firestore.collection('hosts').doc(hostId).collection('forms')\n const read = await Promise.all(\n [...candidates.entries()].map(async ([id, { sameBrief }]) => ({ id, sameBrief, doc: await forms.doc(id).get() })),\n )\n const now = input.now.getTime()\n const qualifying = read\n .filter(({ doc }) => doc.exists && !isFormArchived({ archivedAt: doc.get('archivedAt') }))\n .map(({ id, sameBrief, doc }) => ({\n id,\n sameBrief,\n name: String(doc.get('displayName') ?? ''),\n createdAt: aiDraftInstantMs(doc.get('createdAt')),\n }))\n .filter(\n (form) =>\n form.sameBrief || (form.createdAt !== null && now - form.createdAt <= AI_FORM_REUSE_WINDOW_MS),\n )\n .sort((a, b) => (b.createdAt ?? 0) - (a.createdAt ?? 0))\n const [form] = qualifying\n if (!form) return null\n return {\n id: form.id,\n versionId: null,\n name: form.name,\n hostSubdomain: await aiSiteSubdomain(firestore, hostId),\n }\n}\n"],"names":["isFormArchived","aiOriginJobId","aiDraftInstantMs","aiSiteSubdomain","listAiJobs","AI_FORM_REUSE_WINDOW_MS","AI_FORM_REUSED_NOTE","AI_FORM_REUSE_JOBS_READ","briefKey","brief","trim","replace","toLowerCase","aiSiblingJobFormIds","jobs","input","origin","find","job","$id","originJobId","found","Map","hostId","sameBrief","Boolean","ids","outputs","filter","output","resource","map","id","plan","create","entry","kind","set","get","aiSiblingJobForm","firestore","deps","listJobs","orgId","limit","candidates","delete","size","forms","collection","doc","read","Promise","all","entries","now","getTime","qualifying","exists","archivedAt","name","String","createdAt","form","sort","a","b","versionId","hostSubdomain"],"mappings":"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"}
|
|
@@ -25,6 +25,7 @@ import type { AiSystemBlock, AiTool } from '../runtime/ai-runtime';
|
|
|
25
25
|
import { readSiteInventory } from '../runtime/site-inventory';
|
|
26
26
|
import { type AiJobAdmission } from './ai-job-admission';
|
|
27
27
|
import { type AiFormDataset, type AiFormDatasetBinding, type AiFormDatasetReadDeps } from './ai-job-form-dataset';
|
|
28
|
+
import { aiSiblingJobForm } from './ai-job-form-reuse';
|
|
28
29
|
import type { AiJobStepRunner } from './ai-job-text-step';
|
|
29
30
|
/**
|
|
30
31
|
* The form step (AGL-2913): the generation step of a `form` job, run once a
|
|
@@ -64,6 +65,14 @@ import type { AiJobStepRunner } from './ai-job-text-step';
|
|
|
64
65
|
* one (AGL-3024). A copy still bound to the form it was copied from is a
|
|
65
66
|
* failure, never an output.
|
|
66
67
|
*
|
|
68
|
+
* A site that holds every form its plan includes refuses the draft
|
|
69
|
+
* (AGL-3765). A form the step builds for a page (a unit of a page job, a
|
|
70
|
+
* guided start or a build) then places the form another AI job made on the
|
|
71
|
+
* site moments before (`ai-job-form-reuse.ts`), as two jobs started from one
|
|
72
|
+
* brief race to. Failing that, a site that still had room when the plan was
|
|
73
|
+
* confirmed stops the job on our side, its credits given back; one that was
|
|
74
|
+
* already full is the workspace's limit, for the member to change.
|
|
75
|
+
*
|
|
67
76
|
* Like the other generation steps, it runs on the job beat, never at an inline
|
|
68
77
|
* door: it registers the least time its lookup rounds, its answer and its
|
|
69
78
|
* re-ask need (AGL-3035), and runs without extended thinking under the routing
|
|
@@ -301,6 +310,12 @@ export declare function aiJobFormPrompt(job: Pick<AiJob, 'brief'>, plan: AiJobPl
|
|
|
301
310
|
* (AGL-3143 §15).
|
|
302
311
|
*/
|
|
303
312
|
export declare const aiFormJobAdmission: AiJobAdmission;
|
|
313
|
+
/**
|
|
314
|
+
* What a form step says when the site's last form was taken after its plan
|
|
315
|
+
* was confirmed (AGL-3765): by another build running beside it, or by a
|
|
316
|
+
* member. The failure is ours, so what the job spent is given back.
|
|
317
|
+
*/
|
|
318
|
+
export declare const AI_FORM_BAND_TAKEN_COPY = "Another build on this site used the last form your plan includes while this one was running, so this one stopped. Describe it again to build it with the form the site has now.";
|
|
304
319
|
export interface AiJobFormStepDeps {
|
|
305
320
|
/** The inventory reader; specs hand in a fake. */
|
|
306
321
|
readInventory?: typeof readSiteInventory;
|
|
@@ -308,6 +323,8 @@ export interface AiJobFormStepDeps {
|
|
|
308
323
|
duplicate?: typeof duplicateResource;
|
|
309
324
|
/** The data plugin's writer lookup the form's dataset is read through (AGL-3616); specs hand in a fake. */
|
|
310
325
|
datasetWriterFor?: AiFormDatasetReadDeps['writerFor'];
|
|
326
|
+
/** The form another build just made that a refused unit places (AGL-3765); specs hand in a fake. */
|
|
327
|
+
siblingForm?: typeof aiSiblingJobForm;
|
|
311
328
|
}
|
|
312
329
|
export declare function createAiJobFormStep(deps?: AiJobFormStepDeps): AiJobStepRunner;
|
|
313
330
|
export declare const runAiJobFormStep: AiJobStepRunner;
|
|
@@ -31,7 +31,8 @@ import { registerAiJobAdmission } from "./ai-job-admission.js";
|
|
|
31
31
|
import { AI_FORM_DATASET_MADE_INPUT, aiBindFormToDataset, aiFormDatasetNote, aiFormDatasetPromptLine, aiReadFormDataset } from "./ai-job-form-dataset.js";
|
|
32
32
|
import { aiJobPlanCreationsRefusal } from "./ai-job-plan-creations.js";
|
|
33
33
|
import { aiJobDraftId } from "./ai-job-draft-ids.js";
|
|
34
|
-
import { aiDraftAdmissionRefusal, aiDraftAllowanceRefusal, aiSiteSubdomain, readAiDraft, writeAiDraft } from "./ai-job-drafts.js";
|
|
34
|
+
import { aiDraftAdmissionRefusal, aiDraftAllowanceRefusal, aiSiteSubdomain, readAiDraft, readAiDraftBandTakenSince, writeAiDraft } from "./ai-job-drafts.js";
|
|
35
|
+
import { AI_FORM_REUSED_NOTE, aiSiblingJobForm } from "./ai-job-form-reuse.js";
|
|
35
36
|
import { aiBracketedFactsNote, aiConfirmedPlan, aiDoctrineReview, aiGenerationSpent, aiJobBriefLine, aiLimitReview, aiModelNodeIds, aiPlanCreation, aiPlanReferenceLines, aiUnspentOutcome } from "./ai-job-generation.js";
|
|
36
37
|
import { aiJobStepBudget } from "./ai-job-budget.js";
|
|
37
38
|
import { registerAiJobStep } from "./ai-jobs.js";
|
|
@@ -73,6 +74,14 @@ import { registerAiJobStep } from "./ai-jobs.js";
|
|
|
73
74
|
* one (AGL-3024). A copy still bound to the form it was copied from is a
|
|
74
75
|
* failure, never an output.
|
|
75
76
|
*
|
|
77
|
+
* A site that holds every form its plan includes refuses the draft
|
|
78
|
+
* (AGL-3765). A form the step builds for a page (a unit of a page job, a
|
|
79
|
+
* guided start or a build) then places the form another AI job made on the
|
|
80
|
+
* site moments before (`ai-job-form-reuse.ts`), as two jobs started from one
|
|
81
|
+
* brief race to. Failing that, a site that still had room when the plan was
|
|
82
|
+
* confirmed stops the job on our side, its credits given back; one that was
|
|
83
|
+
* already full is the workspace's limit, for the member to change.
|
|
84
|
+
*
|
|
76
85
|
* Like the other generation steps, it runs on the job beat, never at an inline
|
|
77
86
|
* door: it registers the least time its lookup rounds, its answer and its
|
|
78
87
|
* re-ask need (AGL-3035), and runs without extended thinking under the routing
|
|
@@ -611,10 +620,16 @@ function listOf(items) {
|
|
|
611
620
|
org: context.org,
|
|
612
621
|
ownCheck: (hostId)=>aiJobPlanCreationsRefusal('form', context, hostId)
|
|
613
622
|
});
|
|
623
|
+
/**
|
|
624
|
+
* What a form step says when the site's last form was taken after its plan
|
|
625
|
+
* was confirmed (AGL-3765): by another build running beside it, or by a
|
|
626
|
+
* member. The failure is ours, so what the job spent is given back.
|
|
627
|
+
*/ export const AI_FORM_BAND_TAKEN_COPY = 'Another build on this site used the last form your plan includes while this one was running, so this one stopped. Describe it again to build it with the form the site has now.';
|
|
614
628
|
export function createAiJobFormStep(deps = {}) {
|
|
615
|
-
var _deps_readInventory, _deps_duplicate;
|
|
629
|
+
var _deps_readInventory, _deps_duplicate, _deps_siblingForm;
|
|
616
630
|
const readInventory = (_deps_readInventory = deps.readInventory) != null ? _deps_readInventory : readSiteInventory;
|
|
617
631
|
const duplicate = (_deps_duplicate = deps.duplicate) != null ? _deps_duplicate : duplicateResource;
|
|
632
|
+
const siblingForm = (_deps_siblingForm = deps.siblingForm) != null ? _deps_siblingForm : aiSiblingJobForm;
|
|
618
633
|
return async ({ job, now, signal, firestore, modelFor })=>{
|
|
619
634
|
var _ref, _orgSnapshot_data;
|
|
620
635
|
var _job_inputs;
|
|
@@ -656,6 +671,49 @@ export function createAiJobFormStep(deps = {}) {
|
|
|
656
671
|
const plan = aiConfirmedPlan(job);
|
|
657
672
|
const creation = aiPlanCreation(plan, 'form');
|
|
658
673
|
const name = (creation == null ? void 0 : creation.name) || AI_JOB_FORM_DEFAULT_NAME;
|
|
674
|
+
// The site holds every form its plan includes (AGL-3765). A unit places a
|
|
675
|
+
// form another build on this site just made, as its page would place any
|
|
676
|
+
// form the site has. Otherwise a site that had room when the plan was
|
|
677
|
+
// confirmed and was filled since stopped this job on our side, given
|
|
678
|
+
// back; one that was already full is the workspace's limit to change. A
|
|
679
|
+
// lookup that fails leaves the refusal as it was.
|
|
680
|
+
const refused = async (refusal, spent)=>{
|
|
681
|
+
const lookup = async (read, fallback)=>{
|
|
682
|
+
try {
|
|
683
|
+
return await read();
|
|
684
|
+
} catch (error) {
|
|
685
|
+
console.error('ai form step: band refusal lookup failed', {
|
|
686
|
+
jobId: job.$id,
|
|
687
|
+
hostId,
|
|
688
|
+
error
|
|
689
|
+
});
|
|
690
|
+
return fallback;
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
const reused = await lookup(()=>siblingForm(firestore, {
|
|
694
|
+
job,
|
|
695
|
+
now
|
|
696
|
+
}), null);
|
|
697
|
+
if (reused) return _extends({}, spent, {
|
|
698
|
+
outputs: [
|
|
699
|
+
output(reused, {
|
|
700
|
+
note: AI_FORM_REUSED_NOTE
|
|
701
|
+
})
|
|
702
|
+
]
|
|
703
|
+
});
|
|
704
|
+
const taken = await lookup(()=>readAiDraftBandTakenSince(firestore, {
|
|
705
|
+
kind: 'form',
|
|
706
|
+
hostId,
|
|
707
|
+
org,
|
|
708
|
+
since: plan == null ? void 0 : plan.confirmedAt
|
|
709
|
+
}), false);
|
|
710
|
+
if (taken) return _extends({}, spent, {
|
|
711
|
+
failure: AI_FORM_BAND_TAKEN_COPY
|
|
712
|
+
});
|
|
713
|
+
return _extends({}, spent, {
|
|
714
|
+
review: aiLimitReview(refusal)
|
|
715
|
+
});
|
|
716
|
+
};
|
|
659
717
|
// The plan starts from a copy of a form the site has (rule 15): the copy
|
|
660
718
|
// is the draft, and nothing is generated. A source gone since the plan
|
|
661
719
|
// was made is built from the brief instead, and one that does not collect
|
|
@@ -725,9 +783,7 @@ export function createAiJobFormStep(deps = {}) {
|
|
|
725
783
|
});
|
|
726
784
|
}
|
|
727
785
|
if (copy.ok === false && copy.status === 403) {
|
|
728
|
-
return aiUnspentOutcome(model
|
|
729
|
-
review: aiLimitReview(copy.error)
|
|
730
|
-
});
|
|
786
|
+
return refused(copy.error, aiUnspentOutcome(model));
|
|
731
787
|
}
|
|
732
788
|
}
|
|
733
789
|
const allowance = await aiDraftAllowanceRefusal(firestore, {
|
|
@@ -735,9 +791,7 @@ export function createAiJobFormStep(deps = {}) {
|
|
|
735
791
|
hostId,
|
|
736
792
|
org
|
|
737
793
|
});
|
|
738
|
-
if (allowance) return aiUnspentOutcome(model
|
|
739
|
-
review: aiLimitReview(allowance)
|
|
740
|
-
});
|
|
794
|
+
if (allowance) return refused(allowance, aiUnspentOutcome(model));
|
|
741
795
|
// What the person answered about this form, where they were asked
|
|
742
796
|
// (AGL-2918): a scaffold's form unit carries the guided start's inputs.
|
|
743
797
|
const decisions = {
|
|
@@ -818,9 +872,7 @@ export function createAiJobFormStep(deps = {}) {
|
|
|
818
872
|
});
|
|
819
873
|
if (write.ok === false) {
|
|
820
874
|
if (write.status === 404) throw new Error(`site ${hostId} vanished while its form was generated`);
|
|
821
|
-
return
|
|
822
|
-
review: aiLimitReview(write.error)
|
|
823
|
-
});
|
|
875
|
+
return refused(write.error, spent);
|
|
824
876
|
}
|
|
825
877
|
// What the person decides next, then the facts the brief did not give (AGL-3056).
|
|
826
878
|
const note = [
|