primitive-admin 1.1.0-alpha.85 → 1.1.0-alpha.87
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 +34 -0
- package/assets/skill/skills/primitive-platform/SKILL.md +37 -1
- package/dist/src/commands/analytics.js +17 -5
- package/dist/src/commands/analytics.js.map +1 -1
- package/dist/src/commands/auth-sessions.d.ts +7 -0
- package/dist/src/commands/auth-sessions.js +144 -0
- package/dist/src/commands/auth-sessions.js.map +1 -0
- package/dist/src/commands/auth.js +47 -8
- package/dist/src/commands/auth.js.map +1 -1
- package/dist/src/commands/collections.js +648 -1
- package/dist/src/commands/collections.js.map +1 -1
- package/dist/src/commands/database-type-configs.js +1 -1
- package/dist/src/commands/database-type-configs.js.map +1 -1
- package/dist/src/commands/databases.js +11 -2
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/documents.d.ts +20 -0
- package/dist/src/commands/documents.js +213 -17
- package/dist/src/commands/documents.js.map +1 -1
- package/dist/src/commands/functions.js +56 -5
- package/dist/src/commands/functions.js.map +1 -1
- package/dist/src/commands/integrations.js +4 -0
- package/dist/src/commands/integrations.js.map +1 -1
- package/dist/src/commands/sync.d.ts +83 -41
- package/dist/src/commands/sync.js +553 -81
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/commands/users.js +3 -1
- package/dist/src/commands/users.js.map +1 -1
- package/dist/src/lib/api-client.d.ts +111 -7
- package/dist/src/lib/api-client.js +92 -20
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/app-settings-descriptor.d.ts +1 -1
- package/dist/src/lib/app-settings-descriptor.js +0 -1
- package/dist/src/lib/app-settings-descriptor.js.map +1 -1
- package/dist/src/lib/collection-export.d.ts +184 -0
- package/dist/src/lib/collection-export.js +252 -0
- package/dist/src/lib/collection-export.js.map +1 -0
- package/dist/src/lib/config-object-descriptor.js +17 -3
- package/dist/src/lib/config-object-descriptor.js.map +1 -1
- package/dist/src/lib/config-payload.d.ts +8 -1
- package/dist/src/lib/config-payload.js +29 -9
- package/dist/src/lib/config-payload.js.map +1 -1
- package/dist/src/lib/config-surface.js +11 -7
- package/dist/src/lib/config-surface.js.map +1 -1
- package/dist/src/lib/deprecation.d.ts +22 -0
- package/dist/src/lib/deprecation.js +43 -0
- package/dist/src/lib/deprecation.js.map +1 -0
- package/dist/src/lib/env-resolver-core.js +10 -3
- package/dist/src/lib/env-resolver-core.js.map +1 -1
- package/dist/src/lib/function-db-types.d.ts +14 -3
- package/dist/src/lib/function-db-types.js +55 -11
- package/dist/src/lib/function-db-types.js.map +1 -1
- package/dist/src/lib/function-document-types.d.ts +13 -2
- package/dist/src/lib/function-document-types.js +79 -30
- package/dist/src/lib/function-document-types.js.map +1 -1
- package/dist/src/lib/function-trigger-listing.d.ts +27 -0
- package/dist/src/lib/function-trigger-listing.js +70 -0
- package/dist/src/lib/function-trigger-listing.js.map +1 -0
- package/dist/src/lib/generated-config-surfaces.d.ts +540 -10
- package/dist/src/lib/generated-config-surfaces.js +1915 -173
- package/dist/src/lib/generated-config-surfaces.js.map +1 -1
- package/dist/src/lib/generated-sdk-types.d.ts +1 -1
- package/dist/src/lib/generated-sdk-types.js +1 -1
- package/dist/src/lib/generated-sdk-types.js.map +1 -1
- package/dist/src/lib/log-inspection.d.ts +18 -1
- package/dist/src/lib/log-inspection.js +5 -0
- package/dist/src/lib/log-inspection.js.map +1 -1
- package/dist/src/lib/logout-admin-session.d.ts +33 -0
- package/dist/src/lib/logout-admin-session.js +70 -0
- package/dist/src/lib/logout-admin-session.js.map +1 -0
- package/dist/src/lib/prompt-cost-format.d.ts +11 -0
- package/dist/src/lib/prompt-cost-format.js +41 -0
- package/dist/src/lib/prompt-cost-format.js.map +1 -0
- package/dist/src/lib/prompt-schema-codegen.d.ts +53 -0
- package/dist/src/lib/prompt-schema-codegen.js +253 -3
- package/dist/src/lib/prompt-schema-codegen.js.map +1 -1
- package/dist/src/lib/refresh-admin-credentials.d.ts +9 -1
- package/dist/src/lib/refresh-admin-credentials.js +22 -2
- package/dist/src/lib/refresh-admin-credentials.js.map +1 -1
- package/dist/src/lib/storage-pending-retry.d.ts +26 -0
- package/dist/src/lib/storage-pending-retry.js +42 -0
- package/dist/src/lib/storage-pending-retry.js.map +1 -0
- package/dist/src/lib/swift-codegen/agentGenerator.d.ts +42 -0
- package/dist/src/lib/swift-codegen/agentGenerator.js +118 -0
- package/dist/src/lib/swift-codegen/agentGenerator.js.map +1 -0
- package/dist/src/lib/swift-codegen/banners.d.ts +6 -0
- package/dist/src/lib/swift-codegen/banners.js +6 -0
- package/dist/src/lib/swift-codegen/banners.js.map +1 -1
- package/dist/src/lib/swift-codegen/dbGenerator.js +11 -3
- package/dist/src/lib/swift-codegen/dbGenerator.js.map +1 -1
- package/dist/src/lib/swift-codegen/functionGenerator.d.ts +7 -0
- package/dist/src/lib/swift-codegen/functionGenerator.js +41 -3
- package/dist/src/lib/swift-codegen/functionGenerator.js.map +1 -1
- package/dist/src/lib/swift-codegen/generator.js +8 -2
- package/dist/src/lib/swift-codegen/generator.js.map +1 -1
- package/dist/src/lib/workflow-usage.d.ts +11 -13
- package/dist/src/lib/workflow-usage.js +12 -14
- package/dist/src/lib/workflow-usage.js.map +1 -1
- package/package.json +2 -2
|
@@ -3,6 +3,7 @@ import { join, basename } from "path";
|
|
|
3
3
|
import { createHash } from "crypto";
|
|
4
4
|
import { lookup as mimeLookup } from "mime-types";
|
|
5
5
|
import { ApiClient, ApiError, ConflictError, SchemaRequiredError, OperationRefError, SchemaBreaksOpsError, SchemaHasUncheckableOpsError, TomlParseError, OpsExistError, } from "../lib/api-client.js";
|
|
6
|
+
import { retryOnStoragePending } from "../lib/storage-pending-retry.js";
|
|
6
7
|
import { buildDatabaseTypeTomlData, detectExistingOperationForms, normalizeOperationFromToml, normalizeSubscriptionFromToml, SubscriptionAccessKeyConflictError, } from "../lib/toml-database-config.js";
|
|
7
8
|
import { canEmitNative } from "../lib/toml-native-form.js";
|
|
8
9
|
// #1446's `type: "json"` emission rule, shared with `serializeFunction` since
|
|
@@ -34,10 +35,18 @@ import { WORKFLOW_MODEL_DEFAULTS } from "../lib/workflow-defaults.js";
|
|
|
34
35
|
// #2644 — the one definition per configuration object, vendored from the server.
|
|
35
36
|
import { BLOB_BUCKET_TABLE, COLLECTION_TYPE_CONFIG_TABLE, CRON_TRIGGER_TABLE, DATABASE_TYPE_TABLE, EMAIL_TEMPLATE_TABLE, GROUP_TYPE_CONFIG_TABLE, INTEGRATION_TABLE, METADATA_CATEGORY_CONFIG_TABLE, PROMPT_CONFIG_TABLE, PROMPT_TABLE, RULE_SET_TABLE, SERVER_FUNCTION_TABLE, TEST_CASE_SURFACE, TEST_CASE_TABLE, WEBHOOK_TABLE, WORKFLOW_TABLE, configDeclaredTypeErrors, fieldServerDefault, findMisshapenTables, findUnknownDocumentKeys, findUnknownGroupedTomlKeys, findUnknownServerKeys, findUnknownTomlKeys, misshapenTableMessage, retiredConfigKey, spellsDecimalNumber, unrecognizedPullKeysMessage, unrecognizedPushKeysMessage, unrecognizedPushTablesMessage, warnUnrecognizedServerKeys, } from "../lib/config-surface.js";
|
|
36
37
|
import { buildPayloadFromToml, projectEntityToPayload, projectRecordToToml, tomlOwned, } from "../lib/config-payload.js";
|
|
37
|
-
import { CONFIG_SURFACES, isEmptyForEmit, } from "../lib/generated-config-surfaces.js";
|
|
38
|
+
import { CONFIG_SURFACES, isEmptyForEmit, tomlGroupsOf, } from "../lib/generated-config-surfaces.js";
|
|
38
39
|
// #3626 — the prompt KIND rule, vendored with the definitions so the CLI's
|
|
39
40
|
// preflight refuses exactly what the admin routes refuse.
|
|
40
41
|
import { hoistDeprecatedChatKeys, promptKindOf, resolvePromptKind, } from "../lib/generated-config-surfaces.js";
|
|
42
|
+
// #3798 — the `[prompt.agent]` declaration rule, vendored the same way.
|
|
43
|
+
import { agentDeclarationFunctionKeys, agentTestCaseRefusal, resolveAgentDeclaration, } from "../lib/generated-config-surfaces.js";
|
|
44
|
+
// #3799 — the meaning-preserving schema translation, vendored the same way, so
|
|
45
|
+
// the preflight refuses exactly the schemas the admin routes refuse.
|
|
46
|
+
import { agentSchemaTranslationRefusals } from "../lib/generated-config-surfaces.js";
|
|
47
|
+
// #3801 — the strict-output rule, vendored the same way, so the preflight
|
|
48
|
+
// refuses exactly the strict configs the admin routes refuse.
|
|
49
|
+
import { resolvePromptStrictOutput } from "../lib/generated-config-surfaces.js";
|
|
41
50
|
// #3179 — the function's two halves: the header payloads and the envelope
|
|
42
51
|
// `config push` sends, and the bytes `config pull` writes back.
|
|
43
52
|
import { applyFunctionPull, buildFunctionConfigPush, createDocumentSchemaDropWarner, documentSchemaRetentionFailure, buildFunctionCreatePayload, buildFunctionUpdatePayload, functionDiffEntity, functionTomlIgnoredKeyWarnings, functionTomlPreflightErrors, planDocumentSchemaPull, planFunctionPull, serializeFunction, } from "../lib/function-sync.js";
|
|
@@ -1668,22 +1677,20 @@ export function buildBlobBucketUpdatePayload(bucket) {
|
|
|
1668
1677
|
*/
|
|
1669
1678
|
const PROMPT_CONFIG_DEFAULT_STATUS = fieldServerDefault(PROMPT_CONFIG_TABLE, "status");
|
|
1670
1679
|
/**
|
|
1671
|
-
*
|
|
1672
|
-
* (#3201).
|
|
1680
|
+
* #3201's post-create status patch is RETIRED — #3799 (DSO-3799-005).
|
|
1673
1681
|
*
|
|
1674
|
-
* `status`
|
|
1675
|
-
* both
|
|
1676
|
-
* `status = "archived"`
|
|
1677
|
-
*
|
|
1678
|
-
*
|
|
1679
|
-
*
|
|
1682
|
+
* `status` was update-only, so `POST /prompts` and `POST /prompts/:id/configs`
|
|
1683
|
+
* both assigned `active` whatever the body said, and an authored
|
|
1684
|
+
* `status = "archived"` needed a follow-up PATCH ordered after the activation of
|
|
1685
|
+
* the entry the file marks live. Both create paths honor the key now
|
|
1686
|
+
* (`writableOn: BOTH`, validated by one rule on all three paths), so the entry
|
|
1687
|
+
* lands retired on its own create: the same end state, two fewer calls per
|
|
1688
|
+
* retired config, and no window in which the server holds a status the file
|
|
1689
|
+
* never asked for. That window is what made a pulled tree unpushable into a
|
|
1690
|
+
* fresh environment once an agent's schemas are checked against each
|
|
1691
|
+
* non-archived config's provider — the check fired on the transient active
|
|
1692
|
+
* state.
|
|
1680
1693
|
*/
|
|
1681
|
-
export function createdConfigStatusPatch(tomlConfig) {
|
|
1682
|
-
const { status } = buildPromptConfigPayload(tomlConfig, { mode: "update" });
|
|
1683
|
-
return typeof status === "string" && status !== PROMPT_CONFIG_DEFAULT_STATUS
|
|
1684
|
-
? { status }
|
|
1685
|
-
: null;
|
|
1686
|
-
}
|
|
1687
1694
|
/**
|
|
1688
1695
|
* Which `[[configs]]` entry a prompt file marks as live (issue #2645).
|
|
1689
1696
|
*
|
|
@@ -1791,6 +1798,11 @@ export function serializePrompt(prompt, logger) {
|
|
|
1791
1798
|
...projectRecordToToml(PROMPT_TABLE, prompt, {
|
|
1792
1799
|
inputSchema: jsonField(promptSubject),
|
|
1793
1800
|
outputSchema: jsonField(promptSubject),
|
|
1801
|
+
// #3798 — the agent declaration goes out as `[prompt.agent]` tables
|
|
1802
|
+
// (JSON-string fallback for a `null` TOML cannot spell), exactly as
|
|
1803
|
+
// stored: the server keeps it as authored, so what pull writes is
|
|
1804
|
+
// what the author pushed.
|
|
1805
|
+
agent: jsonField(promptSubject),
|
|
1794
1806
|
}),
|
|
1795
1807
|
},
|
|
1796
1808
|
configs: (prompt.configs || []).map((config) => ({
|
|
@@ -1819,7 +1831,10 @@ export function serializePrompt(prompt, logger) {
|
|
|
1819
1831
|
// `config pull` and `config diff` down with it. The JSON-string
|
|
1820
1832
|
// fallback keeps the declaration whole either way.
|
|
1821
1833
|
questions: jsonField(`Prompt '${prompt?.promptKey}' config '${config?.configName}'`),
|
|
1822
|
-
}
|
|
1834
|
+
},
|
|
1835
|
+
// #3798 — the model settings chat and agent share are written under the
|
|
1836
|
+
// block the prompt's kind names: `[configs.agent]` for an agent.
|
|
1837
|
+
{ tomlGroup: promptKindOf(prompt) }),
|
|
1823
1838
|
// #2645 — the live config has to be identifiable in the repo. Emitted
|
|
1824
1839
|
// only on the active entry (`undefined` elsewhere so the key is omitted),
|
|
1825
1840
|
// which keeps the marker unambiguous by construction.
|
|
@@ -1891,6 +1906,16 @@ export function buildPromptPushPayload(key, tomlData, { mode = "update" } = {})
|
|
|
1891
1906
|
// dropped and leaving a stale rule live. On create it is omitted so
|
|
1892
1907
|
// the push-adopt path (#1006) can still reach the server's 409.
|
|
1893
1908
|
accessRule: (value) => (create ? value : (value ?? null)),
|
|
1909
|
+
// #3798 — the agent declaration, as authored: the server stores it
|
|
1910
|
+
// unchanged, so the value sent is the value pull writes back. On
|
|
1911
|
+
// update a deleted block CLEARS it — for an agent only: a chat or
|
|
1912
|
+
// decisions file carries no block to delete, and omitting the key
|
|
1913
|
+
// keeps its body what it was before this field existed.
|
|
1914
|
+
agent: (value) => create || value !== undefined
|
|
1915
|
+
? value
|
|
1916
|
+
: promptKindOf(prompt) === "agent"
|
|
1917
|
+
? null
|
|
1918
|
+
: undefined,
|
|
1894
1919
|
// #3626, CR3626-004 — the kind NORMALIZED, exactly as the resolver
|
|
1895
1920
|
// that validates it reads it. `kind = " chat "` and `kind = ""` both
|
|
1896
1921
|
// mean `chat` to `resolvePromptKind`, so the server stores `chat` and
|
|
@@ -1914,6 +1939,10 @@ export function buildPromptPushPayload(key, tomlData, { mode = "update" } = {})
|
|
|
1914
1939
|
};
|
|
1915
1940
|
if (create)
|
|
1916
1941
|
payload.promptKey = key;
|
|
1942
|
+
// #3798 — a prompt that declares no agent block sends no `agent` key at all,
|
|
1943
|
+
// so a chat or decisions body is byte for byte what it was.
|
|
1944
|
+
if (payload.agent === undefined)
|
|
1945
|
+
delete payload.agent;
|
|
1917
1946
|
return payload;
|
|
1918
1947
|
}
|
|
1919
1948
|
/**
|
|
@@ -1947,9 +1976,15 @@ export function buildPromptCreatePayload(key, tomlData) {
|
|
|
1947
1976
|
// #3626, SO3626-001 — the placeholder is a CHAT fallback. A decisions prompt
|
|
1948
1977
|
// has no template at all (its input is a `state` value plus its questions),
|
|
1949
1978
|
// and injecting one would store a chat key the block rule then refuses on
|
|
1950
|
-
// every later update.
|
|
1951
|
-
|
|
1952
|
-
|
|
1979
|
+
// every later update. #3798 — nor has an agent: the model sees each message
|
|
1980
|
+
// as sent.
|
|
1981
|
+
if (promptKindOf(prompt) !== "chat") {
|
|
1982
|
+
const body = { ...prompt, ...seed };
|
|
1983
|
+
for (const field of Object.keys(body)) {
|
|
1984
|
+
if (body[field] === undefined)
|
|
1985
|
+
delete body[field];
|
|
1986
|
+
}
|
|
1987
|
+
return body;
|
|
1953
1988
|
}
|
|
1954
1989
|
return {
|
|
1955
1990
|
...prompt,
|
|
@@ -1987,12 +2022,11 @@ const SEEDED_CONFIG_DEFERRED = ["outputSchema", "description"];
|
|
|
1987
2022
|
* response rather than assumed, so a handler that one day honors the name
|
|
1988
2023
|
* produces no needless PATCH.
|
|
1989
2024
|
*
|
|
1990
|
-
* #3201 — the entry's `status`
|
|
1991
|
-
*
|
|
1992
|
-
* config
|
|
1993
|
-
*
|
|
1994
|
-
*
|
|
1995
|
-
* applied after activation instead (`createdConfigStatusPatch`).
|
|
2025
|
+
* #3201 — the entry's `status` was deferred the OTHER way, to a patch after the
|
|
2026
|
+
* activation, because the create pointed the prompt's `activeConfigId` at a
|
|
2027
|
+
* config it had forced live. #3799 (DSO-3799-005) — the create carries the
|
|
2028
|
+
* entry's `status` itself now, so there is no deferral either way and this patch
|
|
2029
|
+
* is the two fields `POST /prompts` cannot express, nothing more.
|
|
1996
2030
|
*/
|
|
1997
2031
|
export function seededOnlyPromptConfigFields(firstConfig, seededConfigName = "default") {
|
|
1998
2032
|
const full = buildPromptConfigPayload(firstConfig || {}, { mode: "update" });
|
|
@@ -2049,6 +2083,12 @@ export function buildPromptConfigPayload(tomlConfig, { mode = "update" } = {}) {
|
|
|
2049
2083
|
mode,
|
|
2050
2084
|
values: {
|
|
2051
2085
|
provider: (value) => (create ? value || "openrouter" : value),
|
|
2086
|
+
// #3799 (DSO-3799-005) — carried on CREATE when the entry names one, and
|
|
2087
|
+
// omitted otherwise, so the create body for a file that says nothing
|
|
2088
|
+
// about status is what the server has always received (principle 5). On
|
|
2089
|
+
// update it still sends the declared default for an absent key, which is
|
|
2090
|
+
// what lets a deleted line converge (#2880 DSO-002).
|
|
2091
|
+
status: (value) => create ? value : (value ?? PROMPT_CONFIG_DEFAULT_STATUS),
|
|
2052
2092
|
model: (value) => create ? value || "google/gemini-2.0-flash-001" : value,
|
|
2053
2093
|
description: tomlOwned,
|
|
2054
2094
|
systemPrompt: tomlOwned,
|
|
@@ -2065,6 +2105,9 @@ export function buildPromptConfigPayload(tomlConfig, { mode = "update" } = {}) {
|
|
|
2065
2105
|
// the merged config refused as "both set".
|
|
2066
2106
|
reasoningEffort: tomlOwned,
|
|
2067
2107
|
reasoningBudget: tomlOwned,
|
|
2108
|
+
// #3801 — deleting `strictOutput = true` has to turn strict output off
|
|
2109
|
+
// server-side, or the file stops describing the request that is sent.
|
|
2110
|
+
strictOutput: tomlOwned,
|
|
2068
2111
|
// #3626, SO3626-002 — the decisions declaration is TOML-owned on the
|
|
2069
2112
|
// same terms: deleting `[configs.decisions]` has to clear `questions`
|
|
2070
2113
|
// server-side, or the file stops describing what the model is asked.
|
|
@@ -2182,13 +2225,14 @@ export function configUnknownKeyError(filePath, tomlData, table) {
|
|
|
2182
2225
|
const misplaced = trulyUnknown
|
|
2183
2226
|
.map((key) => ({
|
|
2184
2227
|
key,
|
|
2185
|
-
|
|
2186
|
-
|
|
2228
|
+
groups: tomlGroupsOf(table.fields.find((f) => f.tomlKey === key && f.tomlGroup) ??
|
|
2229
|
+
{}),
|
|
2187
2230
|
}))
|
|
2188
|
-
.filter((entry) =>
|
|
2189
|
-
for (const { key,
|
|
2231
|
+
.filter((entry) => entry.groups.length > 0);
|
|
2232
|
+
for (const { key, groups } of misplaced) {
|
|
2233
|
+
// #3798 — a key two blocks share names both; the prompt's kind picks one.
|
|
2190
2234
|
messages.push(`${filePath}: ${prefix}[0].${key} is not a [[${prefix}]] key; ` +
|
|
2191
|
-
`write it under [${prefix}.${
|
|
2235
|
+
`write it under ${groups.map((g) => `[${prefix}.${g}]`).join(" or ")}`);
|
|
2192
2236
|
}
|
|
2193
2237
|
const misplacedKeys = new Set(misplaced.map((m) => m.key));
|
|
2194
2238
|
const stillUnknown = trulyUnknown.filter((k) => !misplacedKeys.has(k));
|
|
@@ -2568,6 +2612,15 @@ export async function runConfigPushPreflight(input) {
|
|
|
2568
2612
|
knownTargets =
|
|
2569
2613
|
treeTargets.integrations.size > 0 ? treeTargets : null;
|
|
2570
2614
|
}
|
|
2615
|
+
// #3719 — the project's document schema rides every function version,
|
|
2616
|
+
// so a unique constraint on a stringset field in it refuses the push
|
|
2617
|
+
// here, once, rather than deploying with seeding silently off. Bounded
|
|
2618
|
+
// by the selection like the rest of this block: only a push that
|
|
2619
|
+
// carries a function carries the schema.
|
|
2620
|
+
const documentSchemaError = documentSchemaPreflightError(resolveDocumentSchema(configDir));
|
|
2621
|
+
if (documentSchemaError) {
|
|
2622
|
+
preflightValidationErrors.push(` ${documentSchemaError}`);
|
|
2623
|
+
}
|
|
2571
2624
|
for (const file of functionFiles) {
|
|
2572
2625
|
const filePath = join(preflightFunctionsDir, file);
|
|
2573
2626
|
let tomlData;
|
|
@@ -2636,6 +2689,9 @@ export async function runConfigPushPreflight(input) {
|
|
|
2636
2689
|
// single pull-then-push clears the notice. Printed even under `--dry-run`,
|
|
2637
2690
|
// which never reaches the apply — the notice is about the FILE.
|
|
2638
2691
|
const preflightPromptsDir = join(configDir, "prompts");
|
|
2692
|
+
// #3798 — the functions an agent's references are checked against, read
|
|
2693
|
+
// once and only when a selected prompt is an agent that names one.
|
|
2694
|
+
let agentFunctions = null;
|
|
2639
2695
|
if (existsSync(preflightPromptsDir)) {
|
|
2640
2696
|
for (const file of selectFiles("prompt", preflightPromptsDir, readdirSync(preflightPromptsDir).filter((f) => f.endsWith(".toml")))) {
|
|
2641
2697
|
const filePath = join(preflightPromptsDir, file);
|
|
@@ -2650,8 +2706,36 @@ export async function runConfigPushPreflight(input) {
|
|
|
2650
2706
|
const notice = promptFlatChatKeyWarning(filePath, tomlData);
|
|
2651
2707
|
if (notice)
|
|
2652
2708
|
warn(` ${notice}`);
|
|
2709
|
+
// #3798 — an agent's tool, turn-context and history functions must
|
|
2710
|
+
// exist, and a tool's must declare both schemas, before anything is
|
|
2711
|
+
// applied. Bounded by `--only` like the rest of this loop.
|
|
2712
|
+
if (promptKindOf(tomlData?.prompt) === "agent" &&
|
|
2713
|
+
agentDeclarationFunctionKeys(tomlData?.prompt?.agent).length > 0) {
|
|
2714
|
+
if (!agentFunctions) {
|
|
2715
|
+
let live = null;
|
|
2716
|
+
try {
|
|
2717
|
+
live = (await client.listFunctions(resolvedAppId)).items;
|
|
2718
|
+
}
|
|
2719
|
+
catch {
|
|
2720
|
+
live = null;
|
|
2721
|
+
}
|
|
2722
|
+
agentFunctions = agentFunctionFactsForPush({
|
|
2723
|
+
configDir,
|
|
2724
|
+
pushSelection,
|
|
2725
|
+
live,
|
|
2726
|
+
});
|
|
2727
|
+
}
|
|
2728
|
+
for (const message of promptAgentFunctionErrors(filePath, tomlData, agentFunctions)) {
|
|
2729
|
+
preflightValidationErrors.push(` ${message}`);
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2653
2732
|
}
|
|
2654
2733
|
}
|
|
2734
|
+
// #3798 — a test case on an agent is refused by the admin routes; say so
|
|
2735
|
+
// here, before any other file is applied.
|
|
2736
|
+
for (const message of promptAgentSidecarErrors(configDir)) {
|
|
2737
|
+
preflightValidationErrors.push(` ${message}`);
|
|
2738
|
+
}
|
|
2655
2739
|
// The `<key>.tests/*.toml` sidecars, which sit beside the block they
|
|
2656
2740
|
// test rather than in a type directory of their own. Same posture,
|
|
2657
2741
|
// reached through the registry's `test-case` surface — and through the
|
|
@@ -2691,6 +2775,41 @@ export async function runConfigPushPreflight(input) {
|
|
|
2691
2775
|
* whose spelling is not the declared type. A file that does not parse is left
|
|
2692
2776
|
* to the per-type loop that reads it, which names the parse error itself.
|
|
2693
2777
|
*/
|
|
2778
|
+
/**
|
|
2779
|
+
* The blocks one `[[configs]]` entry carries that its prompt's kind does not
|
|
2780
|
+
* name — #3798. An agent's entry may carry only `[configs.agent]`; a chat or
|
|
2781
|
+
* decisions entry may not carry it. `movedFlat` are the deprecated flat chat
|
|
2782
|
+
* keys the hoist moved under `chat`, named as the author wrote them.
|
|
2783
|
+
*
|
|
2784
|
+
* Only the blocks the other arms of `resolvePromptKind` cannot see are checked
|
|
2785
|
+
* here: a decisions block on a chat entry, and chat keys on a decisions entry,
|
|
2786
|
+
* stay the resolver's refusals with their #3626 wording.
|
|
2787
|
+
*/
|
|
2788
|
+
function promptEntryBlockErrors(kind, entry, movedFlat) {
|
|
2789
|
+
const has = (block) => entry != null && typeof entry === "object" && entry[block] !== undefined;
|
|
2790
|
+
const errors = [];
|
|
2791
|
+
if (kind === "agent") {
|
|
2792
|
+
if (movedFlat.length > 0) {
|
|
2793
|
+
errors.push(`sets ${movedFlat.join(", ")} flat — the deprecated [configs.chat] ` +
|
|
2794
|
+
'spelling — but prompt kind is "agent", so it may carry only a ' +
|
|
2795
|
+
"[configs.agent] block; write them under [configs.agent]");
|
|
2796
|
+
}
|
|
2797
|
+
for (const block of ["chat", "decisions"]) {
|
|
2798
|
+
if (!has(block))
|
|
2799
|
+
continue;
|
|
2800
|
+
errors.push(`carries a [configs.${block}] block, but prompt kind is "agent", so ` +
|
|
2801
|
+
"it may carry only a [configs.agent] block (systemPrompt, " +
|
|
2802
|
+
"temperature, topP, maxTokens, reasoningEffort, reasoningBudget)");
|
|
2803
|
+
}
|
|
2804
|
+
return errors;
|
|
2805
|
+
}
|
|
2806
|
+
if (has("agent")) {
|
|
2807
|
+
errors.push(`carries a [configs.agent] block, but prompt kind is "${kind}", so it ` +
|
|
2808
|
+
`may carry only a [configs.${kind}] block. An agent is its own ` +
|
|
2809
|
+
'prompt: create it under a new key with kind = "agent"');
|
|
2810
|
+
}
|
|
2811
|
+
return errors;
|
|
2812
|
+
}
|
|
2694
2813
|
/**
|
|
2695
2814
|
* A prompt file's KIND errors — #3626, decidable from the file alone.
|
|
2696
2815
|
*
|
|
@@ -2702,15 +2821,94 @@ export async function runConfigPushPreflight(input) {
|
|
|
2702
2821
|
* the server's cannot disagree — and this one lands in the preflight, before
|
|
2703
2822
|
* the first mutating call.
|
|
2704
2823
|
*/
|
|
2824
|
+
/**
|
|
2825
|
+
* The `[[configs]]` entries of a prompt file as the agent rules read them —
|
|
2826
|
+
* #3799: which provider each named configuration runs on, and whether the file
|
|
2827
|
+
* retires it.
|
|
2828
|
+
*
|
|
2829
|
+
* An archived entry is skipped by the translation rule (a config the server does
|
|
2830
|
+
* not run cannot make the declaration untranslatable), which is what lets a
|
|
2831
|
+
* pulled tree keep a retired config on a provider the current schemas could not
|
|
2832
|
+
* reach. `status` is read through the same default the surface declares, so an
|
|
2833
|
+
* entry that omits the key is live.
|
|
2834
|
+
*/
|
|
2835
|
+
function promptFileConfigFacts(entries) {
|
|
2836
|
+
return (entries || []).map((entry, index) => ({
|
|
2837
|
+
configName: String(entry?.name ?? `configs[${index}]`),
|
|
2838
|
+
provider: String(entry?.provider ?? ""),
|
|
2839
|
+
status: String(entry?.status ?? PROMPT_CONFIG_DEFAULT_STATUS),
|
|
2840
|
+
}));
|
|
2841
|
+
}
|
|
2705
2842
|
export function promptKindFileErrors(filePath, tomlData) {
|
|
2706
2843
|
const entries = Array.isArray(tomlData?.configs) ? tomlData.configs : [];
|
|
2707
2844
|
const errors = [];
|
|
2845
|
+
const fileKind = promptKindOf(tomlData?.prompt);
|
|
2846
|
+
// #3798 — the `[prompt.agent]` declaration's file-decidable half: its shape,
|
|
2847
|
+
// and `[prompt].outputSchema` on an agent. The function half needs app state
|
|
2848
|
+
// and runs in the push preflight (`promptAgentFunctionErrors`).
|
|
2849
|
+
const declaration = resolveAgentDeclaration({
|
|
2850
|
+
kind: tomlData?.prompt?.kind,
|
|
2851
|
+
agent: tomlData?.prompt?.agent,
|
|
2852
|
+
outputSchema: tomlData?.prompt?.outputSchema,
|
|
2853
|
+
});
|
|
2854
|
+
if (declaration.ok === false) {
|
|
2855
|
+
for (const refusal of declaration.refusals) {
|
|
2856
|
+
errors.push(`${filePath}: ${refusal.message}`);
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
else if (fileKind === "agent") {
|
|
2860
|
+
// #3799 — the schemas a CLIENT tool and an event declare are in the file,
|
|
2861
|
+
// so whether each provider the file configures can carry them without
|
|
2862
|
+
// losing meaning is file-decidable too: `config diff` reports the same row
|
|
2863
|
+
// as `config push`. A server tool's schemas are its FUNCTION's and need app
|
|
2864
|
+
// state, so they are checked in the push preflight
|
|
2865
|
+
// (`promptAgentFunctionErrors`). Only after the shape refusals above, so a
|
|
2866
|
+
// malformed declaration is not also reported as untranslatable.
|
|
2867
|
+
for (const refusal of agentSchemaTranslationRefusals({
|
|
2868
|
+
declaration: tomlData?.prompt?.agent,
|
|
2869
|
+
configs: promptFileConfigFacts(entries),
|
|
2870
|
+
})) {
|
|
2871
|
+
errors.push(`${filePath}: ${refusal.message}`);
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2708
2874
|
for (const [index, entry] of entries.entries()) {
|
|
2709
2875
|
const hoisted = hoistDeprecatedChatKeys(entry);
|
|
2710
2876
|
for (const key of hoisted.conflicts) {
|
|
2711
2877
|
errors.push(`${filePath}: configs[${index}].${key} is set both flat and under ` +
|
|
2712
2878
|
`[configs.chat]; keep the [configs.chat] spelling`);
|
|
2713
2879
|
}
|
|
2880
|
+
// #3798 — the block rule for three kinds, stated per entry: an entry may
|
|
2881
|
+
// carry only the block its prompt's kind names. An agent's `[configs.agent]`
|
|
2882
|
+
// holds keys chat also holds, so a stray block would otherwise be read (or
|
|
2883
|
+
// ignored) in silence by the payload builder.
|
|
2884
|
+
const blockErrors = promptEntryBlockErrors(fileKind, entry, hoisted.moved).map((message) => `${filePath}: configs[${index}] ${message}`);
|
|
2885
|
+
if (blockErrors.length > 0) {
|
|
2886
|
+
errors.push(...blockErrors);
|
|
2887
|
+
continue;
|
|
2888
|
+
}
|
|
2889
|
+
if (fileKind === "agent") {
|
|
2890
|
+
const agent = hoisted.entry.agent ?? {};
|
|
2891
|
+
const resolution = resolvePromptKind({
|
|
2892
|
+
kind: "agent",
|
|
2893
|
+
provider: hoisted.entry.provider,
|
|
2894
|
+
model: hoisted.entry.model,
|
|
2895
|
+
config: {
|
|
2896
|
+
systemPrompt: agent.systemPrompt,
|
|
2897
|
+
temperature: agent.temperature,
|
|
2898
|
+
topP: agent.topP,
|
|
2899
|
+
maxTokens: agent.maxTokens,
|
|
2900
|
+
reasoningEffort: agent.reasoningEffort,
|
|
2901
|
+
reasoningBudget: agent.reasoningBudget,
|
|
2902
|
+
// #3801 — a chat key: named, so the refusal says which key an agent
|
|
2903
|
+
// does not take (§D1: an agent has no output schema).
|
|
2904
|
+
strictOutput: agent.strictOutput,
|
|
2905
|
+
},
|
|
2906
|
+
});
|
|
2907
|
+
if (resolution.ok === false) {
|
|
2908
|
+
errors.push(`${filePath}: configs[${index}] — ${resolution.error}`);
|
|
2909
|
+
}
|
|
2910
|
+
continue;
|
|
2911
|
+
}
|
|
2714
2912
|
const chat = hoisted.entry.chat ?? {};
|
|
2715
2913
|
const decisions = hoisted.entry.decisions ?? {};
|
|
2716
2914
|
// #3626 — the FIRST entry of a chat prompt carries the create seed's
|
|
@@ -2726,7 +2924,7 @@ export function promptKindFileErrors(filePath, tomlData) {
|
|
|
2726
2924
|
// `userPromptTemplate`, so reading absence more narrowly than the builder
|
|
2727
2925
|
// does refused the file this CLI had just written, for a value the create
|
|
2728
2926
|
// would have replaced with the placeholder anyway.
|
|
2729
|
-
const seedsTemplate = index === 0 &&
|
|
2927
|
+
const seedsTemplate = index === 0 && fileKind === "chat";
|
|
2730
2928
|
const userPromptTemplate = seedsTemplate
|
|
2731
2929
|
? chat.userPromptTemplate || "{{ input }}"
|
|
2732
2930
|
: chat.userPromptTemplate;
|
|
@@ -2744,12 +2942,200 @@ export function promptKindFileErrors(filePath, tomlData) {
|
|
|
2744
2942
|
outputSchema: chat.outputSchema,
|
|
2745
2943
|
reasoningEffort: chat.reasoningEffort,
|
|
2746
2944
|
reasoningBudget: chat.reasoningBudget,
|
|
2945
|
+
strictOutput: chat.strictOutput,
|
|
2747
2946
|
questions: decisions.questions,
|
|
2748
2947
|
},
|
|
2749
2948
|
});
|
|
2750
2949
|
if (resolution.ok === false) {
|
|
2751
2950
|
errors.push(`${filePath}: configs[${index}] — ${resolution.error}`);
|
|
2951
|
+
continue;
|
|
2952
|
+
}
|
|
2953
|
+
// #3801 — strict output, file-decidable: the entry's own keys, the file's
|
|
2954
|
+
// `[prompt].outputSchema` and the entry's status (an archived entry is not
|
|
2955
|
+
// held to the prompt schema, DSO-3801-002). The route runs the same rule
|
|
2956
|
+
// against what the server holds.
|
|
2957
|
+
if (resolution.kind === "chat") {
|
|
2958
|
+
const strict = resolvePromptStrictOutput({
|
|
2959
|
+
provider: hoisted.entry.provider,
|
|
2960
|
+
strictOutput: chat.strictOutput,
|
|
2961
|
+
outputFormat: chat.outputFormat,
|
|
2962
|
+
configOutputSchema: chat.outputSchema,
|
|
2963
|
+
promptOutputSchema: tomlData?.prompt?.outputSchema,
|
|
2964
|
+
status: hoisted.entry.status ?? PROMPT_CONFIG_DEFAULT_STATUS,
|
|
2965
|
+
});
|
|
2966
|
+
if (strict.ok === false) {
|
|
2967
|
+
errors.push(`${filePath}: configs[${index}] — ${strict.error} (${strict.code})`);
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
return errors;
|
|
2972
|
+
}
|
|
2973
|
+
/** A schema slot counts as declared when it holds anything but nothing. */
|
|
2974
|
+
function declaresSchema(value) {
|
|
2975
|
+
if (value === undefined || value === null)
|
|
2976
|
+
return false;
|
|
2977
|
+
return !(typeof value === "string" && value.trim() === "");
|
|
2978
|
+
}
|
|
2979
|
+
/**
|
|
2980
|
+
* The functions an agent's references are checked against at push — #3798,
|
|
2981
|
+
* DSO-3798-002.
|
|
2982
|
+
*
|
|
2983
|
+
* `config push` applies prompts, then (for agents) the server functions leg,
|
|
2984
|
+
* so a reference may be satisfied by a function that is live already or by one
|
|
2985
|
+
* this push creates. What the push BELIEVES is therefore the live listing
|
|
2986
|
+
* overlaid by the function files this push SELECTS (`--only`, every file when
|
|
2987
|
+
* unscoped): a selected file's schemas are what the server will hold after the
|
|
2988
|
+
* push, so it wins over the live row. An UNSELECTED file is not read at all —
|
|
2989
|
+
* its edits will not be applied, and a function only it declares will not
|
|
2990
|
+
* exist afterwards.
|
|
2991
|
+
*
|
|
2992
|
+
* `live` is `null` when the listing could not be fetched: the selected files
|
|
2993
|
+
* still count, and `liveListed` lets the refusal say why every other reference
|
|
2994
|
+
* could not be confirmed. A live row that is archived is a tombstone, not a
|
|
2995
|
+
* function; an inactive one is accepted (the turn engine re-checks
|
|
2996
|
+
* availability on every call).
|
|
2997
|
+
*/
|
|
2998
|
+
export function agentFunctionFactsForPush(input) {
|
|
2999
|
+
const facts = {};
|
|
3000
|
+
for (const row of input.live ?? []) {
|
|
3001
|
+
const key = String(row?.functionKey ?? "");
|
|
3002
|
+
if (!key || row?.status === "archived")
|
|
3003
|
+
continue;
|
|
3004
|
+
facts[key] = {
|
|
3005
|
+
hasInputSchema: declaresSchema(row.inputSchema),
|
|
3006
|
+
hasOutputSchema: declaresSchema(row.outputSchema),
|
|
3007
|
+
// #3799 — the schemas themselves, as stored (a JSON string from the API,
|
|
3008
|
+
// an object from a tree file), so a server tool's two schemas can be
|
|
3009
|
+
// translated for each of the agent's providers.
|
|
3010
|
+
inputSchema: row.inputSchema,
|
|
3011
|
+
outputSchema: row.outputSchema,
|
|
3012
|
+
};
|
|
3013
|
+
}
|
|
3014
|
+
const functionsDir = join(input.configDir, "functions");
|
|
3015
|
+
if (existsSync(functionsDir)) {
|
|
3016
|
+
for (const file of filterFilesBySelection(input.pushSelection, "function", functionsDir, readdirSync(functionsDir).filter(isFunctionDeclarationFile))) {
|
|
3017
|
+
let table;
|
|
3018
|
+
try {
|
|
3019
|
+
table = parseTomlFile(join(functionsDir, file))?.function ?? {};
|
|
3020
|
+
}
|
|
3021
|
+
catch {
|
|
3022
|
+
// Reported by the function preflight with its own message.
|
|
3023
|
+
continue;
|
|
3024
|
+
}
|
|
3025
|
+
const key = table.key || basename(file, ".toml");
|
|
3026
|
+
facts[key] = {
|
|
3027
|
+
hasInputSchema: declaresSchema(table.inputSchema),
|
|
3028
|
+
hasOutputSchema: declaresSchema(table.outputSchema),
|
|
3029
|
+
// #3799 — a SELECTED file's schemas are what the server will hold after
|
|
3030
|
+
// this push, so they are what the translation is checked against.
|
|
3031
|
+
inputSchema: table.inputSchema,
|
|
3032
|
+
outputSchema: table.outputSchema,
|
|
3033
|
+
};
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
return { facts, liveListed: input.live !== null };
|
|
3037
|
+
}
|
|
3038
|
+
/**
|
|
3039
|
+
* The function half of an agent's declaration — #3798: every tool, turn
|
|
3040
|
+
* context and history function that the functions this push believes
|
|
3041
|
+
* (`agentFunctionFactsForPush`) do not have, and every tool function lacking an
|
|
3042
|
+
* input or output schema. The same `resolveAgentDeclaration` the admin routes
|
|
3043
|
+
* run; only its function refusals are reported here, because the collector
|
|
3044
|
+
* (`promptKindFileErrors`) already reported the file-decidable ones.
|
|
3045
|
+
*/
|
|
3046
|
+
export function promptAgentFunctionErrors(filePath, tomlData, known) {
|
|
3047
|
+
if (promptKindOf(tomlData?.prompt) !== "agent")
|
|
3048
|
+
return [];
|
|
3049
|
+
const result = resolveAgentDeclaration({
|
|
3050
|
+
kind: "agent",
|
|
3051
|
+
agent: tomlData?.prompt?.agent,
|
|
3052
|
+
functions: known.facts,
|
|
3053
|
+
});
|
|
3054
|
+
const errors = result.ok === false
|
|
3055
|
+
? result.refusals
|
|
3056
|
+
.filter((refusal) => refusal.code === "AGENT_TOOL_FUNCTION_MISSING" ||
|
|
3057
|
+
refusal.code === "AGENT_TOOL_FUNCTION_SCHEMA_MISSING")
|
|
3058
|
+
.map((refusal) => refusal.code === "AGENT_TOOL_FUNCTION_MISSING" && !known.liveListed
|
|
3059
|
+
? `${filePath}: ${refusal.message} (the app's live functions could ` +
|
|
3060
|
+
"not be listed, so only the function files this push applies were " +
|
|
3061
|
+
"checked)"
|
|
3062
|
+
: `${filePath}: ${refusal.message}`)
|
|
3063
|
+
: [];
|
|
3064
|
+
// #3799 — a SERVER tool's schemas are its function's, so translating them
|
|
3065
|
+
// needs the facts this push believes (the live listing overlaid by the
|
|
3066
|
+
// function files it selects, DSO-3798-002). The client-tool and event half is
|
|
3067
|
+
// file-decidable and reported by `promptKindFileErrors`, which `config diff`
|
|
3068
|
+
// runs too; passing the facts here means only the server-tool refusals are
|
|
3069
|
+
// new, because that collector already saw the rest.
|
|
3070
|
+
const entries = Array.isArray(tomlData?.configs) ? tomlData.configs : [];
|
|
3071
|
+
const configs = promptFileConfigFacts(entries);
|
|
3072
|
+
const fileDecidable = new Set(agentSchemaTranslationRefusals({
|
|
3073
|
+
declaration: tomlData?.prompt?.agent,
|
|
3074
|
+
configs,
|
|
3075
|
+
}).map((refusal) => refusal.message));
|
|
3076
|
+
for (const refusal of agentSchemaTranslationRefusals({
|
|
3077
|
+
declaration: tomlData?.prompt?.agent,
|
|
3078
|
+
functions: known.facts,
|
|
3079
|
+
configs,
|
|
3080
|
+
})) {
|
|
3081
|
+
if (fileDecidable.has(refusal.message))
|
|
3082
|
+
continue;
|
|
3083
|
+
errors.push(`${filePath}: ${refusal.message}`);
|
|
3084
|
+
}
|
|
3085
|
+
return errors;
|
|
3086
|
+
}
|
|
3087
|
+
/**
|
|
3088
|
+
* The order `config push` applies prompt files in — #3798. An agent names
|
|
3089
|
+
* server functions that must exist when it is created, and the push applies
|
|
3090
|
+
* prompts before functions, so agent files go AFTER the server-functions leg;
|
|
3091
|
+
* every other file keeps its place. A file that does not parse stays first, so
|
|
3092
|
+
* the leg reports it where it always has.
|
|
3093
|
+
*/
|
|
3094
|
+
export function partitionPromptFilesForPush(promptsDir, files) {
|
|
3095
|
+
const beforeFunctions = [];
|
|
3096
|
+
const afterFunctions = [];
|
|
3097
|
+
for (const file of files) {
|
|
3098
|
+
let kind = "chat";
|
|
3099
|
+
try {
|
|
3100
|
+
kind = promptKindOf(parseTomlFile(join(promptsDir, file))?.prompt);
|
|
3101
|
+
}
|
|
3102
|
+
catch {
|
|
3103
|
+
beforeFunctions.push(file);
|
|
3104
|
+
continue;
|
|
3105
|
+
}
|
|
3106
|
+
(kind === "agent" ? afterFunctions : beforeFunctions).push(file);
|
|
3107
|
+
}
|
|
3108
|
+
return { beforeFunctions, afterFunctions };
|
|
3109
|
+
}
|
|
3110
|
+
/**
|
|
3111
|
+
* Every `prompts/<key>.tests/*.toml` sidecar whose prompt is an agent — #3798
|
|
3112
|
+
* (§D1: prompt test cases are refused for the kind in v1). The admin test-case
|
|
3113
|
+
* routes refuse the same case with `AGENT_TEST_CASE_NOT_SUPPORTED`; this says
|
|
3114
|
+
* so before anything is applied.
|
|
3115
|
+
*/
|
|
3116
|
+
export function promptAgentSidecarErrors(configDir) {
|
|
3117
|
+
const promptsDir = join(configDir, "prompts");
|
|
3118
|
+
const errors = [];
|
|
3119
|
+
for (const filePath of testCaseTomlFiles(configDir)) {
|
|
3120
|
+
const parent = basename(join(filePath, ".."));
|
|
3121
|
+
if (!filePath.startsWith(promptsDir) || !parent.endsWith(".tests"))
|
|
3122
|
+
continue;
|
|
3123
|
+
const key = parent.slice(0, -".tests".length);
|
|
3124
|
+
const owner = join(promptsDir, `${key}.toml`);
|
|
3125
|
+
if (!existsSync(owner))
|
|
3126
|
+
continue;
|
|
3127
|
+
let tomlData;
|
|
3128
|
+
try {
|
|
3129
|
+
tomlData = parseTomlFile(owner);
|
|
3130
|
+
}
|
|
3131
|
+
catch {
|
|
3132
|
+
continue;
|
|
2752
3133
|
}
|
|
3134
|
+
if (promptKindOf(tomlData?.prompt) !== "agent")
|
|
3135
|
+
continue;
|
|
3136
|
+
const promptKey = tomlData?.prompt?.key || key;
|
|
3137
|
+
errors.push(`${filePath.slice(configDir.length + 1)}: ` +
|
|
3138
|
+
agentTestCaseRefusal(promptKey, "subject").message);
|
|
2753
3139
|
}
|
|
2754
3140
|
return errors;
|
|
2755
3141
|
}
|
|
@@ -3059,6 +3445,17 @@ export function workflowPushSendsUpdate(configDir, key, parsedWorkflowToml, sync
|
|
|
3059
3445
|
*
|
|
3060
3446
|
* Returns `null` when the name is present.
|
|
3061
3447
|
*/
|
|
3448
|
+
/**
|
|
3449
|
+
* The preflight error a project's document schema contributes, or `null` —
|
|
3450
|
+
* #3719.
|
|
3451
|
+
*
|
|
3452
|
+
* Only a `refusal` counts: a unique constraint on a stringset field, which no
|
|
3453
|
+
* writer can key consistently, so the push stops before any mutation. Every
|
|
3454
|
+
* other unusable-schema case stays the apply loop's warning (D3455-004).
|
|
3455
|
+
*/
|
|
3456
|
+
export function documentSchemaPreflightError(resolution) {
|
|
3457
|
+
return resolution.refusal;
|
|
3458
|
+
}
|
|
3062
3459
|
export function workflowUpdateNameError(filePath, workflow) {
|
|
3063
3460
|
const name = workflow?.name;
|
|
3064
3461
|
if (typeof name === "string" && name.trim() !== "")
|
|
@@ -10772,8 +11169,14 @@ What a push guarantees (two stages):
|
|
|
10772
11169
|
}
|
|
10773
11170
|
// Process prompts
|
|
10774
11171
|
const promptsDir = join(configDir, "prompts");
|
|
10775
|
-
|
|
10776
|
-
|
|
11172
|
+
// #3798 — the prompt leg as one function, called twice. `config push`
|
|
11173
|
+
// applies prompts BEFORE server functions, and an agent's tools name
|
|
11174
|
+
// functions the server must hold when the agent is created, so a tree
|
|
11175
|
+
// that pushes an agent together with its tool functions would be
|
|
11176
|
+
// refused on its first push. Chat and decisions files keep their place;
|
|
11177
|
+
// agent files are applied after the server-functions leg below
|
|
11178
|
+
// (`deferredAgentPromptFiles`). Nothing inside the leg changed.
|
|
11179
|
+
const pushPromptFiles = async (files) => {
|
|
10777
11180
|
// #2869 — prompts join the live-state reconcile (#2731 B3). #2748
|
|
10778
11181
|
// gave the type diff's comparator and left push on the manifest byte
|
|
10779
11182
|
// hash, so the two commands answered "changed?" from different
|
|
@@ -10976,6 +11379,79 @@ What a push guarantees (two stages):
|
|
|
10976
11379
|
// (mirrors `applyWorkflowUpdate`). The adopt branch seeds the
|
|
10977
11380
|
// sync-state entity record before calling this so the stamp lands.
|
|
10978
11381
|
const applyPromptUpdate = async (promptId, expectedModifiedAt) => {
|
|
11382
|
+
// #3799 (CSO-3799-005) — an agent's declaration is checked against
|
|
11383
|
+
// every config the server holds non-archived, so a file that
|
|
11384
|
+
// retires a config AND declares a schema that config's provider
|
|
11385
|
+
// cannot carry must retire it first: sent the other way round,
|
|
11386
|
+
// the prompt PATCH is refused for a config the file has already
|
|
11387
|
+
// archived, and no re-run changes the order. Only the entries
|
|
11388
|
+
// the file archives and the server does not yet — every other
|
|
11389
|
+
// config update still follows the prompt, and decisions prompts
|
|
11390
|
+
// keep their sequence. A config PATCH does not move the
|
|
11391
|
+
// prompt's `modifiedAt`, so the concurrency guard below still
|
|
11392
|
+
// holds.
|
|
11393
|
+
//
|
|
11394
|
+
// #3801 (DSO-3801-001) — the same staging, with a second admission
|
|
11395
|
+
// rule. The admin API refuses clearing a CHAT prompt's
|
|
11396
|
+
// `[prompt].outputSchema` while a config it holds non-archived is
|
|
11397
|
+
// strict, so a file that clears the schema AND turns strict output
|
|
11398
|
+
// off (or retires the config) would be refused on every push if the
|
|
11399
|
+
// prompt went first. Those config updates go ahead of the prompt
|
|
11400
|
+
// PATCH, with the payload they would have been sent anyway. The
|
|
11401
|
+
// read this needs is the one the name→id mapping below makes,
|
|
11402
|
+
// moved ahead of the PATCH, so the request count does not grow.
|
|
11403
|
+
const stagedFirst = new Set();
|
|
11404
|
+
const fileKind = promptKindOf(tomlData?.prompt);
|
|
11405
|
+
const retiring = configs.filter((c) => c?.name &&
|
|
11406
|
+
String(c?.status ?? "").trim().toLowerCase() === "archived");
|
|
11407
|
+
const stagesRetiringAgent = fileKind === "agent" && retiring.length > 0;
|
|
11408
|
+
const clearsPromptSchema = fileKind === "chat" &&
|
|
11409
|
+
!declaresSchema(tomlData?.prompt?.outputSchema);
|
|
11410
|
+
const current = stagesRetiringAgent || clearsPromptSchema
|
|
11411
|
+
? await client.getPrompt(resolvedAppId, promptId)
|
|
11412
|
+
: null;
|
|
11413
|
+
const liveServerConfig = (name) => {
|
|
11414
|
+
const found = (current?.configs ?? []).find((c) => c.configName === name);
|
|
11415
|
+
return found && String(found.status ?? "active") !== "archived"
|
|
11416
|
+
? found
|
|
11417
|
+
: null;
|
|
11418
|
+
};
|
|
11419
|
+
const staged = [];
|
|
11420
|
+
if (stagesRetiringAgent) {
|
|
11421
|
+
for (const tomlConfig of retiring) {
|
|
11422
|
+
const serverConfig = liveServerConfig(tomlConfig.name);
|
|
11423
|
+
if (serverConfig)
|
|
11424
|
+
staged.push({ tomlConfig, serverConfig });
|
|
11425
|
+
}
|
|
11426
|
+
}
|
|
11427
|
+
if (clearsPromptSchema && declaresSchema(current?.outputSchema)) {
|
|
11428
|
+
for (const tomlConfig of configs) {
|
|
11429
|
+
if (!tomlConfig?.name)
|
|
11430
|
+
continue;
|
|
11431
|
+
const serverConfig = liveServerConfig(tomlConfig.name);
|
|
11432
|
+
if (!serverConfig || serverConfig.strictOutput !== true)
|
|
11433
|
+
continue;
|
|
11434
|
+
const chat = hoistDeprecatedChatKeys(tomlConfig).entry.chat ?? {};
|
|
11435
|
+
const retired = String(tomlConfig?.status ?? "").trim().toLowerCase() ===
|
|
11436
|
+
"archived";
|
|
11437
|
+
// An entry that stays strict AND live is refused in preflight
|
|
11438
|
+
// (no schema to send); it is never staged.
|
|
11439
|
+
if (chat.strictOutput === true && !retired)
|
|
11440
|
+
continue;
|
|
11441
|
+
staged.push({ tomlConfig, serverConfig });
|
|
11442
|
+
}
|
|
11443
|
+
}
|
|
11444
|
+
for (const { tomlConfig, serverConfig } of staged) {
|
|
11445
|
+
if (stagedFirst.has(tomlConfig.name))
|
|
11446
|
+
continue;
|
|
11447
|
+
try {
|
|
11448
|
+
await client.updatePromptConfig(resolvedAppId, promptId, serverConfig.configId, buildPromptConfigPayload(tomlConfig, { mode: "update" }));
|
|
11449
|
+
}
|
|
11450
|
+
catch (err) {
|
|
11451
|
+
throw wrapPromptConfigError(err, "update", key, tomlConfig.name);
|
|
11452
|
+
}
|
|
11453
|
+
stagedFirst.add(tomlConfig.name);
|
|
11454
|
+
}
|
|
10979
11455
|
const updated = await client.updatePrompt(resolvedAppId, promptId, buildPromptPushPayload(key, tomlData, { mode: "update" }), expectedModifiedAt);
|
|
10980
11456
|
info(` Updated prompt: ${key}`);
|
|
10981
11457
|
// Record the `modifiedAt` this PATCH just caused, so a later
|
|
@@ -10998,7 +11474,10 @@ What a push guarantees (two stages):
|
|
|
10998
11474
|
try {
|
|
10999
11475
|
// Fetch full prompt to get config name→ID mappings
|
|
11000
11476
|
// (updatePrompt response doesn't include configs)
|
|
11001
|
-
|
|
11477
|
+
// #3801 — the read made ahead of the prompt PATCH when the
|
|
11478
|
+
// staging needed it (neither the PATCH nor a staged config
|
|
11479
|
+
// update moves a config's id or the activation).
|
|
11480
|
+
const fullPrompt = current ?? (await client.getPrompt(resolvedAppId, promptId));
|
|
11002
11481
|
if (fullPrompt?.configs) {
|
|
11003
11482
|
for (const config of fullPrompt.configs) {
|
|
11004
11483
|
promptConfigNameToId.set(`${key}#${config.configName}`, config.configId);
|
|
@@ -11012,6 +11491,10 @@ What a push guarantees (two stages):
|
|
|
11012
11491
|
continue;
|
|
11013
11492
|
const serverConfig = fullPrompt.configs.find((c) => c.configName === configName);
|
|
11014
11493
|
if (serverConfig) {
|
|
11494
|
+
// Staged ahead of the prompt PATCH, with this same
|
|
11495
|
+
// payload (#3799 CSO-3799-005, #3801 DSO-3801-001).
|
|
11496
|
+
if (stagedFirst.has(configName))
|
|
11497
|
+
continue;
|
|
11015
11498
|
// Update existing config
|
|
11016
11499
|
try {
|
|
11017
11500
|
await client.updatePromptConfig(resolvedAppId, promptId, serverConfig.configId, buildPromptConfigPayload(tomlConfig, { mode: "update" }));
|
|
@@ -11032,18 +11515,9 @@ What a push guarantees (two stages):
|
|
|
11032
11515
|
}
|
|
11033
11516
|
if (newConfig?.configId) {
|
|
11034
11517
|
promptConfigNameToId.set(`${key}#${configName}`, newConfig.configId);
|
|
11035
|
-
// #
|
|
11036
|
-
//
|
|
11037
|
-
//
|
|
11038
|
-
const statusPatch = createdConfigStatusPatch(tomlConfig);
|
|
11039
|
-
if (statusPatch) {
|
|
11040
|
-
try {
|
|
11041
|
-
await client.updatePromptConfig(resolvedAppId, promptId, newConfig.configId, statusPatch);
|
|
11042
|
-
}
|
|
11043
|
-
catch (err) {
|
|
11044
|
-
throw wrapPromptConfigError(err, "update", key, configName);
|
|
11045
|
-
}
|
|
11046
|
-
}
|
|
11518
|
+
// #3799 — the create body carried the entry's own
|
|
11519
|
+
// `status`, so there is no follow-up patch (#3201's is
|
|
11520
|
+
// retired).
|
|
11047
11521
|
}
|
|
11048
11522
|
}
|
|
11049
11523
|
}
|
|
@@ -11377,21 +11851,11 @@ What a push guarantees (two stages):
|
|
|
11377
11851
|
throw wrapPromptConfigError(err, "create", key, extraName);
|
|
11378
11852
|
}
|
|
11379
11853
|
if (extraCreated?.configId) {
|
|
11380
|
-
|
|
11381
|
-
|
|
11382
|
-
//
|
|
11383
|
-
//
|
|
11384
|
-
//
|
|
11385
|
-
// file retired.
|
|
11386
|
-
const statusPatch = createdConfigStatusPatch(extraConfig);
|
|
11387
|
-
if (statusPatch) {
|
|
11388
|
-
try {
|
|
11389
|
-
await client.updatePromptConfig(resolvedAppId, created.promptId, extraCreated.configId, statusPatch);
|
|
11390
|
-
}
|
|
11391
|
-
catch (err) {
|
|
11392
|
-
throw wrapPromptConfigError(err, "update", key, configName);
|
|
11393
|
-
}
|
|
11394
|
-
}
|
|
11854
|
+
promptConfigNameToId.set(`${key}#${extraName}`, extraCreated.configId);
|
|
11855
|
+
// #3799 — as on the update path: the create carried the
|
|
11856
|
+
// entry's `status`, so a retired entry is retired when
|
|
11857
|
+
// it is created and the fresh environment never runs a
|
|
11858
|
+
// config the file retired.
|
|
11395
11859
|
}
|
|
11396
11860
|
}
|
|
11397
11861
|
info(` Created ${configs.length - 1} additional config(s) for prompt: ${key}`);
|
|
@@ -11423,25 +11887,17 @@ What a push guarantees (two stages):
|
|
|
11423
11887
|
activated = true;
|
|
11424
11888
|
}
|
|
11425
11889
|
}
|
|
11426
|
-
// #
|
|
11427
|
-
// the
|
|
11428
|
-
//
|
|
11429
|
-
//
|
|
11430
|
-
//
|
|
11431
|
-
//
|
|
11432
|
-
//
|
|
11433
|
-
//
|
|
11434
|
-
|
|
11435
|
-
|
|
11436
|
-
|
|
11437
|
-
if (created?.promptId && seededConfigId && seededStatusPatch) {
|
|
11438
|
-
try {
|
|
11439
|
-
await client.updatePromptConfig(resolvedAppId, created.promptId, seededConfigId, seededStatusPatch);
|
|
11440
|
-
}
|
|
11441
|
-
catch (err) {
|
|
11442
|
-
throw wrapPromptConfigError(err, "update", key, firstConfigPatch?.configName || seededConfig?.configName);
|
|
11443
|
-
}
|
|
11444
|
-
}
|
|
11890
|
+
// #3799 (DSO-3799-005) — the seeded config's `status` is in
|
|
11891
|
+
// the create body (`POST /prompts` honors it now), so #3201's
|
|
11892
|
+
// trailing patch is gone. Activation still runs after every
|
|
11893
|
+
// create (#2645), so the marked entry is what the prompt ends
|
|
11894
|
+
// up serving; when the FIRST entry is the retired one, the
|
|
11895
|
+
// seeded config the prompt points at is created archived and
|
|
11896
|
+
// the activation moves the pointer off it. A push that dies
|
|
11897
|
+
// between the two leaves a prompt pointing at an archived
|
|
11898
|
+
// config — recovered by re-running the push, and the price of
|
|
11899
|
+
// never creating a config in a state the file did not ask for
|
|
11900
|
+
// (which is what an agent's schema check fires on).
|
|
11445
11901
|
// Stamp sync state ONLY after all create work (base prompt +
|
|
11446
11902
|
// every extra config) succeeded. If an extra-config create
|
|
11447
11903
|
// above threw, we never reach here, so the file hash is not
|
|
@@ -11477,6 +11933,13 @@ What a push guarantees (two stages):
|
|
|
11477
11933
|
}
|
|
11478
11934
|
}
|
|
11479
11935
|
}
|
|
11936
|
+
};
|
|
11937
|
+
let deferredAgentPromptFiles = [];
|
|
11938
|
+
if (existsSync(promptsDir)) {
|
|
11939
|
+
const files = selectFiles("prompt", promptsDir, readdirSync(promptsDir).filter((f) => f.endsWith(".toml")));
|
|
11940
|
+
const ordered = partitionPromptFilesForPush(promptsDir, files);
|
|
11941
|
+
deferredAgentPromptFiles = ordered.afterFunctions;
|
|
11942
|
+
await pushPromptFiles(ordered.beforeFunctions);
|
|
11480
11943
|
}
|
|
11481
11944
|
// Process transforms (Rhai scripts) — issue #892, slice 7 of #783.
|
|
11482
11945
|
//
|
|
@@ -13850,7 +14313,11 @@ What a push guarantees (two stages):
|
|
|
13850
14313
|
let pushedVersion;
|
|
13851
14314
|
try {
|
|
13852
14315
|
if (!functionId) {
|
|
13853
|
-
|
|
14316
|
+
// #3835 — a create the server could not confirm in storage
|
|
14317
|
+
// (`STORAGE_CONSISTENCY_PENDING`) is asked again, not recorded
|
|
14318
|
+
// as not applied. Its row keeps the key, so a retry that meets
|
|
14319
|
+
// it as a 409 is adopted below like any other held key.
|
|
14320
|
+
const created = await retryOnStoragePending(() => client.createFunction(resolvedAppId, buildFunctionCreatePayload(key, tomlData.function ?? {}))).catch(async (err) => {
|
|
13854
14321
|
// #1006 — a function the server already holds under this key
|
|
13855
14322
|
// is adopted rather than re-created. The conflict may name a
|
|
13856
14323
|
// WORKFLOW, a script or a webhook instead: those are the
|
|
@@ -13949,6 +14416,11 @@ What a push guarantees (two stages):
|
|
|
13949
14416
|
}
|
|
13950
14417
|
}
|
|
13951
14418
|
}
|
|
14419
|
+
// #3798 — the agent prompts, now that every function they name that
|
|
14420
|
+
// this push applies is on the server (see `pushPromptFiles`).
|
|
14421
|
+
if (deferredAgentPromptFiles.length > 0) {
|
|
14422
|
+
await pushPromptFiles(deferredAgentPromptFiles);
|
|
14423
|
+
}
|
|
13952
14424
|
// Process group type configs
|
|
13953
14425
|
const groupTypeConfigsDir = join(configDir, "group-type-configs");
|
|
13954
14426
|
if (existsSync(groupTypeConfigsDir)) {
|