@aglyn/plugins-ai 1.0.0-beta.225 → 1.0.0-beta.227
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 -1
- 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 +35 -17
- package/src/lib/components/ai-brief-dialog.component.js.map +1 -1
- package/src/lib/components/ai-describe-automation.component.js +17 -2
- package/src/lib/components/ai-describe-automation.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 +14 -0
- package/src/lib/components/ai-job-events.js +76 -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-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-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 +3 -3
- package/src/lib/components/ai-page-brief-dialog.component.js.map +1 -1
- package/src/lib/components/ai-site-build-page.component.d.ts +23 -0
- package/src/lib/components/ai-site-build-page.component.js +377 -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 +1 -1
- package/src/lib/components/ai-site-start-card.component.js +522 -130
- 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 +62 -6
- 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-plan-step.d.ts +33 -2
- package/src/lib/jobs/ai-job-plan-step.js +161 -17
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-site-step.d.ts +7 -0
- package/src/lib/jobs/ai-job-site-step.js +34 -4
- package/src/lib/jobs/ai-job-site-step.js.map +1 -1
- package/src/lib/jobs/ai-job-text-step.d.ts +7 -0
- 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 +31 -4
- package/src/lib/jobs/ai-jobs.js +196 -32
- package/src/lib/jobs/ai-jobs.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 +61 -0
- package/src/lib/model/ai-job-notice.js +112 -0
- package/src/lib/model/ai-job-notice.js.map +1 -0
- package/src/lib/model/ai-jobs.types.d.ts +26 -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 +7 -0
- package/src/lib/model/ai-plan-capabilities.js.map +1 -1
- package/src/lib/model/ai-site-build-progress.d.ts +45 -0
- package/src/lib/model/ai-site-build-progress.js +76 -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.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/model/ai-template-subjects.d.ts +4 -3
- package/src/lib/model/ai-template-subjects.js +5 -4
- package/src/lib/model/ai-template-subjects.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 +28 -0
- 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.js +31 -4
- package/src/lib/runtime/ai-doctrine-validators.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/ai-palette.generated.js +12 -4
- package/src/lib/runtime/ai-palette.generated.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-route.js +20 -2
- 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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-text-step.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 { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport type {\n AiJob,\n AiJobOutput,\n AiJobPlan,\n AiJobReview,\n} from '../model/ai-jobs.types'\nimport { AI_STEP_TIERS, type AiStepKind } from '../providers/catalog'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport { runAiRequest, type AiEffort, type AiSystemBlock } from '../runtime/ai-runtime'\nimport type { AssistTokenUsage } from '../usage/assist-usage'\nimport { aiJobStepBudget } from './ai-job-budget'\n\n/**\n * The `text` step (AGL-2904): a brief in, a short piece of copy out.\n *\n * The one step kind this issue ships for real, so the state machine — the\n * lease, the reservation, the record, the beat — is exercised end to end by\n * something that spends tokens, rather than by a stub that could pass with\n * the meter unplugged. Every other kind refuses until its own issue lands.\n *\n * The balanced tier with adaptive thinking: a brief is open-ended in a way the copy\n * assistant's element mode is not, and the cost is one call per job. The\n * static system block is cached; the brief rides in the user turn, so no\n * per-org byte sits inside the cached prefix (AGL-2352, enforced by the\n * runtime before the request leaves).\n */\n\n/** The model the text step runs on: the routing table's answer for `job.text`. */\nexport function aiJobTextModel(): string {\n return aiModelForStep('job.text')\n}\n\n/** The text step's answer ceiling, from the routing table. */\nexport const AI_JOB_TEXT_MAX_TOKENS = AI_ROUTING_TABLE['job.text'].maxTokens\n\n/**\n * The text step's time (AGL-3035): ONE request — no re-ask, and no site\n * inventory to look anything up in — at the routing table's ceiling on the\n * tier `job.text` is served from, with the step's reads and writes. That fits\n * an inline door's budget, so a door runs a text job where it is asked for,\n * and the least time is the helper's worst case rather than a figure anyone\n * chose; a slower tier asks less so its worst case still fits.\n */\nexport const AI_JOB_TEXT_STEP_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS['job.text'],\n maxTokens: AI_JOB_TEXT_MAX_TOKENS,\n attempts: 1,\n lookups: 0,\n})\n\n/** The least time one text step needs before it starts. */\nexport const AI_JOB_TEXT_STEP_MINIMUM_MS = AI_JOB_TEXT_STEP_BUDGET.minimumMs\n\n/** How much of a brief is sent. Past this a brief is a document, not a brief. */\nexport const AI_JOB_BRIEF_MAX_CHARS = 4_000\n\n/**\n * What a step hands back to the machine. The machine meters `usage` at\n * `model`'s rates, records the step and appends the outputs; a step never\n * touches the job document or the meter itself.\n */\nexport interface AiJobStepOutcome {\n outputs: AiJobOutput[]\n /**\n * The step made progress and has more of the same work to do (AGL-2910):\n * a site audit works through its pages a batch at a time. The machine\n * records this pass — its spend and its outputs — and hands the SAME step\n * back to run again, so every pass is one reservation and one provider\n * exchange, and the job document says how far it got.\n */\n continue?: boolean\n usage: AssistTokenUsage\n estCostUsd: number\n model: string\n stopReason: string | null\n /**\n * The thinking effort the request asked for, recorded on the step\n * (AGL-2937); absent or `null` when the request named none.\n */\n effort?: AiEffort | null\n /** The model declined the brief (`stop_reason: 'refusal'`). Tokens were spent. */\n refused?: boolean\n /**\n * The model answered and nothing usable came of it — no structured answer\n * even after the step's own re-ask, say (AGL-2938). Tokens were spent, so\n * the machine meters the step before it fails the job with this sentence,\n * which is customer-safe and the only thing a customer reads.\n */\n failure?: string\n /** The plan the plan step proposed, which the machine keeps on the job (AGL-2935). */\n plan?: AiJobPlan\n /**\n * The step stopped for a person (AGL-2935). The machine records what it\n * spent and parks the job `needs_review`: a `plan` review completes the\n * step, so confirming runs the next one; a `doctrine` review hands the\n * step back, so trying again runs the same one.\n */\n review?: AiJobReview\n}\n\nexport interface AiJobStepContext {\n job: AiJob\n stepIndex: number\n now: Date\n /** Aborts the provider call when the runner's budget ends. */\n signal?: AbortSignal\n /**\n * The Admin SDK handle the machine runs on, for a step that reads what it\n * builds from — a site's theme, say — or writes a draft. A step never\n * writes the job document, the meter or the lease through it.\n */\n firestore: FirebaseFirestore.Firestore\n /** The org document the machine read for the step's reservation. */\n org?: Partial<AglynOrgBilling> | null\n /**\n * The model a step of this kind runs on for THIS job (AGL-2942): the\n * creator's pick where the plan and the allotment allowlists allow it,\n * the routing table otherwise. Absent, a runner asks the table itself.\n */\n modelFor?: (kind: AiStepKind) => string | undefined\n}\n\nexport type AiJobStepRunner = (\n context: AiJobStepContext,\n) => Promise<AiJobStepOutcome>\n\n/**\n * The static block, byte-identical on every request so it caches. The\n * customer's brief and the org's inputs never enter it.\n */\nexport const AI_JOB_TEXT_SYSTEM: AiSystemBlock[] = [\n {\n text:\n 'You write short, publishable copy for a website builder. You are ' +\n 'given a brief from the person who owns the site. Answer with the ' +\n 'copy only: no preamble, no headings unless the brief asks for them, ' +\n 'no closing remarks, no markdown fences. Keep to the length the brief ' +\n 'implies; when it implies none, write under 150 words. Write in the ' +\n 'language of the brief. Never invent facts, prices, names or claims ' +\n 'the brief did not give you; where a detail is missing, leave a ' +\n 'clearly marked placeholder in square brackets.',\n cacheBreakpoint: true,\n },\n]\n\n/** The brief as the user turn, with the kind-specific inputs stated plainly. */\nexport function aiJobTextPrompt(job: Pick<AiJob, 'brief' | 'inputs'>): string {\n const brief = job.brief.slice(0, AI_JOB_BRIEF_MAX_CHARS)\n const tone = typeof job.inputs?.['tone'] === 'string' ? job.inputs['tone'] : ''\n const audience =\n typeof job.inputs?.['audience'] === 'string' ? job.inputs['audience'] : ''\n const lines = [`Brief: ${brief}`]\n if (tone) lines.push(`Tone: ${tone}`)\n if (audience) lines.push(`Audience: ${audience}`)\n return lines.join('\\n')\n}\n\nexport const runAiJobTextStep: AiJobStepRunner = async ({ job, signal, modelFor }) => {\n const model = modelFor?.('job.text') ?? aiJobTextModel()\n const result = await runAiRequest({\n model,\n system: AI_JOB_TEXT_SYSTEM,\n messages: [{ role: 'user', content: aiJobTextPrompt(job) }],\n // The routing ceiling, lowered on a tier too slow to answer it inside the\n // least time the step registered.\n maxTokens: AI_JOB_TEXT_STEP_BUDGET.maxTokens(model),\n ...(AI_ROUTING_TABLE['job.text'].thinking ? { thinking: AI_ROUTING_TABLE['job.text'].thinking } : {}),\n ...(AI_ROUTING_TABLE['job.text'].effort ? { effort: AI_ROUTING_TABLE['job.text'].effort } : {}),\n stream: false,\n ...(signal ? { signal } : {}),\n })\n if (result.kind === 'refusal') {\n return {\n outputs: [],\n usage: result.usage,\n estCostUsd: result.estCostUsd,\n model,\n stopReason: result.stopReason,\n refused: true,\n }\n }\n const text = result.text.trim()\n return {\n outputs: text\n ? [\n {\n resource: 'text',\n // A text output has no document of its own; the id names the\n // step within the job so a later step can address it.\n id: 'draft',\n hostId: job.hostId ?? null,\n label: 'Draft copy',\n text,\n },\n ]\n : [],\n usage: result.usage,\n estCostUsd: result.estCostUsd,\n model,\n stopReason: result.stopReason,\n }\n}\n"],"names":["AI_STEP_TIERS","AI_ROUTING_TABLE","aiModelForStep","runAiRequest","aiJobStepBudget","aiJobTextModel","AI_JOB_TEXT_MAX_TOKENS","maxTokens","AI_JOB_TEXT_STEP_BUDGET","tier","attempts","lookups","AI_JOB_TEXT_STEP_MINIMUM_MS","minimumMs","AI_JOB_BRIEF_MAX_CHARS","AI_JOB_TEXT_SYSTEM","text","cacheBreakpoint","aiJobTextPrompt","job","brief","slice","tone","inputs","audience","lines","push","join","runAiJobTextStep","signal","modelFor","model","result","system","messages","role","content","thinking","effort","stream","kind","outputs","usage","estCostUsd","stopReason","refused","trim","resource","id","hostId","label"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/ai/src/lib/jobs/ai-job-text-step.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 { AglynOrgBilling } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport type {\n AiJob,\n AiJobOutput,\n AiJobPlan,\n AiJobReview,\n} from '../model/ai-jobs.types'\nimport { AI_STEP_TIERS, type AiStepKind } from '../providers/catalog'\nimport { AI_ROUTING_TABLE, aiModelForStep } from '../providers/routing'\nimport { runAiRequest, type AiEffort, type AiSystemBlock } from '../runtime/ai-runtime'\nimport type { AssistTokenUsage } from '../usage/assist-usage'\nimport { aiJobStepBudget } from './ai-job-budget'\n\n/**\n * The `text` step (AGL-2904): a brief in, a short piece of copy out.\n *\n * The one step kind this issue ships for real, so the state machine — the\n * lease, the reservation, the record, the beat — is exercised end to end by\n * something that spends tokens, rather than by a stub that could pass with\n * the meter unplugged. Every other kind refuses until its own issue lands.\n *\n * The balanced tier with adaptive thinking: a brief is open-ended in a way the copy\n * assistant's element mode is not, and the cost is one call per job. The\n * static system block is cached; the brief rides in the user turn, so no\n * per-org byte sits inside the cached prefix (AGL-2352, enforced by the\n * runtime before the request leaves).\n */\n\n/** The model the text step runs on: the routing table's answer for `job.text`. */\nexport function aiJobTextModel(): string {\n return aiModelForStep('job.text')\n}\n\n/** The text step's answer ceiling, from the routing table. */\nexport const AI_JOB_TEXT_MAX_TOKENS = AI_ROUTING_TABLE['job.text'].maxTokens\n\n/**\n * The text step's time (AGL-3035): ONE request — no re-ask, and no site\n * inventory to look anything up in — at the routing table's ceiling on the\n * tier `job.text` is served from, with the step's reads and writes. That fits\n * an inline door's budget, so a door runs a text job where it is asked for,\n * and the least time is the helper's worst case rather than a figure anyone\n * chose; a slower tier asks less so its worst case still fits.\n */\nexport const AI_JOB_TEXT_STEP_BUDGET = aiJobStepBudget({\n tier: AI_STEP_TIERS['job.text'],\n maxTokens: AI_JOB_TEXT_MAX_TOKENS,\n attempts: 1,\n lookups: 0,\n})\n\n/** The least time one text step needs before it starts. */\nexport const AI_JOB_TEXT_STEP_MINIMUM_MS = AI_JOB_TEXT_STEP_BUDGET.minimumMs\n\n/** How much of a brief is sent. Past this a brief is a document, not a brief. */\nexport const AI_JOB_BRIEF_MAX_CHARS = 4_000\n\n/**\n * What a step hands back to the machine. The machine meters `usage` at\n * `model`'s rates, records the step and appends the outputs; a step never\n * touches the job document or the meter itself.\n */\nexport interface AiJobStepOutcome {\n outputs: AiJobOutput[]\n /**\n * The step made progress and has more of the same work to do (AGL-2910):\n * a site audit works through its pages a batch at a time. The machine\n * records this pass — its spend and its outputs — and hands the SAME step\n * back to run again, so every pass is one reservation and one provider\n * exchange, and the job document says how far it got.\n */\n continue?: boolean\n usage: AssistTokenUsage\n estCostUsd: number\n model: string\n stopReason: string | null\n /**\n * The thinking effort the request asked for, recorded on the step\n * (AGL-2937); absent or `null` when the request named none.\n */\n effort?: AiEffort | null\n /** The model declined the brief (`stop_reason: 'refusal'`). Tokens were spent. */\n refused?: boolean\n /**\n * The model answered and nothing usable came of it — no structured answer\n * even after the step's own re-ask, say (AGL-2938). Tokens were spent, so\n * the machine meters the step before it fails the job with this sentence,\n * which is customer-safe and the only thing a customer reads.\n */\n failure?: string\n /** The plan the plan step proposed, which the machine keeps on the job (AGL-2935). */\n plan?: AiJobPlan\n /**\n * The step stopped for a person (AGL-2935). The machine records what it\n * spent and parks the job `needs_review`: a `plan` review completes the\n * step, so confirming runs the next one; a `doctrine` review hands the\n * step back, so trying again runs the same one.\n */\n review?: AiJobReview\n /**\n * The answer was refused by the platform's own checks, not the model's\n * (AGL-3594): the machine meters the step as any other and then gives the\n * job's credits back to the meters it was charged to (`refundJobCredits`),\n * a bounded number of times a day.\n */\n uncredited?: boolean\n}\n\nexport interface AiJobStepContext {\n job: AiJob\n stepIndex: number\n now: Date\n /** Aborts the provider call when the runner's budget ends. */\n signal?: AbortSignal\n /**\n * The Admin SDK handle the machine runs on, for a step that reads what it\n * builds from — a site's theme, say — or writes a draft. A step never\n * writes the job document, the meter or the lease through it.\n */\n firestore: FirebaseFirestore.Firestore\n /** The org document the machine read for the step's reservation. */\n org?: Partial<AglynOrgBilling> | null\n /**\n * The model a step of this kind runs on for THIS job (AGL-2942): the\n * creator's pick where the plan and the allotment allowlists allow it,\n * the routing table otherwise. Absent, a runner asks the table itself.\n */\n modelFor?: (kind: AiStepKind) => string | undefined\n}\n\nexport type AiJobStepRunner = (\n context: AiJobStepContext,\n) => Promise<AiJobStepOutcome>\n\n/**\n * The static block, byte-identical on every request so it caches. The\n * customer's brief and the org's inputs never enter it.\n */\nexport const AI_JOB_TEXT_SYSTEM: AiSystemBlock[] = [\n {\n text:\n 'You write short, publishable copy for a website builder. You are ' +\n 'given a brief from the person who owns the site. Answer with the ' +\n 'copy only: no preamble, no headings unless the brief asks for them, ' +\n 'no closing remarks, no markdown fences. Keep to the length the brief ' +\n 'implies; when it implies none, write under 150 words. Write in the ' +\n 'language of the brief. Never invent facts, prices, names or claims ' +\n 'the brief did not give you; where a detail is missing, leave a ' +\n 'clearly marked placeholder in square brackets.',\n cacheBreakpoint: true,\n },\n]\n\n/** The brief as the user turn, with the kind-specific inputs stated plainly. */\nexport function aiJobTextPrompt(job: Pick<AiJob, 'brief' | 'inputs'>): string {\n const brief = job.brief.slice(0, AI_JOB_BRIEF_MAX_CHARS)\n const tone = typeof job.inputs?.['tone'] === 'string' ? job.inputs['tone'] : ''\n const audience =\n typeof job.inputs?.['audience'] === 'string' ? job.inputs['audience'] : ''\n const lines = [`Brief: ${brief}`]\n if (tone) lines.push(`Tone: ${tone}`)\n if (audience) lines.push(`Audience: ${audience}`)\n return lines.join('\\n')\n}\n\nexport const runAiJobTextStep: AiJobStepRunner = async ({ job, signal, modelFor }) => {\n const model = modelFor?.('job.text') ?? aiJobTextModel()\n const result = await runAiRequest({\n model,\n system: AI_JOB_TEXT_SYSTEM,\n messages: [{ role: 'user', content: aiJobTextPrompt(job) }],\n // The routing ceiling, lowered on a tier too slow to answer it inside the\n // least time the step registered.\n maxTokens: AI_JOB_TEXT_STEP_BUDGET.maxTokens(model),\n ...(AI_ROUTING_TABLE['job.text'].thinking ? { thinking: AI_ROUTING_TABLE['job.text'].thinking } : {}),\n ...(AI_ROUTING_TABLE['job.text'].effort ? { effort: AI_ROUTING_TABLE['job.text'].effort } : {}),\n stream: false,\n ...(signal ? { signal } : {}),\n })\n if (result.kind === 'refusal') {\n return {\n outputs: [],\n usage: result.usage,\n estCostUsd: result.estCostUsd,\n model,\n stopReason: result.stopReason,\n refused: true,\n }\n }\n const text = result.text.trim()\n return {\n outputs: text\n ? [\n {\n resource: 'text',\n // A text output has no document of its own; the id names the\n // step within the job so a later step can address it.\n id: 'draft',\n hostId: job.hostId ?? null,\n label: 'Draft copy',\n text,\n },\n ]\n : [],\n usage: result.usage,\n estCostUsd: result.estCostUsd,\n model,\n stopReason: result.stopReason,\n }\n}\n"],"names":["AI_STEP_TIERS","AI_ROUTING_TABLE","aiModelForStep","runAiRequest","aiJobStepBudget","aiJobTextModel","AI_JOB_TEXT_MAX_TOKENS","maxTokens","AI_JOB_TEXT_STEP_BUDGET","tier","attempts","lookups","AI_JOB_TEXT_STEP_MINIMUM_MS","minimumMs","AI_JOB_BRIEF_MAX_CHARS","AI_JOB_TEXT_SYSTEM","text","cacheBreakpoint","aiJobTextPrompt","job","brief","slice","tone","inputs","audience","lines","push","join","runAiJobTextStep","signal","modelFor","model","result","system","messages","role","content","thinking","effort","stream","kind","outputs","usage","estCostUsd","stopReason","refused","trim","resource","id","hostId","label"],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
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 { notifyUsers } from '@aglyn/tenant-data-admin/server/notifications';
|
|
18
|
+
import { type AiJobTransitionListener } from './ai-jobs';
|
|
19
|
+
/**
|
|
20
|
+
* The person who started an AI job is told when it needs them, when it
|
|
21
|
+
* finishes and when it stops (AGL-3593), through the console's notifications
|
|
22
|
+
* — the bell, and the generic notification email for a person who switched
|
|
23
|
+
* that channel on for the category. No other channel: there is no push.
|
|
24
|
+
*
|
|
25
|
+
* The machine calls this where the job's state changes, once per change
|
|
26
|
+
* (`registerAiJobTransitionListener`): the request that ran a step inline,
|
|
27
|
+
* or the beat that ran it with nobody watching — which is exactly when a
|
|
28
|
+
* person needs telling. The recipient is the job's creator, whose job it is
|
|
29
|
+
* and who alone confirms its plan; a teammate is not told about someone
|
|
30
|
+
* else's draft.
|
|
31
|
+
*
|
|
32
|
+
* `notifyUsers` never throws, and honors the person's per-category,
|
|
33
|
+
* per-workspace and per-site preferences.
|
|
34
|
+
*/
|
|
35
|
+
export declare function aiJobTransitionNotifier(notify?: typeof notifyUsers): AiJobTransitionListener;
|
|
36
|
+
/** Registers the notifier the jobs machine tells about each change. */
|
|
37
|
+
export declare function registerAiJobsNotify(): void;
|
|
@@ -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 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,17 @@ 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[];
|
|
233
257
|
limit?: number;
|
|
234
258
|
}
|
|
235
|
-
/**
|
|
259
|
+
/**
|
|
260
|
+
* Newest first. Needs the (status, createdAt) index when filtered, which
|
|
261
|
+
* answers an `in` over statuses as it answers one status.
|
|
262
|
+
*/
|
|
236
263
|
export declare function listAiJobs(firestore: Firestore, orgId: string, options?: ListAiJobsOptions): Promise<AiJob[]>;
|
|
237
264
|
/** The wire form. `running` is the lease, read against `now`. */
|
|
238
265
|
export declare function aiJobSummary(job: AiJob, now?: Date): AiJobSummary;
|
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,16 +413,23 @@ 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
|
+
]);
|
|
386
428
|
const snapshot = await query.orderBy('createdAt', 'desc').limit(limit).get();
|
|
387
429
|
return snapshot.docs.map((doc)=>jobFrom(doc)).filter((job)=>job !== null);
|
|
388
430
|
}
|
|
389
431
|
/** 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;
|
|
432
|
+
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
433
|
var _job_lease, _job_inputs;
|
|
392
434
|
const leaseUntil = toMillis((_job_lease = job.lease) == null ? void 0 : _job_lease.until);
|
|
393
435
|
return {
|
|
@@ -412,6 +454,7 @@ export async function getAiJob(firestore, orgId, jobId) {
|
|
|
412
454
|
outputs: (_job_outputs = job.outputs) != null ? _job_outputs : [],
|
|
413
455
|
creditsReserved: (_job_creditsReserved = job.creditsReserved) != null ? _job_creditsReserved : 0,
|
|
414
456
|
creditsSpent: (_job_creditsSpent = job.creditsSpent) != null ? _job_creditsSpent : 0,
|
|
457
|
+
refundedCredits: (_job_refundedCredits = job.refundedCredits) != null ? _job_refundedCredits : 0,
|
|
415
458
|
createdBy: job.createdBy,
|
|
416
459
|
createdAt: (_toIso = toIso(job.createdAt)) != null ? _toIso : new Date(0).toISOString(),
|
|
417
460
|
updatedAt: (_toIso1 = toIso(job.updatedAt)) != null ? _toIso1 : new Date(0).toISOString(),
|
|
@@ -605,7 +648,7 @@ function nextStepIndex(job) {
|
|
|
605
648
|
* in the same transaction, so no beat can claim the next step in between.
|
|
606
649
|
*/ export async function recordStep(firestore, orgId, jobId, owner, stepIndex, input, now = new Date()) {
|
|
607
650
|
const ref = jobsCollection(firestore, orgId).doc(jobId);
|
|
608
|
-
|
|
651
|
+
const recorded = await firestore.runTransaction(async (tx)=>{
|
|
609
652
|
var _job_outputs, _input_outputs, _job_creditsSpent, _job_creditsReserved, _job_creditsReserved1;
|
|
610
653
|
var _job_lease;
|
|
611
654
|
const job = jobFrom(await tx.get(ref));
|
|
@@ -654,9 +697,16 @@ function nextStepIndex(job) {
|
|
|
654
697
|
});
|
|
655
698
|
return {
|
|
656
699
|
job: _extends({}, job, patch),
|
|
657
|
-
remaining
|
|
700
|
+
remaining,
|
|
701
|
+
// Entered `needs_review` in this write, rather than parked again.
|
|
702
|
+
parked: parksForReview && job.status !== 'needs_review'
|
|
658
703
|
};
|
|
659
704
|
});
|
|
705
|
+
if (recorded.parked) await announceAiJobTransition(recorded.job, 'needs-review');
|
|
706
|
+
return {
|
|
707
|
+
job: recorded.job,
|
|
708
|
+
remaining: recorded.remaining
|
|
709
|
+
};
|
|
660
710
|
}
|
|
661
711
|
async function transition(firestore, orgId, jobId, apply) {
|
|
662
712
|
const ref = jobsCollection(firestore, orgId).doc(jobId);
|
|
@@ -678,13 +728,14 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
678
728
|
});
|
|
679
729
|
}
|
|
680
730
|
/** 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 : {
|
|
731
|
+
const { job, changed } = await transition(firestore, orgId, jobId, (current)=>isAiJobTerminal(current.status) ? null : {
|
|
682
732
|
status: 'done',
|
|
683
733
|
creditsReserved: 0,
|
|
684
734
|
lease: null,
|
|
685
735
|
error: null,
|
|
686
736
|
updatedAt: now
|
|
687
737
|
});
|
|
738
|
+
if (changed) await announceAiJobTransition(job, 'done');
|
|
688
739
|
return job;
|
|
689
740
|
}
|
|
690
741
|
/**
|
|
@@ -699,7 +750,7 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
699
750
|
stepIndex: (_ref = detail == null ? void 0 : detail.stepIndex) != null ? _ref : null,
|
|
700
751
|
error: (detail == null ? void 0 : detail.error) instanceof Error ? detail.error.message : (_ref1 = detail == null ? void 0 : detail.error) != null ? _ref1 : null
|
|
701
752
|
});
|
|
702
|
-
const { job } = await transition(firestore, orgId, jobId, (current)=>isAiJobTerminal(current.status) ? null : {
|
|
753
|
+
const { job, changed } = await transition(firestore, orgId, jobId, (current)=>isAiJobTerminal(current.status) ? null : {
|
|
703
754
|
status: 'failed',
|
|
704
755
|
error: message,
|
|
705
756
|
creditsReserved: 0,
|
|
@@ -711,6 +762,7 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
711
762
|
}) : step),
|
|
712
763
|
updatedAt: now
|
|
713
764
|
});
|
|
765
|
+
if (changed) await announceAiJobTransition(job, 'failed');
|
|
714
766
|
return job;
|
|
715
767
|
}
|
|
716
768
|
/**
|
|
@@ -760,6 +812,57 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
760
812
|
}
|
|
761
813
|
return result;
|
|
762
814
|
}
|
|
815
|
+
/**
|
|
816
|
+
* Gives back what a job spent when it failed on our side (AGL-3594), and
|
|
817
|
+
* records the give-back on the job — `refundedCredits`, `refundReason` and
|
|
818
|
+
* the count its next key is numbered by. Everything the job spent and has not
|
|
819
|
+
* given back where it delivered nothing; the failing step's own spend where
|
|
820
|
+
* it delivered drafts, which it keeps. `assist-job-refund.ts` has the rule
|
|
821
|
+
* and the bound. Never throws: a give-back that cannot be made leaves the job
|
|
822
|
+
* as the failure left it, metered as every other step.
|
|
823
|
+
*/ async function refundOurFailure(firestore, orgId, job, input) {
|
|
824
|
+
try {
|
|
825
|
+
var _job_outputs, _job_creditsSpent, _job_refundedCredits, _data, _job_refunds;
|
|
826
|
+
const delivered = ((_job_outputs = job.outputs) != null ? _job_outputs : []).some((output)=>output.resource !== 'seo');
|
|
827
|
+
const credits = aiJobRefundCredits({
|
|
828
|
+
delivered,
|
|
829
|
+
jobCredits: (_job_creditsSpent = job.creditsSpent) != null ? _job_creditsSpent : 0,
|
|
830
|
+
stepCredits: input.stepCredits,
|
|
831
|
+
alreadyRefunded: (_job_refundedCredits = job.refundedCredits) != null ? _job_refundedCredits : 0
|
|
832
|
+
});
|
|
833
|
+
if (credits <= 0) return job;
|
|
834
|
+
const free = input.free !== undefined ? input.free : freeAssistAccount((_data = (await firestore.collection('orgs').doc(orgId).get()).data()) != null ? _data : {});
|
|
835
|
+
const ordinal = (_job_refunds = job.refunds) != null ? _job_refunds : 0;
|
|
836
|
+
const refund = await refundJobCredits(firestore, {
|
|
837
|
+
orgId,
|
|
838
|
+
free,
|
|
839
|
+
jobId: job.$id,
|
|
840
|
+
ordinal,
|
|
841
|
+
credits,
|
|
842
|
+
month: assistUsageMonth(input.now),
|
|
843
|
+
day: assistUsageDay(input.now),
|
|
844
|
+
reason: input.reason
|
|
845
|
+
});
|
|
846
|
+
if (refund.status !== 'returned') return job;
|
|
847
|
+
const { job: refunded } = await transition(firestore, orgId, job.$id, (current)=>{
|
|
848
|
+
var _current_refundedCredits, _current_refunds;
|
|
849
|
+
return {
|
|
850
|
+
refundedCredits: ((_current_refundedCredits = current.refundedCredits) != null ? _current_refundedCredits : 0) + credits,
|
|
851
|
+
refundReason: input.reason,
|
|
852
|
+
refunds: ((_current_refunds = current.refunds) != null ? _current_refunds : 0) + 1,
|
|
853
|
+
updatedAt: input.now
|
|
854
|
+
};
|
|
855
|
+
});
|
|
856
|
+
return refunded;
|
|
857
|
+
} catch (error) {
|
|
858
|
+
console.error('ai job refund failed', {
|
|
859
|
+
orgId,
|
|
860
|
+
jobId: job.$id,
|
|
861
|
+
error
|
|
862
|
+
});
|
|
863
|
+
return job;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
763
866
|
/**
|
|
764
867
|
* Resume a job that stopped for a person (AGL-2935): confirm its plan, or
|
|
765
868
|
* try again the step whose answer broke a building rule. One transaction,
|
|
@@ -938,22 +1041,32 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
938
1041
|
const runner = aiJobRunnerForStep(job.kind, step.name);
|
|
939
1042
|
if (!runner) {
|
|
940
1043
|
await releaseHeld();
|
|
1044
|
+
const failed = await failAiJob(firestore, orgId, jobId, AI_JOB_NOT_AVAILABLE_COPY, {
|
|
1045
|
+
stepIndex,
|
|
1046
|
+
error: `no runner registered for kind ${job.kind}`
|
|
1047
|
+
}, now);
|
|
941
1048
|
return {
|
|
942
1049
|
outcome: 'failed',
|
|
943
|
-
job: await
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
1050
|
+
job: await refundOurFailure(firestore, orgId, failed, {
|
|
1051
|
+
reason: 'unavailable',
|
|
1052
|
+
stepCredits: 0,
|
|
1053
|
+
now
|
|
1054
|
+
})
|
|
947
1055
|
};
|
|
948
1056
|
}
|
|
949
1057
|
if (((_step_attempts = step.attempts) != null ? _step_attempts : 0) > AI_JOB_STEP_MAX_ATTEMPTS) {
|
|
950
1058
|
await releaseHeld();
|
|
1059
|
+
const failed = await failAiJob(firestore, orgId, jobId, AI_UPSTREAM_FAILURE_COPY, {
|
|
1060
|
+
stepIndex,
|
|
1061
|
+
error: `step ${step.name} exhausted ${AI_JOB_STEP_MAX_ATTEMPTS} attempts`
|
|
1062
|
+
}, now);
|
|
951
1063
|
return {
|
|
952
1064
|
outcome: 'failed',
|
|
953
|
-
job: await
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1065
|
+
job: await refundOurFailure(firestore, orgId, failed, {
|
|
1066
|
+
reason: 'timeout',
|
|
1067
|
+
stepCredits: 0,
|
|
1068
|
+
now
|
|
1069
|
+
})
|
|
957
1070
|
};
|
|
958
1071
|
}
|
|
959
1072
|
const org = (_options_org = options.org) != null ? _options_org : (_data = (await firestore.collection('orgs').doc(orgId).get()).data()) != null ? _data : {};
|
|
@@ -1058,6 +1171,7 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
1058
1171
|
modelFor
|
|
1059
1172
|
});
|
|
1060
1173
|
} catch (error) {
|
|
1174
|
+
var _reservation_free;
|
|
1061
1175
|
// The provider refused the request or the budget ended before it
|
|
1062
1176
|
// answered: no usage came back, so nothing is metered and the message
|
|
1063
1177
|
// goes back to the org. The provider's own words are already in the
|
|
@@ -1078,12 +1192,18 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
1078
1192
|
job: requeued
|
|
1079
1193
|
};
|
|
1080
1194
|
}
|
|
1195
|
+
const failed = await failAiJob(firestore, orgId, jobId, AI_UPSTREAM_FAILURE_COPY, {
|
|
1196
|
+
stepIndex,
|
|
1197
|
+
error
|
|
1198
|
+
}, now);
|
|
1081
1199
|
return {
|
|
1082
1200
|
outcome: 'failed',
|
|
1083
|
-
job: await
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1201
|
+
job: await refundOurFailure(firestore, orgId, failed, {
|
|
1202
|
+
reason: 'provider',
|
|
1203
|
+
stepCredits: 0,
|
|
1204
|
+
free: (_reservation_free = reservation.free) != null ? _reservation_free : null,
|
|
1205
|
+
now
|
|
1206
|
+
})
|
|
1087
1207
|
};
|
|
1088
1208
|
}
|
|
1089
1209
|
const latencyMs = Math.max(0, Date.now() - runStarted);
|
|
@@ -1092,6 +1212,7 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
1092
1212
|
// back and nothing is metered, exactly as for a request the provider
|
|
1093
1213
|
// refused.
|
|
1094
1214
|
if (outcome.failure && aiJobStepSpentNothing(outcome)) {
|
|
1215
|
+
var _reservation_free1;
|
|
1095
1216
|
await releaseAssistMessage(firestore, orgId, reservation).catch((releaseError)=>console.error('ai job release failed', {
|
|
1096
1217
|
orgId,
|
|
1097
1218
|
jobId,
|
|
@@ -1102,12 +1223,18 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
1102
1223
|
creditsSpent: 0,
|
|
1103
1224
|
error: outcome.failure
|
|
1104
1225
|
}, now);
|
|
1226
|
+
const failed = await failAiJob(firestore, orgId, jobId, outcome.failure, {
|
|
1227
|
+
stepIndex,
|
|
1228
|
+
error: `step failure before the provider: ${outcome.failure}`
|
|
1229
|
+
}, now);
|
|
1105
1230
|
return {
|
|
1106
1231
|
outcome: 'failed',
|
|
1107
|
-
job: await
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1232
|
+
job: await refundOurFailure(firestore, orgId, failed, {
|
|
1233
|
+
reason: 'step-failure',
|
|
1234
|
+
stepCredits: 0,
|
|
1235
|
+
free: (_reservation_free1 = reservation.free) != null ? _reservation_free1 : null,
|
|
1236
|
+
now
|
|
1237
|
+
})
|
|
1111
1238
|
};
|
|
1112
1239
|
}
|
|
1113
1240
|
const tokens = {
|
|
@@ -1132,7 +1259,7 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
1132
1259
|
// Tokens were spent, so the bill is written first and its failure is a
|
|
1133
1260
|
// log line, never a failed job — the same rule the chat route keeps.
|
|
1134
1261
|
try {
|
|
1135
|
-
var _job_hostId1,
|
|
1262
|
+
var _job_hostId1, _reservation_free2;
|
|
1136
1263
|
await recordAssistCost(firestore, orgId, {
|
|
1137
1264
|
route: 'ai/jobs',
|
|
1138
1265
|
hostId: (_job_hostId1 = job.hostId) != null ? _job_hostId1 : null,
|
|
@@ -1145,7 +1272,7 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
1145
1272
|
// The account this job drew on, as the reservation decided it
|
|
1146
1273
|
// (AGL-2925): a Free job lands on the owner's allowance and the
|
|
1147
1274
|
// platform's day; a paid one on neither.
|
|
1148
|
-
free: (
|
|
1275
|
+
free: (_reservation_free2 = reservation.free) != null ? _reservation_free2 : null,
|
|
1149
1276
|
// The step is the creator's spend, under the job's kind (AGL-2928).
|
|
1150
1277
|
uid: job.createdBy,
|
|
1151
1278
|
kind: job.kind
|
|
@@ -1163,6 +1290,7 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
1163
1290
|
// model's answer (AGL-2938), fails the job with its own sentence. The
|
|
1164
1291
|
// tokens are already on the bill above.
|
|
1165
1292
|
if (outcome.refused || outcome.failure) {
|
|
1293
|
+
var _reservation_free3;
|
|
1166
1294
|
const message = outcome.refused ? AI_JOB_REFUSED_COPY : outcome.failure;
|
|
1167
1295
|
await recordStep(firestore, orgId, jobId, options.owner, stepIndex, {
|
|
1168
1296
|
status: 'failed',
|
|
@@ -1170,12 +1298,19 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
1170
1298
|
error: message,
|
|
1171
1299
|
tokens
|
|
1172
1300
|
}, now);
|
|
1301
|
+
const failed = await failAiJob(firestore, orgId, jobId, message, {
|
|
1302
|
+
stepIndex,
|
|
1303
|
+
error: outcome.refused ? 'stop_reason refusal' : `step failure: ${message}`
|
|
1304
|
+
}, now);
|
|
1305
|
+
// A model declining the brief is not our failure; a step's own is (AGL-3594).
|
|
1173
1306
|
return {
|
|
1174
1307
|
outcome: 'failed',
|
|
1175
|
-
job: await
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1308
|
+
job: outcome.refused ? failed : await refundOurFailure(firestore, orgId, failed, {
|
|
1309
|
+
reason: 'step-failure',
|
|
1310
|
+
stepCredits: credits,
|
|
1311
|
+
free: (_reservation_free3 = reservation.free) != null ? _reservation_free3 : null,
|
|
1312
|
+
now
|
|
1313
|
+
})
|
|
1179
1314
|
};
|
|
1180
1315
|
}
|
|
1181
1316
|
// A step that stopped for a person parks the job in the same write that
|
|
@@ -1245,7 +1380,30 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
1245
1380
|
}
|
|
1246
1381
|
});
|
|
1247
1382
|
}
|
|
1383
|
+
if ((review == null ? void 0 : review.reason) === 'plan' && recorded.job.status === 'needs_review' && aiJobAutoConfirms(job)) {
|
|
1384
|
+
// A guided site start confirms its own plan (AGL-3594): the resume
|
|
1385
|
+
// door's confirmation, by the job's creator, and the build queued.
|
|
1386
|
+
const resumed = await resumeAiJob(firestore, orgId, jobId, {
|
|
1387
|
+
uid: job.createdBy
|
|
1388
|
+
}, now);
|
|
1389
|
+
await writeAiJobAudit(firestore, {
|
|
1390
|
+
action: 'ai.job.resume',
|
|
1391
|
+
actorUid: job.createdBy,
|
|
1392
|
+
orgId,
|
|
1393
|
+
jobId,
|
|
1394
|
+
after: {
|
|
1395
|
+
status: resumed.job.status,
|
|
1396
|
+
confirmedPlan: true,
|
|
1397
|
+
autoConfirm: true
|
|
1398
|
+
}
|
|
1399
|
+
});
|
|
1400
|
+
return {
|
|
1401
|
+
outcome: 'done',
|
|
1402
|
+
job: resumed.job
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1248
1405
|
if (review && recorded.job.status === 'needs_review') {
|
|
1406
|
+
var _reservation_free4;
|
|
1249
1407
|
// Nobody parked the job but its own step, so the row names no actor, as
|
|
1250
1408
|
// the meter's park does.
|
|
1251
1409
|
await logAiJobNeedsInput(orgId, {
|
|
@@ -1255,9 +1413,15 @@ async function transition(firestore, orgId, jobId, apply) {
|
|
|
1255
1413
|
kind: job.kind,
|
|
1256
1414
|
reason: review.reason
|
|
1257
1415
|
});
|
|
1416
|
+
// A plan the plan rules still refused is our failure (AGL-3594).
|
|
1258
1417
|
return {
|
|
1259
1418
|
outcome: 'needs_review',
|
|
1260
|
-
job: recorded.job
|
|
1419
|
+
job: outcome.uncredited ? await refundOurFailure(firestore, orgId, recorded.job, {
|
|
1420
|
+
reason: 'plan-refused',
|
|
1421
|
+
stepCredits: credits,
|
|
1422
|
+
free: (_reservation_free4 = reservation.free) != null ? _reservation_free4 : null,
|
|
1423
|
+
now
|
|
1424
|
+
}) : recorded.job
|
|
1261
1425
|
};
|
|
1262
1426
|
}
|
|
1263
1427
|
if (recorded.remaining > 0 || isAiJobTerminal(recorded.job.status)) {
|