primitive-admin 1.1.0-alpha.85 → 1.1.0-alpha.86
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 +8 -0
- package/dist/src/commands/analytics.js +13 -1
- package/dist/src/commands/analytics.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 +73 -41
- package/dist/src/commands/sync.js +527 -80
- 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 +93 -7
- package/dist/src/lib/api-client.js +72 -19
- package/dist/src/lib/api-client.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/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-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 +1914 -155
- 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/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/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/package.json +2 -2
|
@@ -34,10 +34,18 @@ import { WORKFLOW_MODEL_DEFAULTS } from "../lib/workflow-defaults.js";
|
|
|
34
34
|
// #2644 — the one definition per configuration object, vendored from the server.
|
|
35
35
|
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
36
|
import { buildPayloadFromToml, projectEntityToPayload, projectRecordToToml, tomlOwned, } from "../lib/config-payload.js";
|
|
37
|
-
import { CONFIG_SURFACES, isEmptyForEmit, } from "../lib/generated-config-surfaces.js";
|
|
37
|
+
import { CONFIG_SURFACES, isEmptyForEmit, tomlGroupsOf, } from "../lib/generated-config-surfaces.js";
|
|
38
38
|
// #3626 — the prompt KIND rule, vendored with the definitions so the CLI's
|
|
39
39
|
// preflight refuses exactly what the admin routes refuse.
|
|
40
40
|
import { hoistDeprecatedChatKeys, promptKindOf, resolvePromptKind, } from "../lib/generated-config-surfaces.js";
|
|
41
|
+
// #3798 — the `[prompt.agent]` declaration rule, vendored the same way.
|
|
42
|
+
import { agentDeclarationFunctionKeys, agentTestCaseRefusal, resolveAgentDeclaration, } from "../lib/generated-config-surfaces.js";
|
|
43
|
+
// #3799 — the meaning-preserving schema translation, vendored the same way, so
|
|
44
|
+
// the preflight refuses exactly the schemas the admin routes refuse.
|
|
45
|
+
import { agentSchemaTranslationRefusals } from "../lib/generated-config-surfaces.js";
|
|
46
|
+
// #3801 — the strict-output rule, vendored the same way, so the preflight
|
|
47
|
+
// refuses exactly the strict configs the admin routes refuse.
|
|
48
|
+
import { resolvePromptStrictOutput } from "../lib/generated-config-surfaces.js";
|
|
41
49
|
// #3179 — the function's two halves: the header payloads and the envelope
|
|
42
50
|
// `config push` sends, and the bytes `config pull` writes back.
|
|
43
51
|
import { applyFunctionPull, buildFunctionConfigPush, createDocumentSchemaDropWarner, documentSchemaRetentionFailure, buildFunctionCreatePayload, buildFunctionUpdatePayload, functionDiffEntity, functionTomlIgnoredKeyWarnings, functionTomlPreflightErrors, planDocumentSchemaPull, planFunctionPull, serializeFunction, } from "../lib/function-sync.js";
|
|
@@ -1668,22 +1676,20 @@ export function buildBlobBucketUpdatePayload(bucket) {
|
|
|
1668
1676
|
*/
|
|
1669
1677
|
const PROMPT_CONFIG_DEFAULT_STATUS = fieldServerDefault(PROMPT_CONFIG_TABLE, "status");
|
|
1670
1678
|
/**
|
|
1671
|
-
*
|
|
1672
|
-
* (#3201).
|
|
1679
|
+
* #3201's post-create status patch is RETIRED — #3799 (DSO-3799-005).
|
|
1673
1680
|
*
|
|
1674
|
-
* `status`
|
|
1675
|
-
* both
|
|
1676
|
-
* `status = "archived"`
|
|
1677
|
-
*
|
|
1678
|
-
*
|
|
1679
|
-
*
|
|
1681
|
+
* `status` was update-only, so `POST /prompts` and `POST /prompts/:id/configs`
|
|
1682
|
+
* both assigned `active` whatever the body said, and an authored
|
|
1683
|
+
* `status = "archived"` needed a follow-up PATCH ordered after the activation of
|
|
1684
|
+
* the entry the file marks live. Both create paths honor the key now
|
|
1685
|
+
* (`writableOn: BOTH`, validated by one rule on all three paths), so the entry
|
|
1686
|
+
* lands retired on its own create: the same end state, two fewer calls per
|
|
1687
|
+
* retired config, and no window in which the server holds a status the file
|
|
1688
|
+
* never asked for. That window is what made a pulled tree unpushable into a
|
|
1689
|
+
* fresh environment once an agent's schemas are checked against each
|
|
1690
|
+
* non-archived config's provider — the check fired on the transient active
|
|
1691
|
+
* state.
|
|
1680
1692
|
*/
|
|
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
1693
|
/**
|
|
1688
1694
|
* Which `[[configs]]` entry a prompt file marks as live (issue #2645).
|
|
1689
1695
|
*
|
|
@@ -1791,6 +1797,11 @@ export function serializePrompt(prompt, logger) {
|
|
|
1791
1797
|
...projectRecordToToml(PROMPT_TABLE, prompt, {
|
|
1792
1798
|
inputSchema: jsonField(promptSubject),
|
|
1793
1799
|
outputSchema: jsonField(promptSubject),
|
|
1800
|
+
// #3798 — the agent declaration goes out as `[prompt.agent]` tables
|
|
1801
|
+
// (JSON-string fallback for a `null` TOML cannot spell), exactly as
|
|
1802
|
+
// stored: the server keeps it as authored, so what pull writes is
|
|
1803
|
+
// what the author pushed.
|
|
1804
|
+
agent: jsonField(promptSubject),
|
|
1794
1805
|
}),
|
|
1795
1806
|
},
|
|
1796
1807
|
configs: (prompt.configs || []).map((config) => ({
|
|
@@ -1819,7 +1830,10 @@ export function serializePrompt(prompt, logger) {
|
|
|
1819
1830
|
// `config pull` and `config diff` down with it. The JSON-string
|
|
1820
1831
|
// fallback keeps the declaration whole either way.
|
|
1821
1832
|
questions: jsonField(`Prompt '${prompt?.promptKey}' config '${config?.configName}'`),
|
|
1822
|
-
}
|
|
1833
|
+
},
|
|
1834
|
+
// #3798 — the model settings chat and agent share are written under the
|
|
1835
|
+
// block the prompt's kind names: `[configs.agent]` for an agent.
|
|
1836
|
+
{ tomlGroup: promptKindOf(prompt) }),
|
|
1823
1837
|
// #2645 — the live config has to be identifiable in the repo. Emitted
|
|
1824
1838
|
// only on the active entry (`undefined` elsewhere so the key is omitted),
|
|
1825
1839
|
// which keeps the marker unambiguous by construction.
|
|
@@ -1891,6 +1905,16 @@ export function buildPromptPushPayload(key, tomlData, { mode = "update" } = {})
|
|
|
1891
1905
|
// dropped and leaving a stale rule live. On create it is omitted so
|
|
1892
1906
|
// the push-adopt path (#1006) can still reach the server's 409.
|
|
1893
1907
|
accessRule: (value) => (create ? value : (value ?? null)),
|
|
1908
|
+
// #3798 — the agent declaration, as authored: the server stores it
|
|
1909
|
+
// unchanged, so the value sent is the value pull writes back. On
|
|
1910
|
+
// update a deleted block CLEARS it — for an agent only: a chat or
|
|
1911
|
+
// decisions file carries no block to delete, and omitting the key
|
|
1912
|
+
// keeps its body what it was before this field existed.
|
|
1913
|
+
agent: (value) => create || value !== undefined
|
|
1914
|
+
? value
|
|
1915
|
+
: promptKindOf(prompt) === "agent"
|
|
1916
|
+
? null
|
|
1917
|
+
: undefined,
|
|
1894
1918
|
// #3626, CR3626-004 — the kind NORMALIZED, exactly as the resolver
|
|
1895
1919
|
// that validates it reads it. `kind = " chat "` and `kind = ""` both
|
|
1896
1920
|
// mean `chat` to `resolvePromptKind`, so the server stores `chat` and
|
|
@@ -1914,6 +1938,10 @@ export function buildPromptPushPayload(key, tomlData, { mode = "update" } = {})
|
|
|
1914
1938
|
};
|
|
1915
1939
|
if (create)
|
|
1916
1940
|
payload.promptKey = key;
|
|
1941
|
+
// #3798 — a prompt that declares no agent block sends no `agent` key at all,
|
|
1942
|
+
// so a chat or decisions body is byte for byte what it was.
|
|
1943
|
+
if (payload.agent === undefined)
|
|
1944
|
+
delete payload.agent;
|
|
1917
1945
|
return payload;
|
|
1918
1946
|
}
|
|
1919
1947
|
/**
|
|
@@ -1947,9 +1975,15 @@ export function buildPromptCreatePayload(key, tomlData) {
|
|
|
1947
1975
|
// #3626, SO3626-001 — the placeholder is a CHAT fallback. A decisions prompt
|
|
1948
1976
|
// has no template at all (its input is a `state` value plus its questions),
|
|
1949
1977
|
// and injecting one would store a chat key the block rule then refuses on
|
|
1950
|
-
// every later update.
|
|
1951
|
-
|
|
1952
|
-
|
|
1978
|
+
// every later update. #3798 — nor has an agent: the model sees each message
|
|
1979
|
+
// as sent.
|
|
1980
|
+
if (promptKindOf(prompt) !== "chat") {
|
|
1981
|
+
const body = { ...prompt, ...seed };
|
|
1982
|
+
for (const field of Object.keys(body)) {
|
|
1983
|
+
if (body[field] === undefined)
|
|
1984
|
+
delete body[field];
|
|
1985
|
+
}
|
|
1986
|
+
return body;
|
|
1953
1987
|
}
|
|
1954
1988
|
return {
|
|
1955
1989
|
...prompt,
|
|
@@ -1987,12 +2021,11 @@ const SEEDED_CONFIG_DEFERRED = ["outputSchema", "description"];
|
|
|
1987
2021
|
* response rather than assumed, so a handler that one day honors the name
|
|
1988
2022
|
* produces no needless PATCH.
|
|
1989
2023
|
*
|
|
1990
|
-
* #3201 — the entry's `status`
|
|
1991
|
-
*
|
|
1992
|
-
* config
|
|
1993
|
-
*
|
|
1994
|
-
*
|
|
1995
|
-
* applied after activation instead (`createdConfigStatusPatch`).
|
|
2024
|
+
* #3201 — the entry's `status` was deferred the OTHER way, to a patch after the
|
|
2025
|
+
* activation, because the create pointed the prompt's `activeConfigId` at a
|
|
2026
|
+
* config it had forced live. #3799 (DSO-3799-005) — the create carries the
|
|
2027
|
+
* entry's `status` itself now, so there is no deferral either way and this patch
|
|
2028
|
+
* is the two fields `POST /prompts` cannot express, nothing more.
|
|
1996
2029
|
*/
|
|
1997
2030
|
export function seededOnlyPromptConfigFields(firstConfig, seededConfigName = "default") {
|
|
1998
2031
|
const full = buildPromptConfigPayload(firstConfig || {}, { mode: "update" });
|
|
@@ -2049,6 +2082,12 @@ export function buildPromptConfigPayload(tomlConfig, { mode = "update" } = {}) {
|
|
|
2049
2082
|
mode,
|
|
2050
2083
|
values: {
|
|
2051
2084
|
provider: (value) => (create ? value || "openrouter" : value),
|
|
2085
|
+
// #3799 (DSO-3799-005) — carried on CREATE when the entry names one, and
|
|
2086
|
+
// omitted otherwise, so the create body for a file that says nothing
|
|
2087
|
+
// about status is what the server has always received (principle 5). On
|
|
2088
|
+
// update it still sends the declared default for an absent key, which is
|
|
2089
|
+
// what lets a deleted line converge (#2880 DSO-002).
|
|
2090
|
+
status: (value) => create ? value : (value ?? PROMPT_CONFIG_DEFAULT_STATUS),
|
|
2052
2091
|
model: (value) => create ? value || "google/gemini-2.0-flash-001" : value,
|
|
2053
2092
|
description: tomlOwned,
|
|
2054
2093
|
systemPrompt: tomlOwned,
|
|
@@ -2065,6 +2104,9 @@ export function buildPromptConfigPayload(tomlConfig, { mode = "update" } = {}) {
|
|
|
2065
2104
|
// the merged config refused as "both set".
|
|
2066
2105
|
reasoningEffort: tomlOwned,
|
|
2067
2106
|
reasoningBudget: tomlOwned,
|
|
2107
|
+
// #3801 — deleting `strictOutput = true` has to turn strict output off
|
|
2108
|
+
// server-side, or the file stops describing the request that is sent.
|
|
2109
|
+
strictOutput: tomlOwned,
|
|
2068
2110
|
// #3626, SO3626-002 — the decisions declaration is TOML-owned on the
|
|
2069
2111
|
// same terms: deleting `[configs.decisions]` has to clear `questions`
|
|
2070
2112
|
// server-side, or the file stops describing what the model is asked.
|
|
@@ -2182,13 +2224,14 @@ export function configUnknownKeyError(filePath, tomlData, table) {
|
|
|
2182
2224
|
const misplaced = trulyUnknown
|
|
2183
2225
|
.map((key) => ({
|
|
2184
2226
|
key,
|
|
2185
|
-
|
|
2186
|
-
|
|
2227
|
+
groups: tomlGroupsOf(table.fields.find((f) => f.tomlKey === key && f.tomlGroup) ??
|
|
2228
|
+
{}),
|
|
2187
2229
|
}))
|
|
2188
|
-
.filter((entry) =>
|
|
2189
|
-
for (const { key,
|
|
2230
|
+
.filter((entry) => entry.groups.length > 0);
|
|
2231
|
+
for (const { key, groups } of misplaced) {
|
|
2232
|
+
// #3798 — a key two blocks share names both; the prompt's kind picks one.
|
|
2190
2233
|
messages.push(`${filePath}: ${prefix}[0].${key} is not a [[${prefix}]] key; ` +
|
|
2191
|
-
`write it under [${prefix}.${
|
|
2234
|
+
`write it under ${groups.map((g) => `[${prefix}.${g}]`).join(" or ")}`);
|
|
2192
2235
|
}
|
|
2193
2236
|
const misplacedKeys = new Set(misplaced.map((m) => m.key));
|
|
2194
2237
|
const stillUnknown = trulyUnknown.filter((k) => !misplacedKeys.has(k));
|
|
@@ -2636,6 +2679,9 @@ export async function runConfigPushPreflight(input) {
|
|
|
2636
2679
|
// single pull-then-push clears the notice. Printed even under `--dry-run`,
|
|
2637
2680
|
// which never reaches the apply — the notice is about the FILE.
|
|
2638
2681
|
const preflightPromptsDir = join(configDir, "prompts");
|
|
2682
|
+
// #3798 — the functions an agent's references are checked against, read
|
|
2683
|
+
// once and only when a selected prompt is an agent that names one.
|
|
2684
|
+
let agentFunctions = null;
|
|
2639
2685
|
if (existsSync(preflightPromptsDir)) {
|
|
2640
2686
|
for (const file of selectFiles("prompt", preflightPromptsDir, readdirSync(preflightPromptsDir).filter((f) => f.endsWith(".toml")))) {
|
|
2641
2687
|
const filePath = join(preflightPromptsDir, file);
|
|
@@ -2650,8 +2696,36 @@ export async function runConfigPushPreflight(input) {
|
|
|
2650
2696
|
const notice = promptFlatChatKeyWarning(filePath, tomlData);
|
|
2651
2697
|
if (notice)
|
|
2652
2698
|
warn(` ${notice}`);
|
|
2699
|
+
// #3798 — an agent's tool, turn-context and history functions must
|
|
2700
|
+
// exist, and a tool's must declare both schemas, before anything is
|
|
2701
|
+
// applied. Bounded by `--only` like the rest of this loop.
|
|
2702
|
+
if (promptKindOf(tomlData?.prompt) === "agent" &&
|
|
2703
|
+
agentDeclarationFunctionKeys(tomlData?.prompt?.agent).length > 0) {
|
|
2704
|
+
if (!agentFunctions) {
|
|
2705
|
+
let live = null;
|
|
2706
|
+
try {
|
|
2707
|
+
live = (await client.listFunctions(resolvedAppId)).items;
|
|
2708
|
+
}
|
|
2709
|
+
catch {
|
|
2710
|
+
live = null;
|
|
2711
|
+
}
|
|
2712
|
+
agentFunctions = agentFunctionFactsForPush({
|
|
2713
|
+
configDir,
|
|
2714
|
+
pushSelection,
|
|
2715
|
+
live,
|
|
2716
|
+
});
|
|
2717
|
+
}
|
|
2718
|
+
for (const message of promptAgentFunctionErrors(filePath, tomlData, agentFunctions)) {
|
|
2719
|
+
preflightValidationErrors.push(` ${message}`);
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2653
2722
|
}
|
|
2654
2723
|
}
|
|
2724
|
+
// #3798 — a test case on an agent is refused by the admin routes; say so
|
|
2725
|
+
// here, before any other file is applied.
|
|
2726
|
+
for (const message of promptAgentSidecarErrors(configDir)) {
|
|
2727
|
+
preflightValidationErrors.push(` ${message}`);
|
|
2728
|
+
}
|
|
2655
2729
|
// The `<key>.tests/*.toml` sidecars, which sit beside the block they
|
|
2656
2730
|
// test rather than in a type directory of their own. Same posture,
|
|
2657
2731
|
// reached through the registry's `test-case` surface — and through the
|
|
@@ -2691,6 +2765,41 @@ export async function runConfigPushPreflight(input) {
|
|
|
2691
2765
|
* whose spelling is not the declared type. A file that does not parse is left
|
|
2692
2766
|
* to the per-type loop that reads it, which names the parse error itself.
|
|
2693
2767
|
*/
|
|
2768
|
+
/**
|
|
2769
|
+
* The blocks one `[[configs]]` entry carries that its prompt's kind does not
|
|
2770
|
+
* name — #3798. An agent's entry may carry only `[configs.agent]`; a chat or
|
|
2771
|
+
* decisions entry may not carry it. `movedFlat` are the deprecated flat chat
|
|
2772
|
+
* keys the hoist moved under `chat`, named as the author wrote them.
|
|
2773
|
+
*
|
|
2774
|
+
* Only the blocks the other arms of `resolvePromptKind` cannot see are checked
|
|
2775
|
+
* here: a decisions block on a chat entry, and chat keys on a decisions entry,
|
|
2776
|
+
* stay the resolver's refusals with their #3626 wording.
|
|
2777
|
+
*/
|
|
2778
|
+
function promptEntryBlockErrors(kind, entry, movedFlat) {
|
|
2779
|
+
const has = (block) => entry != null && typeof entry === "object" && entry[block] !== undefined;
|
|
2780
|
+
const errors = [];
|
|
2781
|
+
if (kind === "agent") {
|
|
2782
|
+
if (movedFlat.length > 0) {
|
|
2783
|
+
errors.push(`sets ${movedFlat.join(", ")} flat — the deprecated [configs.chat] ` +
|
|
2784
|
+
'spelling — but prompt kind is "agent", so it may carry only a ' +
|
|
2785
|
+
"[configs.agent] block; write them under [configs.agent]");
|
|
2786
|
+
}
|
|
2787
|
+
for (const block of ["chat", "decisions"]) {
|
|
2788
|
+
if (!has(block))
|
|
2789
|
+
continue;
|
|
2790
|
+
errors.push(`carries a [configs.${block}] block, but prompt kind is "agent", so ` +
|
|
2791
|
+
"it may carry only a [configs.agent] block (systemPrompt, " +
|
|
2792
|
+
"temperature, topP, maxTokens, reasoningEffort, reasoningBudget)");
|
|
2793
|
+
}
|
|
2794
|
+
return errors;
|
|
2795
|
+
}
|
|
2796
|
+
if (has("agent")) {
|
|
2797
|
+
errors.push(`carries a [configs.agent] block, but prompt kind is "${kind}", so it ` +
|
|
2798
|
+
`may carry only a [configs.${kind}] block. An agent is its own ` +
|
|
2799
|
+
'prompt: create it under a new key with kind = "agent"');
|
|
2800
|
+
}
|
|
2801
|
+
return errors;
|
|
2802
|
+
}
|
|
2694
2803
|
/**
|
|
2695
2804
|
* A prompt file's KIND errors — #3626, decidable from the file alone.
|
|
2696
2805
|
*
|
|
@@ -2702,15 +2811,94 @@ export async function runConfigPushPreflight(input) {
|
|
|
2702
2811
|
* the server's cannot disagree — and this one lands in the preflight, before
|
|
2703
2812
|
* the first mutating call.
|
|
2704
2813
|
*/
|
|
2814
|
+
/**
|
|
2815
|
+
* The `[[configs]]` entries of a prompt file as the agent rules read them —
|
|
2816
|
+
* #3799: which provider each named configuration runs on, and whether the file
|
|
2817
|
+
* retires it.
|
|
2818
|
+
*
|
|
2819
|
+
* An archived entry is skipped by the translation rule (a config the server does
|
|
2820
|
+
* not run cannot make the declaration untranslatable), which is what lets a
|
|
2821
|
+
* pulled tree keep a retired config on a provider the current schemas could not
|
|
2822
|
+
* reach. `status` is read through the same default the surface declares, so an
|
|
2823
|
+
* entry that omits the key is live.
|
|
2824
|
+
*/
|
|
2825
|
+
function promptFileConfigFacts(entries) {
|
|
2826
|
+
return (entries || []).map((entry, index) => ({
|
|
2827
|
+
configName: String(entry?.name ?? `configs[${index}]`),
|
|
2828
|
+
provider: String(entry?.provider ?? ""),
|
|
2829
|
+
status: String(entry?.status ?? PROMPT_CONFIG_DEFAULT_STATUS),
|
|
2830
|
+
}));
|
|
2831
|
+
}
|
|
2705
2832
|
export function promptKindFileErrors(filePath, tomlData) {
|
|
2706
2833
|
const entries = Array.isArray(tomlData?.configs) ? tomlData.configs : [];
|
|
2707
2834
|
const errors = [];
|
|
2835
|
+
const fileKind = promptKindOf(tomlData?.prompt);
|
|
2836
|
+
// #3798 — the `[prompt.agent]` declaration's file-decidable half: its shape,
|
|
2837
|
+
// and `[prompt].outputSchema` on an agent. The function half needs app state
|
|
2838
|
+
// and runs in the push preflight (`promptAgentFunctionErrors`).
|
|
2839
|
+
const declaration = resolveAgentDeclaration({
|
|
2840
|
+
kind: tomlData?.prompt?.kind,
|
|
2841
|
+
agent: tomlData?.prompt?.agent,
|
|
2842
|
+
outputSchema: tomlData?.prompt?.outputSchema,
|
|
2843
|
+
});
|
|
2844
|
+
if (declaration.ok === false) {
|
|
2845
|
+
for (const refusal of declaration.refusals) {
|
|
2846
|
+
errors.push(`${filePath}: ${refusal.message}`);
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
else if (fileKind === "agent") {
|
|
2850
|
+
// #3799 — the schemas a CLIENT tool and an event declare are in the file,
|
|
2851
|
+
// so whether each provider the file configures can carry them without
|
|
2852
|
+
// losing meaning is file-decidable too: `config diff` reports the same row
|
|
2853
|
+
// as `config push`. A server tool's schemas are its FUNCTION's and need app
|
|
2854
|
+
// state, so they are checked in the push preflight
|
|
2855
|
+
// (`promptAgentFunctionErrors`). Only after the shape refusals above, so a
|
|
2856
|
+
// malformed declaration is not also reported as untranslatable.
|
|
2857
|
+
for (const refusal of agentSchemaTranslationRefusals({
|
|
2858
|
+
declaration: tomlData?.prompt?.agent,
|
|
2859
|
+
configs: promptFileConfigFacts(entries),
|
|
2860
|
+
})) {
|
|
2861
|
+
errors.push(`${filePath}: ${refusal.message}`);
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2708
2864
|
for (const [index, entry] of entries.entries()) {
|
|
2709
2865
|
const hoisted = hoistDeprecatedChatKeys(entry);
|
|
2710
2866
|
for (const key of hoisted.conflicts) {
|
|
2711
2867
|
errors.push(`${filePath}: configs[${index}].${key} is set both flat and under ` +
|
|
2712
2868
|
`[configs.chat]; keep the [configs.chat] spelling`);
|
|
2713
2869
|
}
|
|
2870
|
+
// #3798 — the block rule for three kinds, stated per entry: an entry may
|
|
2871
|
+
// carry only the block its prompt's kind names. An agent's `[configs.agent]`
|
|
2872
|
+
// holds keys chat also holds, so a stray block would otherwise be read (or
|
|
2873
|
+
// ignored) in silence by the payload builder.
|
|
2874
|
+
const blockErrors = promptEntryBlockErrors(fileKind, entry, hoisted.moved).map((message) => `${filePath}: configs[${index}] ${message}`);
|
|
2875
|
+
if (blockErrors.length > 0) {
|
|
2876
|
+
errors.push(...blockErrors);
|
|
2877
|
+
continue;
|
|
2878
|
+
}
|
|
2879
|
+
if (fileKind === "agent") {
|
|
2880
|
+
const agent = hoisted.entry.agent ?? {};
|
|
2881
|
+
const resolution = resolvePromptKind({
|
|
2882
|
+
kind: "agent",
|
|
2883
|
+
provider: hoisted.entry.provider,
|
|
2884
|
+
model: hoisted.entry.model,
|
|
2885
|
+
config: {
|
|
2886
|
+
systemPrompt: agent.systemPrompt,
|
|
2887
|
+
temperature: agent.temperature,
|
|
2888
|
+
topP: agent.topP,
|
|
2889
|
+
maxTokens: agent.maxTokens,
|
|
2890
|
+
reasoningEffort: agent.reasoningEffort,
|
|
2891
|
+
reasoningBudget: agent.reasoningBudget,
|
|
2892
|
+
// #3801 — a chat key: named, so the refusal says which key an agent
|
|
2893
|
+
// does not take (§D1: an agent has no output schema).
|
|
2894
|
+
strictOutput: agent.strictOutput,
|
|
2895
|
+
},
|
|
2896
|
+
});
|
|
2897
|
+
if (resolution.ok === false) {
|
|
2898
|
+
errors.push(`${filePath}: configs[${index}] — ${resolution.error}`);
|
|
2899
|
+
}
|
|
2900
|
+
continue;
|
|
2901
|
+
}
|
|
2714
2902
|
const chat = hoisted.entry.chat ?? {};
|
|
2715
2903
|
const decisions = hoisted.entry.decisions ?? {};
|
|
2716
2904
|
// #3626 — the FIRST entry of a chat prompt carries the create seed's
|
|
@@ -2726,7 +2914,7 @@ export function promptKindFileErrors(filePath, tomlData) {
|
|
|
2726
2914
|
// `userPromptTemplate`, so reading absence more narrowly than the builder
|
|
2727
2915
|
// does refused the file this CLI had just written, for a value the create
|
|
2728
2916
|
// would have replaced with the placeholder anyway.
|
|
2729
|
-
const seedsTemplate = index === 0 &&
|
|
2917
|
+
const seedsTemplate = index === 0 && fileKind === "chat";
|
|
2730
2918
|
const userPromptTemplate = seedsTemplate
|
|
2731
2919
|
? chat.userPromptTemplate || "{{ input }}"
|
|
2732
2920
|
: chat.userPromptTemplate;
|
|
@@ -2744,15 +2932,203 @@ export function promptKindFileErrors(filePath, tomlData) {
|
|
|
2744
2932
|
outputSchema: chat.outputSchema,
|
|
2745
2933
|
reasoningEffort: chat.reasoningEffort,
|
|
2746
2934
|
reasoningBudget: chat.reasoningBudget,
|
|
2935
|
+
strictOutput: chat.strictOutput,
|
|
2747
2936
|
questions: decisions.questions,
|
|
2748
2937
|
},
|
|
2749
2938
|
});
|
|
2750
2939
|
if (resolution.ok === false) {
|
|
2751
2940
|
errors.push(`${filePath}: configs[${index}] — ${resolution.error}`);
|
|
2941
|
+
continue;
|
|
2942
|
+
}
|
|
2943
|
+
// #3801 — strict output, file-decidable: the entry's own keys, the file's
|
|
2944
|
+
// `[prompt].outputSchema` and the entry's status (an archived entry is not
|
|
2945
|
+
// held to the prompt schema, DSO-3801-002). The route runs the same rule
|
|
2946
|
+
// against what the server holds.
|
|
2947
|
+
if (resolution.kind === "chat") {
|
|
2948
|
+
const strict = resolvePromptStrictOutput({
|
|
2949
|
+
provider: hoisted.entry.provider,
|
|
2950
|
+
strictOutput: chat.strictOutput,
|
|
2951
|
+
outputFormat: chat.outputFormat,
|
|
2952
|
+
configOutputSchema: chat.outputSchema,
|
|
2953
|
+
promptOutputSchema: tomlData?.prompt?.outputSchema,
|
|
2954
|
+
status: hoisted.entry.status ?? PROMPT_CONFIG_DEFAULT_STATUS,
|
|
2955
|
+
});
|
|
2956
|
+
if (strict.ok === false) {
|
|
2957
|
+
errors.push(`${filePath}: configs[${index}] — ${strict.error} (${strict.code})`);
|
|
2958
|
+
}
|
|
2752
2959
|
}
|
|
2753
2960
|
}
|
|
2754
2961
|
return errors;
|
|
2755
2962
|
}
|
|
2963
|
+
/** A schema slot counts as declared when it holds anything but nothing. */
|
|
2964
|
+
function declaresSchema(value) {
|
|
2965
|
+
if (value === undefined || value === null)
|
|
2966
|
+
return false;
|
|
2967
|
+
return !(typeof value === "string" && value.trim() === "");
|
|
2968
|
+
}
|
|
2969
|
+
/**
|
|
2970
|
+
* The functions an agent's references are checked against at push — #3798,
|
|
2971
|
+
* DSO-3798-002.
|
|
2972
|
+
*
|
|
2973
|
+
* `config push` applies prompts, then (for agents) the server functions leg,
|
|
2974
|
+
* so a reference may be satisfied by a function that is live already or by one
|
|
2975
|
+
* this push creates. What the push BELIEVES is therefore the live listing
|
|
2976
|
+
* overlaid by the function files this push SELECTS (`--only`, every file when
|
|
2977
|
+
* unscoped): a selected file's schemas are what the server will hold after the
|
|
2978
|
+
* push, so it wins over the live row. An UNSELECTED file is not read at all —
|
|
2979
|
+
* its edits will not be applied, and a function only it declares will not
|
|
2980
|
+
* exist afterwards.
|
|
2981
|
+
*
|
|
2982
|
+
* `live` is `null` when the listing could not be fetched: the selected files
|
|
2983
|
+
* still count, and `liveListed` lets the refusal say why every other reference
|
|
2984
|
+
* could not be confirmed. A live row that is archived is a tombstone, not a
|
|
2985
|
+
* function; an inactive one is accepted (the turn engine re-checks
|
|
2986
|
+
* availability on every call).
|
|
2987
|
+
*/
|
|
2988
|
+
export function agentFunctionFactsForPush(input) {
|
|
2989
|
+
const facts = {};
|
|
2990
|
+
for (const row of input.live ?? []) {
|
|
2991
|
+
const key = String(row?.functionKey ?? "");
|
|
2992
|
+
if (!key || row?.status === "archived")
|
|
2993
|
+
continue;
|
|
2994
|
+
facts[key] = {
|
|
2995
|
+
hasInputSchema: declaresSchema(row.inputSchema),
|
|
2996
|
+
hasOutputSchema: declaresSchema(row.outputSchema),
|
|
2997
|
+
// #3799 — the schemas themselves, as stored (a JSON string from the API,
|
|
2998
|
+
// an object from a tree file), so a server tool's two schemas can be
|
|
2999
|
+
// translated for each of the agent's providers.
|
|
3000
|
+
inputSchema: row.inputSchema,
|
|
3001
|
+
outputSchema: row.outputSchema,
|
|
3002
|
+
};
|
|
3003
|
+
}
|
|
3004
|
+
const functionsDir = join(input.configDir, "functions");
|
|
3005
|
+
if (existsSync(functionsDir)) {
|
|
3006
|
+
for (const file of filterFilesBySelection(input.pushSelection, "function", functionsDir, readdirSync(functionsDir).filter(isFunctionDeclarationFile))) {
|
|
3007
|
+
let table;
|
|
3008
|
+
try {
|
|
3009
|
+
table = parseTomlFile(join(functionsDir, file))?.function ?? {};
|
|
3010
|
+
}
|
|
3011
|
+
catch {
|
|
3012
|
+
// Reported by the function preflight with its own message.
|
|
3013
|
+
continue;
|
|
3014
|
+
}
|
|
3015
|
+
const key = table.key || basename(file, ".toml");
|
|
3016
|
+
facts[key] = {
|
|
3017
|
+
hasInputSchema: declaresSchema(table.inputSchema),
|
|
3018
|
+
hasOutputSchema: declaresSchema(table.outputSchema),
|
|
3019
|
+
// #3799 — a SELECTED file's schemas are what the server will hold after
|
|
3020
|
+
// this push, so they are what the translation is checked against.
|
|
3021
|
+
inputSchema: table.inputSchema,
|
|
3022
|
+
outputSchema: table.outputSchema,
|
|
3023
|
+
};
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
return { facts, liveListed: input.live !== null };
|
|
3027
|
+
}
|
|
3028
|
+
/**
|
|
3029
|
+
* The function half of an agent's declaration — #3798: every tool, turn
|
|
3030
|
+
* context and history function that the functions this push believes
|
|
3031
|
+
* (`agentFunctionFactsForPush`) do not have, and every tool function lacking an
|
|
3032
|
+
* input or output schema. The same `resolveAgentDeclaration` the admin routes
|
|
3033
|
+
* run; only its function refusals are reported here, because the collector
|
|
3034
|
+
* (`promptKindFileErrors`) already reported the file-decidable ones.
|
|
3035
|
+
*/
|
|
3036
|
+
export function promptAgentFunctionErrors(filePath, tomlData, known) {
|
|
3037
|
+
if (promptKindOf(tomlData?.prompt) !== "agent")
|
|
3038
|
+
return [];
|
|
3039
|
+
const result = resolveAgentDeclaration({
|
|
3040
|
+
kind: "agent",
|
|
3041
|
+
agent: tomlData?.prompt?.agent,
|
|
3042
|
+
functions: known.facts,
|
|
3043
|
+
});
|
|
3044
|
+
const errors = result.ok === false
|
|
3045
|
+
? result.refusals
|
|
3046
|
+
.filter((refusal) => refusal.code === "AGENT_TOOL_FUNCTION_MISSING" ||
|
|
3047
|
+
refusal.code === "AGENT_TOOL_FUNCTION_SCHEMA_MISSING")
|
|
3048
|
+
.map((refusal) => refusal.code === "AGENT_TOOL_FUNCTION_MISSING" && !known.liveListed
|
|
3049
|
+
? `${filePath}: ${refusal.message} (the app's live functions could ` +
|
|
3050
|
+
"not be listed, so only the function files this push applies were " +
|
|
3051
|
+
"checked)"
|
|
3052
|
+
: `${filePath}: ${refusal.message}`)
|
|
3053
|
+
: [];
|
|
3054
|
+
// #3799 — a SERVER tool's schemas are its function's, so translating them
|
|
3055
|
+
// needs the facts this push believes (the live listing overlaid by the
|
|
3056
|
+
// function files it selects, DSO-3798-002). The client-tool and event half is
|
|
3057
|
+
// file-decidable and reported by `promptKindFileErrors`, which `config diff`
|
|
3058
|
+
// runs too; passing the facts here means only the server-tool refusals are
|
|
3059
|
+
// new, because that collector already saw the rest.
|
|
3060
|
+
const entries = Array.isArray(tomlData?.configs) ? tomlData.configs : [];
|
|
3061
|
+
const configs = promptFileConfigFacts(entries);
|
|
3062
|
+
const fileDecidable = new Set(agentSchemaTranslationRefusals({
|
|
3063
|
+
declaration: tomlData?.prompt?.agent,
|
|
3064
|
+
configs,
|
|
3065
|
+
}).map((refusal) => refusal.message));
|
|
3066
|
+
for (const refusal of agentSchemaTranslationRefusals({
|
|
3067
|
+
declaration: tomlData?.prompt?.agent,
|
|
3068
|
+
functions: known.facts,
|
|
3069
|
+
configs,
|
|
3070
|
+
})) {
|
|
3071
|
+
if (fileDecidable.has(refusal.message))
|
|
3072
|
+
continue;
|
|
3073
|
+
errors.push(`${filePath}: ${refusal.message}`);
|
|
3074
|
+
}
|
|
3075
|
+
return errors;
|
|
3076
|
+
}
|
|
3077
|
+
/**
|
|
3078
|
+
* The order `config push` applies prompt files in — #3798. An agent names
|
|
3079
|
+
* server functions that must exist when it is created, and the push applies
|
|
3080
|
+
* prompts before functions, so agent files go AFTER the server-functions leg;
|
|
3081
|
+
* every other file keeps its place. A file that does not parse stays first, so
|
|
3082
|
+
* the leg reports it where it always has.
|
|
3083
|
+
*/
|
|
3084
|
+
export function partitionPromptFilesForPush(promptsDir, files) {
|
|
3085
|
+
const beforeFunctions = [];
|
|
3086
|
+
const afterFunctions = [];
|
|
3087
|
+
for (const file of files) {
|
|
3088
|
+
let kind = "chat";
|
|
3089
|
+
try {
|
|
3090
|
+
kind = promptKindOf(parseTomlFile(join(promptsDir, file))?.prompt);
|
|
3091
|
+
}
|
|
3092
|
+
catch {
|
|
3093
|
+
beforeFunctions.push(file);
|
|
3094
|
+
continue;
|
|
3095
|
+
}
|
|
3096
|
+
(kind === "agent" ? afterFunctions : beforeFunctions).push(file);
|
|
3097
|
+
}
|
|
3098
|
+
return { beforeFunctions, afterFunctions };
|
|
3099
|
+
}
|
|
3100
|
+
/**
|
|
3101
|
+
* Every `prompts/<key>.tests/*.toml` sidecar whose prompt is an agent — #3798
|
|
3102
|
+
* (§D1: prompt test cases are refused for the kind in v1). The admin test-case
|
|
3103
|
+
* routes refuse the same case with `AGENT_TEST_CASE_NOT_SUPPORTED`; this says
|
|
3104
|
+
* so before anything is applied.
|
|
3105
|
+
*/
|
|
3106
|
+
export function promptAgentSidecarErrors(configDir) {
|
|
3107
|
+
const promptsDir = join(configDir, "prompts");
|
|
3108
|
+
const errors = [];
|
|
3109
|
+
for (const filePath of testCaseTomlFiles(configDir)) {
|
|
3110
|
+
const parent = basename(join(filePath, ".."));
|
|
3111
|
+
if (!filePath.startsWith(promptsDir) || !parent.endsWith(".tests"))
|
|
3112
|
+
continue;
|
|
3113
|
+
const key = parent.slice(0, -".tests".length);
|
|
3114
|
+
const owner = join(promptsDir, `${key}.toml`);
|
|
3115
|
+
if (!existsSync(owner))
|
|
3116
|
+
continue;
|
|
3117
|
+
let tomlData;
|
|
3118
|
+
try {
|
|
3119
|
+
tomlData = parseTomlFile(owner);
|
|
3120
|
+
}
|
|
3121
|
+
catch {
|
|
3122
|
+
continue;
|
|
3123
|
+
}
|
|
3124
|
+
if (promptKindOf(tomlData?.prompt) !== "agent")
|
|
3125
|
+
continue;
|
|
3126
|
+
const promptKey = tomlData?.prompt?.key || key;
|
|
3127
|
+
errors.push(`${filePath.slice(configDir.length + 1)}: ` +
|
|
3128
|
+
agentTestCaseRefusal(promptKey, "subject").message);
|
|
3129
|
+
}
|
|
3130
|
+
return errors;
|
|
3131
|
+
}
|
|
2756
3132
|
/**
|
|
2757
3133
|
* The immutable-`kind` refusal for a prompt update the comparison gate did not
|
|
2758
3134
|
* decide — #3626, CR3626-001.
|
|
@@ -10772,8 +11148,14 @@ What a push guarantees (two stages):
|
|
|
10772
11148
|
}
|
|
10773
11149
|
// Process prompts
|
|
10774
11150
|
const promptsDir = join(configDir, "prompts");
|
|
10775
|
-
|
|
10776
|
-
|
|
11151
|
+
// #3798 — the prompt leg as one function, called twice. `config push`
|
|
11152
|
+
// applies prompts BEFORE server functions, and an agent's tools name
|
|
11153
|
+
// functions the server must hold when the agent is created, so a tree
|
|
11154
|
+
// that pushes an agent together with its tool functions would be
|
|
11155
|
+
// refused on its first push. Chat and decisions files keep their place;
|
|
11156
|
+
// agent files are applied after the server-functions leg below
|
|
11157
|
+
// (`deferredAgentPromptFiles`). Nothing inside the leg changed.
|
|
11158
|
+
const pushPromptFiles = async (files) => {
|
|
10777
11159
|
// #2869 — prompts join the live-state reconcile (#2731 B3). #2748
|
|
10778
11160
|
// gave the type diff's comparator and left push on the manifest byte
|
|
10779
11161
|
// hash, so the two commands answered "changed?" from different
|
|
@@ -10976,6 +11358,79 @@ What a push guarantees (two stages):
|
|
|
10976
11358
|
// (mirrors `applyWorkflowUpdate`). The adopt branch seeds the
|
|
10977
11359
|
// sync-state entity record before calling this so the stamp lands.
|
|
10978
11360
|
const applyPromptUpdate = async (promptId, expectedModifiedAt) => {
|
|
11361
|
+
// #3799 (CSO-3799-005) — an agent's declaration is checked against
|
|
11362
|
+
// every config the server holds non-archived, so a file that
|
|
11363
|
+
// retires a config AND declares a schema that config's provider
|
|
11364
|
+
// cannot carry must retire it first: sent the other way round,
|
|
11365
|
+
// the prompt PATCH is refused for a config the file has already
|
|
11366
|
+
// archived, and no re-run changes the order. Only the entries
|
|
11367
|
+
// the file archives and the server does not yet — every other
|
|
11368
|
+
// config update still follows the prompt, and decisions prompts
|
|
11369
|
+
// keep their sequence. A config PATCH does not move the
|
|
11370
|
+
// prompt's `modifiedAt`, so the concurrency guard below still
|
|
11371
|
+
// holds.
|
|
11372
|
+
//
|
|
11373
|
+
// #3801 (DSO-3801-001) — the same staging, with a second admission
|
|
11374
|
+
// rule. The admin API refuses clearing a CHAT prompt's
|
|
11375
|
+
// `[prompt].outputSchema` while a config it holds non-archived is
|
|
11376
|
+
// strict, so a file that clears the schema AND turns strict output
|
|
11377
|
+
// off (or retires the config) would be refused on every push if the
|
|
11378
|
+
// prompt went first. Those config updates go ahead of the prompt
|
|
11379
|
+
// PATCH, with the payload they would have been sent anyway. The
|
|
11380
|
+
// read this needs is the one the name→id mapping below makes,
|
|
11381
|
+
// moved ahead of the PATCH, so the request count does not grow.
|
|
11382
|
+
const stagedFirst = new Set();
|
|
11383
|
+
const fileKind = promptKindOf(tomlData?.prompt);
|
|
11384
|
+
const retiring = configs.filter((c) => c?.name &&
|
|
11385
|
+
String(c?.status ?? "").trim().toLowerCase() === "archived");
|
|
11386
|
+
const stagesRetiringAgent = fileKind === "agent" && retiring.length > 0;
|
|
11387
|
+
const clearsPromptSchema = fileKind === "chat" &&
|
|
11388
|
+
!declaresSchema(tomlData?.prompt?.outputSchema);
|
|
11389
|
+
const current = stagesRetiringAgent || clearsPromptSchema
|
|
11390
|
+
? await client.getPrompt(resolvedAppId, promptId)
|
|
11391
|
+
: null;
|
|
11392
|
+
const liveServerConfig = (name) => {
|
|
11393
|
+
const found = (current?.configs ?? []).find((c) => c.configName === name);
|
|
11394
|
+
return found && String(found.status ?? "active") !== "archived"
|
|
11395
|
+
? found
|
|
11396
|
+
: null;
|
|
11397
|
+
};
|
|
11398
|
+
const staged = [];
|
|
11399
|
+
if (stagesRetiringAgent) {
|
|
11400
|
+
for (const tomlConfig of retiring) {
|
|
11401
|
+
const serverConfig = liveServerConfig(tomlConfig.name);
|
|
11402
|
+
if (serverConfig)
|
|
11403
|
+
staged.push({ tomlConfig, serverConfig });
|
|
11404
|
+
}
|
|
11405
|
+
}
|
|
11406
|
+
if (clearsPromptSchema && declaresSchema(current?.outputSchema)) {
|
|
11407
|
+
for (const tomlConfig of configs) {
|
|
11408
|
+
if (!tomlConfig?.name)
|
|
11409
|
+
continue;
|
|
11410
|
+
const serverConfig = liveServerConfig(tomlConfig.name);
|
|
11411
|
+
if (!serverConfig || serverConfig.strictOutput !== true)
|
|
11412
|
+
continue;
|
|
11413
|
+
const chat = hoistDeprecatedChatKeys(tomlConfig).entry.chat ?? {};
|
|
11414
|
+
const retired = String(tomlConfig?.status ?? "").trim().toLowerCase() ===
|
|
11415
|
+
"archived";
|
|
11416
|
+
// An entry that stays strict AND live is refused in preflight
|
|
11417
|
+
// (no schema to send); it is never staged.
|
|
11418
|
+
if (chat.strictOutput === true && !retired)
|
|
11419
|
+
continue;
|
|
11420
|
+
staged.push({ tomlConfig, serverConfig });
|
|
11421
|
+
}
|
|
11422
|
+
}
|
|
11423
|
+
for (const { tomlConfig, serverConfig } of staged) {
|
|
11424
|
+
if (stagedFirst.has(tomlConfig.name))
|
|
11425
|
+
continue;
|
|
11426
|
+
try {
|
|
11427
|
+
await client.updatePromptConfig(resolvedAppId, promptId, serverConfig.configId, buildPromptConfigPayload(tomlConfig, { mode: "update" }));
|
|
11428
|
+
}
|
|
11429
|
+
catch (err) {
|
|
11430
|
+
throw wrapPromptConfigError(err, "update", key, tomlConfig.name);
|
|
11431
|
+
}
|
|
11432
|
+
stagedFirst.add(tomlConfig.name);
|
|
11433
|
+
}
|
|
10979
11434
|
const updated = await client.updatePrompt(resolvedAppId, promptId, buildPromptPushPayload(key, tomlData, { mode: "update" }), expectedModifiedAt);
|
|
10980
11435
|
info(` Updated prompt: ${key}`);
|
|
10981
11436
|
// Record the `modifiedAt` this PATCH just caused, so a later
|
|
@@ -10998,7 +11453,10 @@ What a push guarantees (two stages):
|
|
|
10998
11453
|
try {
|
|
10999
11454
|
// Fetch full prompt to get config name→ID mappings
|
|
11000
11455
|
// (updatePrompt response doesn't include configs)
|
|
11001
|
-
|
|
11456
|
+
// #3801 — the read made ahead of the prompt PATCH when the
|
|
11457
|
+
// staging needed it (neither the PATCH nor a staged config
|
|
11458
|
+
// update moves a config's id or the activation).
|
|
11459
|
+
const fullPrompt = current ?? (await client.getPrompt(resolvedAppId, promptId));
|
|
11002
11460
|
if (fullPrompt?.configs) {
|
|
11003
11461
|
for (const config of fullPrompt.configs) {
|
|
11004
11462
|
promptConfigNameToId.set(`${key}#${config.configName}`, config.configId);
|
|
@@ -11012,6 +11470,10 @@ What a push guarantees (two stages):
|
|
|
11012
11470
|
continue;
|
|
11013
11471
|
const serverConfig = fullPrompt.configs.find((c) => c.configName === configName);
|
|
11014
11472
|
if (serverConfig) {
|
|
11473
|
+
// Staged ahead of the prompt PATCH, with this same
|
|
11474
|
+
// payload (#3799 CSO-3799-005, #3801 DSO-3801-001).
|
|
11475
|
+
if (stagedFirst.has(configName))
|
|
11476
|
+
continue;
|
|
11015
11477
|
// Update existing config
|
|
11016
11478
|
try {
|
|
11017
11479
|
await client.updatePromptConfig(resolvedAppId, promptId, serverConfig.configId, buildPromptConfigPayload(tomlConfig, { mode: "update" }));
|
|
@@ -11032,18 +11494,9 @@ What a push guarantees (two stages):
|
|
|
11032
11494
|
}
|
|
11033
11495
|
if (newConfig?.configId) {
|
|
11034
11496
|
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
|
-
}
|
|
11497
|
+
// #3799 — the create body carried the entry's own
|
|
11498
|
+
// `status`, so there is no follow-up patch (#3201's is
|
|
11499
|
+
// retired).
|
|
11047
11500
|
}
|
|
11048
11501
|
}
|
|
11049
11502
|
}
|
|
@@ -11377,21 +11830,11 @@ What a push guarantees (two stages):
|
|
|
11377
11830
|
throw wrapPromptConfigError(err, "create", key, extraName);
|
|
11378
11831
|
}
|
|
11379
11832
|
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
|
-
}
|
|
11833
|
+
promptConfigNameToId.set(`${key}#${extraName}`, extraCreated.configId);
|
|
11834
|
+
// #3799 — as on the update path: the create carried the
|
|
11835
|
+
// entry's `status`, so a retired entry is retired when
|
|
11836
|
+
// it is created and the fresh environment never runs a
|
|
11837
|
+
// config the file retired.
|
|
11395
11838
|
}
|
|
11396
11839
|
}
|
|
11397
11840
|
info(` Created ${configs.length - 1} additional config(s) for prompt: ${key}`);
|
|
@@ -11423,25 +11866,17 @@ What a push guarantees (two stages):
|
|
|
11423
11866
|
activated = true;
|
|
11424
11867
|
}
|
|
11425
11868
|
}
|
|
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
|
-
}
|
|
11869
|
+
// #3799 (DSO-3799-005) — the seeded config's `status` is in
|
|
11870
|
+
// the create body (`POST /prompts` honors it now), so #3201's
|
|
11871
|
+
// trailing patch is gone. Activation still runs after every
|
|
11872
|
+
// create (#2645), so the marked entry is what the prompt ends
|
|
11873
|
+
// up serving; when the FIRST entry is the retired one, the
|
|
11874
|
+
// seeded config the prompt points at is created archived and
|
|
11875
|
+
// the activation moves the pointer off it. A push that dies
|
|
11876
|
+
// between the two leaves a prompt pointing at an archived
|
|
11877
|
+
// config — recovered by re-running the push, and the price of
|
|
11878
|
+
// never creating a config in a state the file did not ask for
|
|
11879
|
+
// (which is what an agent's schema check fires on).
|
|
11445
11880
|
// Stamp sync state ONLY after all create work (base prompt +
|
|
11446
11881
|
// every extra config) succeeded. If an extra-config create
|
|
11447
11882
|
// above threw, we never reach here, so the file hash is not
|
|
@@ -11477,6 +11912,13 @@ What a push guarantees (two stages):
|
|
|
11477
11912
|
}
|
|
11478
11913
|
}
|
|
11479
11914
|
}
|
|
11915
|
+
};
|
|
11916
|
+
let deferredAgentPromptFiles = [];
|
|
11917
|
+
if (existsSync(promptsDir)) {
|
|
11918
|
+
const files = selectFiles("prompt", promptsDir, readdirSync(promptsDir).filter((f) => f.endsWith(".toml")));
|
|
11919
|
+
const ordered = partitionPromptFilesForPush(promptsDir, files);
|
|
11920
|
+
deferredAgentPromptFiles = ordered.afterFunctions;
|
|
11921
|
+
await pushPromptFiles(ordered.beforeFunctions);
|
|
11480
11922
|
}
|
|
11481
11923
|
// Process transforms (Rhai scripts) — issue #892, slice 7 of #783.
|
|
11482
11924
|
//
|
|
@@ -13949,6 +14391,11 @@ What a push guarantees (two stages):
|
|
|
13949
14391
|
}
|
|
13950
14392
|
}
|
|
13951
14393
|
}
|
|
14394
|
+
// #3798 — the agent prompts, now that every function they name that
|
|
14395
|
+
// this push applies is on the server (see `pushPromptFiles`).
|
|
14396
|
+
if (deferredAgentPromptFiles.length > 0) {
|
|
14397
|
+
await pushPromptFiles(deferredAgentPromptFiles);
|
|
14398
|
+
}
|
|
13952
14399
|
// Process group type configs
|
|
13953
14400
|
const groupTypeConfigsDir = join(configDir, "group-type-configs");
|
|
13954
14401
|
if (existsSync(groupTypeConfigsDir)) {
|