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
|
@@ -372,13 +372,26 @@ export function handlerReferences(table) {
|
|
|
372
372
|
// functions are the whole mechanism: everything else — the pull projection, the
|
|
373
373
|
// push payload builder, the CLI's unknown-key and declared-type checks — reads
|
|
374
374
|
// the declaration through them.
|
|
375
|
+
/**
|
|
376
|
+
* The blocks a field is authored under, in declaration order: `[]` for a root
|
|
377
|
+
* key, one name for most grouped keys, several for a key two blocks share
|
|
378
|
+
* (#3798). Every reader of `tomlGroup` goes through this.
|
|
379
|
+
*/
|
|
380
|
+
export function tomlGroupsOf(field) {
|
|
381
|
+
const group = field.tomlGroup;
|
|
382
|
+
if (group === undefined)
|
|
383
|
+
return [];
|
|
384
|
+
return typeof group === "string" ? [group] : group;
|
|
385
|
+
}
|
|
375
386
|
/** The group names this table declares, sorted. Empty for an ungrouped table. */
|
|
376
387
|
export function tomlGroupNames(table) {
|
|
377
388
|
return Object.keys(table.tomlGroups ?? {}).sort();
|
|
378
389
|
}
|
|
379
390
|
/** The TOML keys accepted INSIDE `group`: every field that declares it. */
|
|
380
391
|
export function acceptedGroupTomlKeys(table, group) {
|
|
381
|
-
return new Set(table.fields
|
|
392
|
+
return new Set(table.fields
|
|
393
|
+
.filter((f) => tomlGroupsOf(f).includes(group))
|
|
394
|
+
.map((f) => f.tomlKey));
|
|
382
395
|
}
|
|
383
396
|
/**
|
|
384
397
|
* TOML keys this table accepts AT ITS ROOT: the UNGROUPED fields, the declared
|
|
@@ -437,13 +450,13 @@ export function findUnknownGroupedTomlKeys(tomlTable, table) {
|
|
|
437
450
|
*/
|
|
438
451
|
export function findBadTomlGroupDeclarations(table) {
|
|
439
452
|
const declared = new Set(tomlGroupNames(table));
|
|
440
|
-
const used = new Set(table.fields
|
|
441
|
-
.map((f) => f.tomlGroup)
|
|
442
|
-
.filter((g) => typeof g === "string"));
|
|
453
|
+
const used = new Set(table.fields.flatMap((f) => [...tomlGroupsOf(f)]));
|
|
443
454
|
const bad = [];
|
|
444
455
|
for (const field of table.fields) {
|
|
445
|
-
|
|
446
|
-
|
|
456
|
+
for (const group of tomlGroupsOf(field)) {
|
|
457
|
+
if (!declared.has(group)) {
|
|
458
|
+
bad.push(`${field.field}: tomlGroup "${group}" is not declared in tomlGroups`);
|
|
459
|
+
}
|
|
447
460
|
}
|
|
448
461
|
}
|
|
449
462
|
for (const [group, entry] of Object.entries(table.tomlGroups ?? {})) {
|
|
@@ -2146,8 +2159,9 @@ export function requestSchemaKeys(table, mode) {
|
|
|
2146
2159
|
*/
|
|
2147
2160
|
export function configRequestSchema(table, mode) {
|
|
2148
2161
|
const properties = {};
|
|
2162
|
+
const deprecatedFields = new Set(table.fields.filter((f) => f.deprecated).map((f) => f.field));
|
|
2149
2163
|
for (const key of requestSchemaKeys(table, mode)) {
|
|
2150
|
-
properties[key] = {};
|
|
2164
|
+
properties[key] = deprecatedFields.has(key) ? { deprecated: true } : {};
|
|
2151
2165
|
}
|
|
2152
2166
|
const schema = {
|
|
2153
2167
|
type: "object",
|
|
@@ -2210,8 +2224,9 @@ export function retiredRequestKeys(schema) {
|
|
|
2210
2224
|
export function mergeRequestSchemas(...schemas) {
|
|
2211
2225
|
const properties = {};
|
|
2212
2226
|
for (const schema of schemas) {
|
|
2213
|
-
for (const key of Object.
|
|
2214
|
-
properties[key] = {};
|
|
2227
|
+
for (const [key, property] of Object.entries(schema.properties)) {
|
|
2228
|
+
properties[key] = { ...(properties[key] ?? {}), ...property };
|
|
2229
|
+
}
|
|
2215
2230
|
}
|
|
2216
2231
|
// The union's guidance is the union of its halves'. A key one table retired
|
|
2217
2232
|
// and another accepts is admitted by the merged key set, so `rememberRetiredKeys`
|
|
@@ -4859,7 +4874,7 @@ function geminiResolution(model, effort, budget) {
|
|
|
4859
4874
|
/**
|
|
4860
4875
|
* What a prompt's KIND admits — issue #3626.
|
|
4861
4876
|
*
|
|
4862
|
-
* A prompt declares `[prompt] kind = "chat" | "decisions"`. The kind decides
|
|
4877
|
+
* A prompt declares `[prompt] kind = "chat" | "decisions" | "agent"`. The kind decides
|
|
4863
4878
|
* the input envelope (`variables` rendered into templates for chat, a `state`
|
|
4864
4879
|
* value for decisions), the provider endpoint the executor posts to, and which
|
|
4865
4880
|
* `[configs.<kind>]` block a `[[configs]]` entry may carry. Adding a kind is a
|
|
@@ -4876,13 +4891,22 @@ function geminiResolution(model, effort, budget) {
|
|
|
4876
4891
|
*
|
|
4877
4892
|
* Pure and dependency-free, like every module under `src/config-surface/`.
|
|
4878
4893
|
*/
|
|
4879
|
-
/** The kinds a prompt may declare. An omitted `kind` is `chat`. */
|
|
4880
|
-
export const PROMPT_KINDS = ["chat", "decisions"];
|
|
4881
4894
|
/**
|
|
4882
|
-
* The
|
|
4883
|
-
*
|
|
4895
|
+
* The kinds a prompt may declare. An omitted `kind` is `chat`.
|
|
4896
|
+
*
|
|
4897
|
+
* #3798 — `agent` is a prompt run turn by turn in a session: no template, no
|
|
4898
|
+
* output schema, never run single-shot. Its tools and events are declared in
|
|
4899
|
+
* `[prompt.agent]` (`agent-declaration.ts`); its configs carry
|
|
4900
|
+
* `[configs.agent]`.
|
|
4884
4901
|
*/
|
|
4885
|
-
export const
|
|
4902
|
+
export const PROMPT_KINDS = ["chat", "decisions", "agent"];
|
|
4903
|
+
/**
|
|
4904
|
+
* The nine chat settings that existed before `[configs.chat]` did. Only these
|
|
4905
|
+
* keep the deprecated FLAT spelling (#3642): a key added to the block later was
|
|
4906
|
+
* never written flat by any file, so it has none (the `questions` posture,
|
|
4907
|
+
* SO3626-013).
|
|
4908
|
+
*/
|
|
4909
|
+
export const LEGACY_FLAT_CHAT_KEYS = [
|
|
4886
4910
|
"systemPrompt",
|
|
4887
4911
|
"userPromptTemplate",
|
|
4888
4912
|
"temperature",
|
|
@@ -4893,8 +4917,35 @@ export const CHAT_CONFIG_KEYS = [
|
|
|
4893
4917
|
"reasoningEffort",
|
|
4894
4918
|
"reasoningBudget",
|
|
4895
4919
|
];
|
|
4920
|
+
/**
|
|
4921
|
+
* The settings that describe a CHAT COMPLETION and nothing else. They are
|
|
4922
|
+
* authored under `[configs.chat]`; the flat spelling of the nine legacy keys
|
|
4923
|
+
* is deprecated (#3642).
|
|
4924
|
+
*
|
|
4925
|
+
* #3801 — `strictOutput`, the opt-in that sends `[prompt].outputSchema` as a
|
|
4926
|
+
* strict `json_schema` on OpenRouter (`prompt-strict-output.ts`). Not an agent
|
|
4927
|
+
* key (§D1: an agent has no output schema), so the agent arm refuses it.
|
|
4928
|
+
*/
|
|
4929
|
+
export const CHAT_CONFIG_KEYS = [
|
|
4930
|
+
...LEGACY_FLAT_CHAT_KEYS,
|
|
4931
|
+
"strictOutput",
|
|
4932
|
+
];
|
|
4896
4933
|
/** The settings that describe a DECISIONS request. */
|
|
4897
4934
|
export const DECISIONS_CONFIG_KEYS = ["questions"];
|
|
4935
|
+
/**
|
|
4936
|
+
* #3798 — the settings of an AGENT's model round: the chat block less
|
|
4937
|
+
* `userPromptTemplate` (the model sees each message as sent), `outputFormat`
|
|
4938
|
+
* and `outputSchema` (an agent's final answer is text; anything structured is
|
|
4939
|
+
* a tool call). The same wire keys as chat, authored under `[configs.agent]`.
|
|
4940
|
+
*/
|
|
4941
|
+
export const AGENT_CONFIG_KEYS = [
|
|
4942
|
+
"systemPrompt",
|
|
4943
|
+
"temperature",
|
|
4944
|
+
"topP",
|
|
4945
|
+
"maxTokens",
|
|
4946
|
+
"reasoningEffort",
|
|
4947
|
+
"reasoningBudget",
|
|
4948
|
+
];
|
|
4898
4949
|
/**
|
|
4899
4950
|
* The WIRE field names each kind's block holds. The config surface declares the
|
|
4900
4951
|
* same grouping through `ConfigField.tomlGroup`, and the drift guard asserts
|
|
@@ -4905,11 +4956,26 @@ export const DECISIONS_CONFIG_KEYS = ["questions"];
|
|
|
4905
4956
|
export const PROMPT_KIND_BLOCKS = {
|
|
4906
4957
|
chat: CHAT_CONFIG_KEYS,
|
|
4907
4958
|
decisions: DECISIONS_CONFIG_KEYS,
|
|
4959
|
+
agent: AGENT_CONFIG_KEYS,
|
|
4908
4960
|
};
|
|
4909
4961
|
/** The question types TypeSafe's System One models answer. */
|
|
4910
4962
|
export const DECISIONS_QUESTION_TYPES = ["choice", "score", "noul"];
|
|
4911
4963
|
/** A question name the endpoint accepts, and that reads as a JSON key. */
|
|
4912
4964
|
const QUESTION_NAME = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
4965
|
+
/**
|
|
4966
|
+
* #3813 — where a `choice` question's options come from. `"static"`: the
|
|
4967
|
+
* config's `criteria` table. `"dynamic"`: each run supplies the table as
|
|
4968
|
+
* `variables.criteria.<question>`. Required on every `choice` question, with
|
|
4969
|
+
* no default, and a platform key: it is removed before the provider request.
|
|
4970
|
+
*/
|
|
4971
|
+
export const DECISIONS_CRITERIA_SOURCES = ["static", "dynamic"];
|
|
4972
|
+
/**
|
|
4973
|
+
* #3813 — the code a decisions run carries when the options it was asked to
|
|
4974
|
+
* choose between are refused before the provider call: malformed per-run
|
|
4975
|
+
* criteria, criteria for a question that does not take them, or a stored
|
|
4976
|
+
* `choice` question that does not say where its options come from.
|
|
4977
|
+
*/
|
|
4978
|
+
export const PROMPT_CRITERIA_INVALID = "PROMPT_CRITERIA_INVALID";
|
|
4913
4979
|
/**
|
|
4914
4980
|
* The prompt's kind, normalized.
|
|
4915
4981
|
*
|
|
@@ -4961,20 +5027,96 @@ function parseQuestions(value) {
|
|
|
4961
5027
|
function chatKeysSet(config) {
|
|
4962
5028
|
return CHAT_CONFIG_KEYS.filter((key) => !isUnset(config[key]));
|
|
4963
5029
|
}
|
|
5030
|
+
/**
|
|
5031
|
+
* The block a stray key is authored under: `questions` is decisions', every
|
|
5032
|
+
* other key chat's (the agent block's keys are chat keys too, so a key an
|
|
5033
|
+
* agent refuses is one only the chat block holds).
|
|
5034
|
+
*/
|
|
5035
|
+
function owningBlock(key) {
|
|
5036
|
+
return DECISIONS_CONFIG_KEYS.includes(key)
|
|
5037
|
+
? "decisions"
|
|
5038
|
+
: "chat";
|
|
5039
|
+
}
|
|
4964
5040
|
/**
|
|
4965
5041
|
* One rule replaces the nine per-key refusals: a `[[configs]]` entry may carry
|
|
4966
5042
|
* only the block named by its prompt's kind.
|
|
5043
|
+
*
|
|
5044
|
+
* #3798 — the offending keys are named with the block each belongs to. Keys
|
|
5045
|
+
* from ONE block read exactly as they did before a third kind existed.
|
|
4967
5046
|
*/
|
|
4968
5047
|
function wrongBlockError(kind, offending) {
|
|
4969
|
-
const
|
|
4970
|
-
|
|
4971
|
-
`[configs.${kind}] block; this one sets
|
|
4972
|
-
|
|
4973
|
-
|
|
5048
|
+
const blocks = [...new Set(offending.map(owningBlock))];
|
|
5049
|
+
const head = `prompt kind is "${kind}", so a [[configs]] entry may carry only a ` +
|
|
5050
|
+
`[configs.${kind}] block; this one sets `;
|
|
5051
|
+
if (blocks.length === 1) {
|
|
5052
|
+
const wrongKind = blocks[0];
|
|
5053
|
+
return (head +
|
|
5054
|
+
`${offending.join(", ")} ([configs.${wrongKind}] in TOML). Move them ` +
|
|
5055
|
+
`to a ${wrongKind} prompt or remove them.`);
|
|
5056
|
+
}
|
|
5057
|
+
const named = blocks
|
|
5058
|
+
.map((block) => `${offending.filter((key) => owningBlock(key) === block).join(", ")} ` +
|
|
5059
|
+
`([configs.${block}] in TOML)`)
|
|
5060
|
+
.join("; ");
|
|
5061
|
+
return head + `${named}. Move them to a prompt of that kind or remove them.`;
|
|
4974
5062
|
}
|
|
4975
5063
|
function questionError(name, detail) {
|
|
4976
5064
|
return `questions.${name} ${detail}`;
|
|
4977
5065
|
}
|
|
5066
|
+
/**
|
|
5067
|
+
* #3813 — the one rule for a `choice` question's options table, whoever
|
|
5068
|
+
* supplies it: the config (`criteriaSource = "static"`) or the run
|
|
5069
|
+
* (`"dynamic"`). `null` when the table is usable, otherwise the detail the
|
|
5070
|
+
* caller prefixes with where the table came from.
|
|
5071
|
+
*
|
|
5072
|
+
* At least two options; each key non-empty with no leading or trailing
|
|
5073
|
+
* whitespace (the endpoint answers `choice` as the key, so a padded key is an
|
|
5074
|
+
* answer no caller would match); each description a non-empty string or a
|
|
5075
|
+
* non-empty JSON object, which is how a run describes a structured option such
|
|
5076
|
+
* as a previous transaction.
|
|
5077
|
+
*/
|
|
5078
|
+
function criteriaTableError(table) {
|
|
5079
|
+
if (!isPlainObject(table)) {
|
|
5080
|
+
return "must be a table of option key = description";
|
|
5081
|
+
}
|
|
5082
|
+
const entries = Object.entries(table);
|
|
5083
|
+
if (entries.length < 2) {
|
|
5084
|
+
return ("needs at least two options; a choice between one option is not a " +
|
|
5085
|
+
"choice");
|
|
5086
|
+
}
|
|
5087
|
+
for (const [key, description] of entries) {
|
|
5088
|
+
if (key.trim() === "" || key !== key.trim()) {
|
|
5089
|
+
return (`has the option key ${JSON.stringify(key)}, which is empty or starts ` +
|
|
5090
|
+
"or ends with whitespace");
|
|
5091
|
+
}
|
|
5092
|
+
const usable = isPlainObject(description)
|
|
5093
|
+
? Object.keys(description).length > 0
|
|
5094
|
+
: isNonEmptyString(description);
|
|
5095
|
+
if (!usable) {
|
|
5096
|
+
return (`option '${key}' needs a non-empty description (a string or a JSON ` +
|
|
5097
|
+
"object)");
|
|
5098
|
+
}
|
|
5099
|
+
}
|
|
5100
|
+
return null;
|
|
5101
|
+
}
|
|
5102
|
+
/** `criteriaSource` as the question declares it, or `undefined` when unset. */
|
|
5103
|
+
function criteriaSourceOf(question) {
|
|
5104
|
+
return question.criteriaSource;
|
|
5105
|
+
}
|
|
5106
|
+
function isCriteriaSource(value) {
|
|
5107
|
+
return (typeof value === "string" &&
|
|
5108
|
+
DECISIONS_CRITERIA_SOURCES.includes(value));
|
|
5109
|
+
}
|
|
5110
|
+
/** The refusal for a `choice` question whose options' source is unusable. */
|
|
5111
|
+
function criteriaSourceError(name, source) {
|
|
5112
|
+
if (source === undefined) {
|
|
5113
|
+
return questionError(name, 'is a choice question, so it needs criteriaSource = "static" (its ' +
|
|
5114
|
+
'options in a criteria table here) or "dynamic" (each run supplies ' +
|
|
5115
|
+
`them as variables.criteria.${name})`);
|
|
5116
|
+
}
|
|
5117
|
+
return questionError(name, `criteriaSource must be "static" or "dynamic"; found ` +
|
|
5118
|
+
JSON.stringify(source));
|
|
5119
|
+
}
|
|
4978
5120
|
/** Every refusal a single decisions question can earn, or `null`. */
|
|
4979
5121
|
function decisionsQuestionError(name, question) {
|
|
4980
5122
|
if (!QUESTION_NAME.test(name)) {
|
|
@@ -4992,20 +5134,32 @@ function decisionsQuestionError(name, question) {
|
|
|
4992
5134
|
if (!isNonEmptyString(question.instructions)) {
|
|
4993
5135
|
return questionError(name, "needs instructions — the sentence the model answers");
|
|
4994
5136
|
}
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5137
|
+
const source = criteriaSourceOf(question);
|
|
5138
|
+
if (type !== "choice") {
|
|
5139
|
+
// #3813 — only a choice question has options to source.
|
|
5140
|
+
if (source !== undefined) {
|
|
5141
|
+
return questionError(name, `is a ${type} question; criteriaSource applies only to a choice ` +
|
|
5142
|
+
"question");
|
|
5000
5143
|
}
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5144
|
+
}
|
|
5145
|
+
else {
|
|
5146
|
+
if (!isCriteriaSource(source))
|
|
5147
|
+
return criteriaSourceError(name, source);
|
|
5148
|
+
const criteria = question.criteria;
|
|
5149
|
+
if (source === "dynamic") {
|
|
5150
|
+
if (criteria !== undefined && criteria !== null) {
|
|
5151
|
+
return questionError(name, 'has criteriaSource = "dynamic", so its options come from each ' +
|
|
5152
|
+
`run as variables.criteria.${name}; remove its criteria table`);
|
|
5153
|
+
}
|
|
5005
5154
|
}
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5155
|
+
else {
|
|
5156
|
+
if (!isPlainObject(criteria)) {
|
|
5157
|
+
return questionError(name, 'has criteriaSource = "static", so it needs a criteria table of ' +
|
|
5158
|
+
"option key = description");
|
|
5159
|
+
}
|
|
5160
|
+
const tableError = criteriaTableError(criteria);
|
|
5161
|
+
if (tableError)
|
|
5162
|
+
return questionError(name, `criteria ${tableError}`);
|
|
5009
5163
|
}
|
|
5010
5164
|
}
|
|
5011
5165
|
// Every other key on a question — `score`'s array `criteria`, anything
|
|
@@ -5035,6 +5189,23 @@ export function resolvePromptKind(input) {
|
|
|
5035
5189
|
kind = text;
|
|
5036
5190
|
}
|
|
5037
5191
|
const config = input.config || {};
|
|
5192
|
+
// ── agent (#3798) ──────────────────────────────────────────────────────
|
|
5193
|
+
// No template: the model sees each message as sent. Either provider; the
|
|
5194
|
+
// capability checks are the provider mapping's (#3799), not the kind's.
|
|
5195
|
+
if (kind === "agent") {
|
|
5196
|
+
const stray = [
|
|
5197
|
+
...CHAT_CONFIG_KEYS.filter((key) => !AGENT_CONFIG_KEYS.includes(key)),
|
|
5198
|
+
...DECISIONS_CONFIG_KEYS,
|
|
5199
|
+
].filter((key) => !isUnset(config[key]));
|
|
5200
|
+
if (stray.length > 0) {
|
|
5201
|
+
return {
|
|
5202
|
+
ok: false,
|
|
5203
|
+
error: wrongBlockError("agent", stray),
|
|
5204
|
+
code: "AGENT_CONFIG_KEY_NOT_ALLOWED",
|
|
5205
|
+
};
|
|
5206
|
+
}
|
|
5207
|
+
return { ok: true, kind: "agent" };
|
|
5208
|
+
}
|
|
5038
5209
|
if (kind === "chat") {
|
|
5039
5210
|
if (!isUnset(config.questions)) {
|
|
5040
5211
|
return { ok: false, error: wrongBlockError("chat", ["questions"]) };
|
|
@@ -5072,6 +5243,126 @@ export function resolvePromptKind(input) {
|
|
|
5072
5243
|
}
|
|
5073
5244
|
return { ok: true, kind: "decisions", questions };
|
|
5074
5245
|
}
|
|
5246
|
+
/** A run-time refusal, naming the question the way the executor's errors do. */
|
|
5247
|
+
function runQuestionError(name, detail) {
|
|
5248
|
+
return `Decisions question '${name}' ${detail}`;
|
|
5249
|
+
}
|
|
5250
|
+
/**
|
|
5251
|
+
* #3813 — the questions one run sends to the provider.
|
|
5252
|
+
*
|
|
5253
|
+
* `questions` is the RAN config's declaration, as stored; `runCriteria` is
|
|
5254
|
+
* the run's `variables.criteria` (`undefined` when the run sent none). A
|
|
5255
|
+
* `"dynamic"` question is given the run's table as its `criteria`; every
|
|
5256
|
+
* question loses `criteriaSource`, which is the platform's key and not the
|
|
5257
|
+
* endpoint's. The config still owns each question's name, type and
|
|
5258
|
+
* instructions: a run can supply options only for a question declared
|
|
5259
|
+
* `"dynamic"`, so nothing a run sends can change what is being asked.
|
|
5260
|
+
*
|
|
5261
|
+
* Refuses, before any provider call: a `variables.criteria` that is not an
|
|
5262
|
+
* object; criteria for a name the config does not declare, for a `"static"`
|
|
5263
|
+
* question, or for a `score` or `noul` one; a `"dynamic"` question with no
|
|
5264
|
+
* table or a table the criteria rule refuses; and a stored `choice` question
|
|
5265
|
+
* with no usable `criteriaSource` — a config written before the key existed,
|
|
5266
|
+
* which is refused rather than given a silent default.
|
|
5267
|
+
*
|
|
5268
|
+
* Pure: the stored questions object is never mutated.
|
|
5269
|
+
*/
|
|
5270
|
+
export function resolveDecisionsRunQuestions(questions, runCriteria) {
|
|
5271
|
+
if (runCriteria !== undefined && !isPlainObject(runCriteria)) {
|
|
5272
|
+
return {
|
|
5273
|
+
ok: false,
|
|
5274
|
+
error: "variables.criteria must be an object keyed by question name, each " +
|
|
5275
|
+
"value a table of option key = description; found " +
|
|
5276
|
+
(Array.isArray(runCriteria)
|
|
5277
|
+
? "an array"
|
|
5278
|
+
: runCriteria === null
|
|
5279
|
+
? "null"
|
|
5280
|
+
: `a ${typeof runCriteria}`),
|
|
5281
|
+
};
|
|
5282
|
+
}
|
|
5283
|
+
const supplied = isPlainObject(runCriteria)
|
|
5284
|
+
? runCriteria
|
|
5285
|
+
: {};
|
|
5286
|
+
for (const name of Object.keys(supplied)) {
|
|
5287
|
+
// Own keys only: a question name such as `__proto__` passes QUESTION_NAME,
|
|
5288
|
+
// and an inherited property is not a declared question.
|
|
5289
|
+
const question = Object.prototype.hasOwnProperty.call(questions, name)
|
|
5290
|
+
? questions[name]
|
|
5291
|
+
: undefined;
|
|
5292
|
+
if (!isPlainObject(question)) {
|
|
5293
|
+
return {
|
|
5294
|
+
ok: false,
|
|
5295
|
+
error: runQuestionError(name, `is not a question this config declares, so variables.criteria.` +
|
|
5296
|
+
`${name} cannot be supplied (the config does not declare it)`),
|
|
5297
|
+
};
|
|
5298
|
+
}
|
|
5299
|
+
if (question.type !== "choice") {
|
|
5300
|
+
return {
|
|
5301
|
+
ok: false,
|
|
5302
|
+
error: runQuestionError(name, `is a ${String(question.type)} question; only a choice question ` +
|
|
5303
|
+
'declared with criteriaSource = "dynamic" takes ' +
|
|
5304
|
+
`variables.criteria.${name}`),
|
|
5305
|
+
};
|
|
5306
|
+
}
|
|
5307
|
+
if (criteriaSourceOf(question) === "static") {
|
|
5308
|
+
return {
|
|
5309
|
+
ok: false,
|
|
5310
|
+
error: runQuestionError(name, 'takes its criteria from the config (criteriaSource = "static"), ' +
|
|
5311
|
+
`so variables.criteria.${name} cannot be supplied`),
|
|
5312
|
+
};
|
|
5313
|
+
}
|
|
5314
|
+
}
|
|
5315
|
+
const resolved = {};
|
|
5316
|
+
// Defined, not assigned: `resolved.__proto__ = …` would set the prototype and
|
|
5317
|
+
// the question would vanish from the provider request.
|
|
5318
|
+
const keep = (name, value) => Object.defineProperty(resolved, name, {
|
|
5319
|
+
value,
|
|
5320
|
+
enumerable: true,
|
|
5321
|
+
writable: true,
|
|
5322
|
+
configurable: true,
|
|
5323
|
+
});
|
|
5324
|
+
for (const [name, question] of Object.entries(questions)) {
|
|
5325
|
+
if (!isPlainObject(question)) {
|
|
5326
|
+
keep(name, question);
|
|
5327
|
+
continue;
|
|
5328
|
+
}
|
|
5329
|
+
const { criteriaSource: source, ...sent } = question;
|
|
5330
|
+
if (question.type === "choice") {
|
|
5331
|
+
if (!isCriteriaSource(source)) {
|
|
5332
|
+
return {
|
|
5333
|
+
ok: false,
|
|
5334
|
+
error: runQuestionError(name, source === undefined
|
|
5335
|
+
? 'is a choice question with no criteriaSource; declare ' +
|
|
5336
|
+
'criteriaSource = "static" or "dynamic" on it and push the ' +
|
|
5337
|
+
"prompt again"
|
|
5338
|
+
: `has criteriaSource ${JSON.stringify(source)}; it must be ` +
|
|
5339
|
+
'"static" or "dynamic"'),
|
|
5340
|
+
};
|
|
5341
|
+
}
|
|
5342
|
+
if (source === "dynamic") {
|
|
5343
|
+
if (!Object.prototype.hasOwnProperty.call(supplied, name)) {
|
|
5344
|
+
return {
|
|
5345
|
+
ok: false,
|
|
5346
|
+
error: runQuestionError(name, "takes its criteria from the run (criteriaSource = " +
|
|
5347
|
+
`"dynamic"), and variables.criteria.${name} is missing`),
|
|
5348
|
+
};
|
|
5349
|
+
}
|
|
5350
|
+
const table = supplied[name];
|
|
5351
|
+
const tableError = criteriaTableError(table);
|
|
5352
|
+
if (tableError) {
|
|
5353
|
+
return {
|
|
5354
|
+
ok: false,
|
|
5355
|
+
error: runQuestionError(name, `takes its criteria from the run, and variables.criteria.` +
|
|
5356
|
+
`${name} ${tableError}`),
|
|
5357
|
+
};
|
|
5358
|
+
}
|
|
5359
|
+
sent.criteria = { ...table };
|
|
5360
|
+
}
|
|
5361
|
+
}
|
|
5362
|
+
keep(name, sent);
|
|
5363
|
+
}
|
|
5364
|
+
return { ok: true, questions: resolved };
|
|
5365
|
+
}
|
|
5075
5366
|
/**
|
|
5076
5367
|
* Move the deprecated flat chat keys of one `[[configs]]` entry under `chat`
|
|
5077
5368
|
* — #3626 §4, the blob-bucket `accessPolicy` precedent made reusable.
|
|
@@ -5093,7 +5384,9 @@ export function hoistDeprecatedChatKeys(entry) {
|
|
|
5093
5384
|
const group = existingGroup ?? {};
|
|
5094
5385
|
const moved = [];
|
|
5095
5386
|
const conflicts = [];
|
|
5096
|
-
|
|
5387
|
+
// #3801 — only the legacy keys have a flat spelling to hoist; `strictOutput`
|
|
5388
|
+
// at the root is an unknown key naming its block.
|
|
5389
|
+
for (const key of LEGACY_FLAT_CHAT_KEYS) {
|
|
5097
5390
|
if (!(key in out))
|
|
5098
5391
|
continue;
|
|
5099
5392
|
if (existingGroup && key in existingGroup) {
|
|
@@ -5109,128 +5402,1557 @@ export function hoistDeprecatedChatKeys(entry) {
|
|
|
5109
5402
|
out.chat = group;
|
|
5110
5403
|
return { entry: out, moved: moved.sort(), conflicts: conflicts.sort() };
|
|
5111
5404
|
}
|
|
5112
|
-
// ── src/config-surface/prompt.ts
|
|
5405
|
+
// ── src/config-surface/prompt-strict-output.ts ───────────────────────────
|
|
5406
|
+
/**
|
|
5407
|
+
* Strict output for a chat config — issue #3801 (project `agents`, §D6).
|
|
5408
|
+
*
|
|
5409
|
+
* A chat prompt's OpenRouter request asks for JSON mode (`outputFormat =
|
|
5410
|
+
* "json"`) and the answer is checked AFTERWARDS against `[prompt].outputSchema`
|
|
5411
|
+
* (`derivePromptRunEnvelope`). A config that sets `[configs.chat].strictOutput
|
|
5412
|
+
* = true` asks the provider to constrain the answer instead: the prompt-level
|
|
5413
|
+
* schema is sent as `response_format: { type: "json_schema", json_schema: {
|
|
5414
|
+
* name: "output", strict: true, schema } }`, with `provider: {
|
|
5415
|
+
* require_parameters: true }` so OpenRouter refuses an endpoint that would
|
|
5416
|
+
* ignore it rather than dropping it in silence (the #3358 posture).
|
|
5417
|
+
*
|
|
5418
|
+
* This module is the whole rule, and it is read in both places a config is
|
|
5419
|
+
* written: the admin routes, and (vendored) the CLI's push preflight, so a
|
|
5420
|
+
* refusal at push and at the route cannot disagree — `resolvePromptReasoning`
|
|
5421
|
+
* and `resolvePromptKind`'s posture.
|
|
5422
|
+
*
|
|
5423
|
+
* Which schema is sent is never ambiguous: always the PROMPT's (§D6), so a
|
|
5424
|
+
* config that also declares its own `outputSchema` is refused. Five refusals,
|
|
5425
|
+
* in this order, each with its own code:
|
|
5426
|
+
*
|
|
5427
|
+
* - `PROMPT_STRICT_OUTPUT_INVALID` — the value is not a boolean.
|
|
5428
|
+
* - `PROMPT_STRICT_OUTPUT_SCHEMA_CONFLICT` — the config declares its own
|
|
5429
|
+
* `outputSchema` too.
|
|
5430
|
+
* - `PROMPT_STRICT_OUTPUT_PROVIDER_UNSUPPORTED` — not OpenRouter. A Gemini
|
|
5431
|
+
* config already constrains its request through `responseSchema`.
|
|
5432
|
+
* - `PROMPT_STRICT_OUTPUT_FORMAT_CONFLICT` — `outputFormat = "text"`.
|
|
5433
|
+
* - `PROMPT_STRICT_OUTPUT_SCHEMA_MISSING` — the prompt declares no usable
|
|
5434
|
+
* `[prompt].outputSchema`.
|
|
5435
|
+
*
|
|
5436
|
+
* The first four are about the config itself and apply whatever its status.
|
|
5437
|
+
* The last depends on PROMPT-level state that can change under a config, so it
|
|
5438
|
+
* is skipped for an ARCHIVED config (DSO-3801-002): a retired strict config may
|
|
5439
|
+
* be pulled, pushed and renamed after its prompt dropped the schema, and the
|
|
5440
|
+
* requirement is enforced when a config is or becomes active — the #3799
|
|
5441
|
+
* posture, "an archive is never checked".
|
|
5442
|
+
*
|
|
5443
|
+
* Pure and dependency-free, like every module under `src/config-surface/`.
|
|
5444
|
+
*/
|
|
5445
|
+
/** Every code the rule answers, in the order it checks. */
|
|
5446
|
+
export const PROMPT_STRICT_OUTPUT_CODES = [
|
|
5447
|
+
"PROMPT_STRICT_OUTPUT_INVALID",
|
|
5448
|
+
"PROMPT_STRICT_OUTPUT_SCHEMA_CONFLICT",
|
|
5449
|
+
"PROMPT_STRICT_OUTPUT_PROVIDER_UNSUPPORTED",
|
|
5450
|
+
"PROMPT_STRICT_OUTPUT_FORMAT_CONFLICT",
|
|
5451
|
+
"PROMPT_STRICT_OUTPUT_SCHEMA_MISSING",
|
|
5452
|
+
];
|
|
5113
5453
|
/**
|
|
5114
|
-
* The
|
|
5454
|
+
* The one code a RUN can also carry: the executor refuses a strict config whose
|
|
5455
|
+
* stored prompt schema is absent or unusable before any provider call, rather
|
|
5456
|
+
* than downgrading the request to JSON mode.
|
|
5457
|
+
*/
|
|
5458
|
+
export const PROMPT_STRICT_OUTPUT_SCHEMA_MISSING = "PROMPT_STRICT_OUTPUT_SCHEMA_MISSING";
|
|
5459
|
+
/**
|
|
5460
|
+
* `json_schema.name`. The OpenAI-shaped API requires one and the executor has
|
|
5461
|
+
* no prompt key to hand, so it is fixed — the same request for the same schema.
|
|
5462
|
+
*/
|
|
5463
|
+
export const STRICT_OUTPUT_SCHEMA_NAME = "output";
|
|
5464
|
+
/** `null`, `undefined` and `""` are absence; `false` is a value. */
|
|
5465
|
+
function isStrictOutputUnset(value) {
|
|
5466
|
+
if (value === null || value === undefined)
|
|
5467
|
+
return true;
|
|
5468
|
+
return typeof value === "string" && value.trim() === "";
|
|
5469
|
+
}
|
|
5470
|
+
function isSchemaObject(value) {
|
|
5471
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5472
|
+
}
|
|
5473
|
+
/** Does this config opt in? Only a literal `true` does. */
|
|
5474
|
+
export function isStrictOutput(value) {
|
|
5475
|
+
return value === true;
|
|
5476
|
+
}
|
|
5477
|
+
/**
|
|
5478
|
+
* The schema strict output sends, from a stored or authored
|
|
5479
|
+
* `[prompt].outputSchema`: a JSON object (or its text), else `null`.
|
|
5480
|
+
*/
|
|
5481
|
+
export function strictOutputSchemaOf(value) {
|
|
5482
|
+
if (isSchemaObject(value))
|
|
5483
|
+
return value;
|
|
5484
|
+
if (typeof value !== "string" || value.trim() === "")
|
|
5485
|
+
return null;
|
|
5486
|
+
try {
|
|
5487
|
+
const parsed = JSON.parse(value);
|
|
5488
|
+
return isSchemaObject(parsed) ? parsed : null;
|
|
5489
|
+
}
|
|
5490
|
+
catch {
|
|
5491
|
+
return null;
|
|
5492
|
+
}
|
|
5493
|
+
}
|
|
5494
|
+
/**
|
|
5495
|
+
* Is this (provider, strictOutput, outputFormat, schemas, status) a config the
|
|
5496
|
+
* platform can run as strict — or not strict at all?
|
|
5497
|
+
*/
|
|
5498
|
+
export function resolvePromptStrictOutput(input) {
|
|
5499
|
+
const value = input.strictOutput;
|
|
5500
|
+
if (isStrictOutputUnset(value) || value === false)
|
|
5501
|
+
return { ok: true, strict: false };
|
|
5502
|
+
if (value !== true) {
|
|
5503
|
+
return {
|
|
5504
|
+
ok: false,
|
|
5505
|
+
code: "PROMPT_STRICT_OUTPUT_INVALID",
|
|
5506
|
+
error: "strictOutput must be true or false; found " +
|
|
5507
|
+
`${JSON.stringify(value)} (${typeof value}). Write strictOutput = true ` +
|
|
5508
|
+
"without quotes, or remove the line.",
|
|
5509
|
+
};
|
|
5510
|
+
}
|
|
5511
|
+
if (!isStrictOutputUnset(input.configOutputSchema)) {
|
|
5512
|
+
return {
|
|
5513
|
+
ok: false,
|
|
5514
|
+
code: "PROMPT_STRICT_OUTPUT_SCHEMA_CONFLICT",
|
|
5515
|
+
error: "strictOutput = true sends the prompt's [prompt].outputSchema as the " +
|
|
5516
|
+
"strict json_schema, and this config also declares its own " +
|
|
5517
|
+
"[configs.chat].outputSchema, so which schema is sent would be " +
|
|
5518
|
+
"ambiguous. Remove the config's outputSchema (declare the schema on " +
|
|
5519
|
+
"[prompt]), or remove strictOutput.",
|
|
5520
|
+
};
|
|
5521
|
+
}
|
|
5522
|
+
const provider = typeof input.provider === "string" ? input.provider.trim().toLowerCase() : "";
|
|
5523
|
+
if (provider && provider !== "openrouter") {
|
|
5524
|
+
return {
|
|
5525
|
+
ok: false,
|
|
5526
|
+
code: "PROMPT_STRICT_OUTPUT_PROVIDER_UNSUPPORTED",
|
|
5527
|
+
error: `strictOutput = true is an OpenRouter option; this config runs on ` +
|
|
5528
|
+
`"${provider}". A gemini config already constrains its request with ` +
|
|
5529
|
+
"[prompt].outputSchema as responseSchema — remove strictOutput.",
|
|
5530
|
+
};
|
|
5531
|
+
}
|
|
5532
|
+
const outputFormat = typeof input.outputFormat === "string"
|
|
5533
|
+
? input.outputFormat.trim().toLowerCase()
|
|
5534
|
+
: "";
|
|
5535
|
+
if (outputFormat === "text") {
|
|
5536
|
+
return {
|
|
5537
|
+
ok: false,
|
|
5538
|
+
code: "PROMPT_STRICT_OUTPUT_FORMAT_CONFLICT",
|
|
5539
|
+
error: 'strictOutput = true asks for JSON matching [prompt].outputSchema, and ' +
|
|
5540
|
+
'this config says outputFormat = "text". Set outputFormat = "json" (or ' +
|
|
5541
|
+
"remove it), or remove strictOutput.",
|
|
5542
|
+
};
|
|
5543
|
+
}
|
|
5544
|
+
const status = typeof input.status === "string" ? input.status.trim().toLowerCase() : "";
|
|
5545
|
+
if (status !== "archived" && strictOutputSchemaOf(input.promptOutputSchema) === null) {
|
|
5546
|
+
return {
|
|
5547
|
+
ok: false,
|
|
5548
|
+
code: "PROMPT_STRICT_OUTPUT_SCHEMA_MISSING",
|
|
5549
|
+
error: "strictOutput = true sends the prompt's [prompt].outputSchema as the " +
|
|
5550
|
+
"strict json_schema, and this prompt declares none that is a JSON " +
|
|
5551
|
+
"object. Declare [prompt].outputSchema, or remove strictOutput.",
|
|
5552
|
+
};
|
|
5553
|
+
}
|
|
5554
|
+
return { ok: true, strict: true };
|
|
5555
|
+
}
|
|
5556
|
+
// ── src/config-surface/agent-declaration.ts ──────────────────────────────
|
|
5557
|
+
/**
|
|
5558
|
+
* What an agent prompt may declare — issue #3798 (project `agents`, §D1).
|
|
5115
5559
|
*
|
|
5116
|
-
* `
|
|
5117
|
-
*
|
|
5118
|
-
*
|
|
5119
|
-
*
|
|
5560
|
+
* An agent is a prompt of `kind = "agent"`. Its configs carry `[configs.agent]`
|
|
5561
|
+
* (the rule for those is `resolvePromptKind`); its contract lives in ONE
|
|
5562
|
+
* prompt-level block, `[prompt.agent]`: the tools the model may call, the event
|
|
5563
|
+
* kinds members record, the turn context, how history is selected, who answers
|
|
5564
|
+
* a paused call, and the limits an agent may lower but never raise.
|
|
5120
5565
|
*
|
|
5121
|
-
*
|
|
5122
|
-
*
|
|
5123
|
-
*
|
|
5566
|
+
* This module is the whole rule for that block. `resolveAgentDeclaration` is
|
|
5567
|
+
* called by the CLI's push preflight through the vendored artifact and by the
|
|
5568
|
+
* admin prompt routes, so a refusal at push and a refusal at the route cannot
|
|
5569
|
+
* disagree — the `resolvePromptKind` posture of #3626.
|
|
5570
|
+
*
|
|
5571
|
+
* STORED AS WRITTEN (DSO-3798-001). The resolver returns the AUTHORED value —
|
|
5572
|
+
* a block written as one JSON string is parsed to the object it encodes, and
|
|
5573
|
+
* nothing is added. `config diff` compares a `json` field by value with no
|
|
5574
|
+
* defaults applied, so a stored declaration with defaults filled in would read
|
|
5575
|
+
* Modified after every push, and `config pull` would write keys the author
|
|
5576
|
+
* never did. The defaults live in one reader instead, `normalizeAgentDeclaration`,
|
|
5577
|
+
* which every consumer (the executor, codegen, the turn engine) calls.
|
|
5578
|
+
*
|
|
5579
|
+
* Every refusal carries a stable code (intent criterion 22) and a refusal lists
|
|
5580
|
+
* EVERY offending item, so an author fixes a declaration in one pass.
|
|
5581
|
+
*
|
|
5582
|
+
* Pure and dependency-free, like every module under `src/config-surface/`.
|
|
5124
5583
|
*/
|
|
5125
|
-
|
|
5584
|
+
/** The stable codes every agent agentRefusal this project adds carries. */
|
|
5585
|
+
export const AGENT_ERROR_CODES = [
|
|
5586
|
+
"AGENT_DECLARATION_INVALID",
|
|
5587
|
+
"AGENT_TOOL_FUNCTION_MISSING",
|
|
5588
|
+
"AGENT_TOOL_FUNCTION_SCHEMA_MISSING",
|
|
5589
|
+
"AGENT_OUTPUT_SCHEMA_NOT_ALLOWED",
|
|
5590
|
+
"AGENT_CONFIG_KEY_NOT_ALLOWED",
|
|
5591
|
+
"AGENT_NOT_RUNNABLE",
|
|
5592
|
+
"AGENT_TEST_CASE_NOT_SUPPORTED",
|
|
5593
|
+
// #3799 §7 — a tool or event schema that cannot be mapped without loss to
|
|
5594
|
+
// the provider of a config that is not archived (`provider-schema.ts`).
|
|
5595
|
+
"AGENT_SCHEMA_NOT_TRANSLATABLE",
|
|
5596
|
+
// #3799 §7 — the capability rule (`src/llm/model-capabilities.ts`). The
|
|
5597
|
+
// catalog does not list the model; it lists it and it lacks a capability an
|
|
5598
|
+
// agent needs; or the capability could not be established at all
|
|
5599
|
+
// (DSO-3799-002 — a refusal, not a pass).
|
|
5600
|
+
"AGENT_MODEL_UNKNOWN",
|
|
5601
|
+
"AGENT_MODEL_CAPABILITY_MISSING",
|
|
5602
|
+
"AGENT_MODEL_CAPABILITIES_UNAVAILABLE",
|
|
5603
|
+
// #3800 §3 — the agent round. A round asked of a prompt that is not an
|
|
5604
|
+
// agent; a round whose input cannot mean anything; a call naming a tool the
|
|
5605
|
+
// round did not declare; a call whose arguments fail its tool's schema; a
|
|
5606
|
+
// response cut off at the token limit; a response with nothing in it.
|
|
5607
|
+
"AGENT_ROUND_NOT_AGENT",
|
|
5608
|
+
"AGENT_ROUND_INPUT_INVALID",
|
|
5609
|
+
"AGENT_TOOL_UNKNOWN",
|
|
5610
|
+
"AGENT_TOOL_ARGUMENTS_INVALID",
|
|
5611
|
+
"AGENT_ROUND_TRUNCATED",
|
|
5612
|
+
"AGENT_ROUND_EMPTY",
|
|
5613
|
+
];
|
|
5126
5614
|
/**
|
|
5127
|
-
*
|
|
5128
|
-
*
|
|
5129
|
-
*
|
|
5130
|
-
* `config push` keeps taking a file written before the `[configs.chat]` block
|
|
5131
|
-
* existed, while `config pull` never writes the spelling again. One
|
|
5132
|
-
* pull-then-push migrates a file. `hoistDeprecatedChatKeys` moves them into the
|
|
5133
|
-
* group before the payload is built; rejecting the flat spelling outright is
|
|
5134
|
-
* #3642.
|
|
5615
|
+
* The platform maximums. An agent may lower `maxSteps` and `waitLimitSeconds`,
|
|
5616
|
+
* never raise them; the tool and event counts bound what one declaration can
|
|
5617
|
+
* make every turn carry (principle 2).
|
|
5135
5618
|
*/
|
|
5136
|
-
const
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
]
|
|
5147
|
-
export const
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5619
|
+
export const AGENT_LIMITS = {
|
|
5620
|
+
maxSteps: 25,
|
|
5621
|
+
waitLimitSeconds: 604800,
|
|
5622
|
+
tools: 64,
|
|
5623
|
+
events: 64,
|
|
5624
|
+
};
|
|
5625
|
+
/**
|
|
5626
|
+
* A tool or event name: the intersection of OpenRouter's and Gemini's
|
|
5627
|
+
* function-name rules, so one declaration maps onto either provider.
|
|
5628
|
+
*/
|
|
5629
|
+
export const AGENT_NAME = /^[A-Za-z_][A-Za-z0-9_-]{0,63}$/;
|
|
5630
|
+
export const AGENT_ANSWERED_BY = ["initiator", "participants"];
|
|
5631
|
+
export const AGENT_TOOL_INVOKE = ["request", "task"];
|
|
5632
|
+
export const AGENT_TOOL_HISTORY_SCOPES = ["session", "turn"];
|
|
5633
|
+
const AGENT_TOP_LEVEL_KEYS = [
|
|
5634
|
+
"answeredBy",
|
|
5635
|
+
"maxSteps",
|
|
5636
|
+
"waitLimitSeconds",
|
|
5637
|
+
"turnContext",
|
|
5638
|
+
"history",
|
|
5639
|
+
"tools",
|
|
5640
|
+
"events",
|
|
5641
|
+
];
|
|
5642
|
+
const AGENT_TURN_CONTEXT_KEYS = ["schema", "function"];
|
|
5643
|
+
const AGENT_HISTORY_KEYS = ["function", "maxHistoryChars"];
|
|
5644
|
+
const AGENT_TOOL_KEYS = [
|
|
5645
|
+
"name",
|
|
5646
|
+
"description",
|
|
5647
|
+
"function",
|
|
5648
|
+
"invoke",
|
|
5649
|
+
"runs",
|
|
5650
|
+
"approval",
|
|
5651
|
+
"statusText",
|
|
5652
|
+
"historyScope",
|
|
5653
|
+
"inputSchema",
|
|
5654
|
+
"outputSchema",
|
|
5655
|
+
];
|
|
5656
|
+
const AGENT_EVENT_KEYS = ["name", "schema"];
|
|
5657
|
+
/** Where every message points: the author's own spelling of the block. */
|
|
5658
|
+
const AGENT_BLOCK = "[prompt.agent]";
|
|
5659
|
+
/**
|
|
5660
|
+
* The same label, for the sibling rules that refuse a declaration from their
|
|
5661
|
+
* own module (#3799's `provider-schema.ts`), so every agent refusal points at
|
|
5662
|
+
* the block the author wrote.
|
|
5663
|
+
*/
|
|
5664
|
+
export const AGENT_BLOCK_LABEL = AGENT_BLOCK;
|
|
5665
|
+
function agentRefusal(code, message) {
|
|
5666
|
+
return { code, message };
|
|
5667
|
+
}
|
|
5668
|
+
function agentInvalid(path, detail) {
|
|
5669
|
+
return agentRefusal("AGENT_DECLARATION_INVALID", `${AGENT_BLOCK} ${path ? `${path} ` : ""}${detail}`);
|
|
5670
|
+
}
|
|
5671
|
+
/** `null`, `undefined` and `""` are absence; `false` and `0` are values. */
|
|
5672
|
+
function agentIsUnset(value) {
|
|
5673
|
+
if (value === null || value === undefined)
|
|
5674
|
+
return true;
|
|
5675
|
+
return typeof value === "string" && value.trim() === "";
|
|
5676
|
+
}
|
|
5677
|
+
function agentIsPlainObject(value) {
|
|
5678
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5679
|
+
}
|
|
5680
|
+
function agentIsNonEmptyString(value) {
|
|
5681
|
+
return typeof value === "string" && value.trim() !== "";
|
|
5682
|
+
}
|
|
5683
|
+
function agentKindOf(kind) {
|
|
5684
|
+
return agentIsUnset(kind) ? "chat" : String(kind).trim();
|
|
5685
|
+
}
|
|
5686
|
+
/**
|
|
5687
|
+
* The block as a value: a table as it is, a JSON string parsed. `undefined`
|
|
5688
|
+
* for anything that is not a JSON object, which the resolver refuses.
|
|
5689
|
+
*/
|
|
5690
|
+
function parseAgentBlock(agent) {
|
|
5691
|
+
if (agentIsPlainObject(agent))
|
|
5692
|
+
return agent;
|
|
5693
|
+
if (typeof agent === "string") {
|
|
5694
|
+
try {
|
|
5695
|
+
const parsed = JSON.parse(agent);
|
|
5696
|
+
return agentIsPlainObject(parsed) ? parsed : undefined;
|
|
5697
|
+
}
|
|
5698
|
+
catch {
|
|
5699
|
+
return undefined;
|
|
5700
|
+
}
|
|
5701
|
+
}
|
|
5702
|
+
return undefined;
|
|
5703
|
+
}
|
|
5704
|
+
function agentUnknownKeys(value, allowed, path, out) {
|
|
5705
|
+
for (const key of Object.keys(value)) {
|
|
5706
|
+
if (!allowed.includes(key)) {
|
|
5707
|
+
out.push(agentInvalid(path ? `${path}.${key}` : key, `is not a key an agent declares; allowed: ${allowed.join(", ")}`));
|
|
5708
|
+
}
|
|
5709
|
+
}
|
|
5710
|
+
}
|
|
5711
|
+
/**
|
|
5712
|
+
* A schema position holds an OBJECT. A JSON string there is refused rather
|
|
5713
|
+
* than parsed: the stored value must equal the pushed one, and a nested string
|
|
5714
|
+
* parsed server-side would not (DSO-3798-001).
|
|
5715
|
+
*/
|
|
5716
|
+
function agentSchemaError(value, path) {
|
|
5717
|
+
if (agentIsPlainObject(value))
|
|
5718
|
+
return null;
|
|
5719
|
+
const found = typeof value === "string"
|
|
5720
|
+
? "a string"
|
|
5721
|
+
: Array.isArray(value)
|
|
5722
|
+
? "a list"
|
|
5723
|
+
: typeof value;
|
|
5724
|
+
return agentInvalid(path, `must be a table (a JSON Schema object); found ${found}. Write it as ` +
|
|
5725
|
+
"TOML tables, or as an object inside a block written as one JSON string");
|
|
5726
|
+
}
|
|
5727
|
+
function agentIntegerInRange(value, path, max, out) {
|
|
5728
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
5729
|
+
out.push(agentInvalid(path, `must be an integer from 1 to ${max}`));
|
|
5730
|
+
return;
|
|
5731
|
+
}
|
|
5732
|
+
if (value < 1 || value > max) {
|
|
5733
|
+
out.push(agentInvalid(path, `must be from 1 to ${max} (the platform maximum); found ${value}`));
|
|
5734
|
+
}
|
|
5735
|
+
}
|
|
5736
|
+
function checkAgentName(value, path, seen, what, out) {
|
|
5737
|
+
if (typeof value !== "string" || !AGENT_NAME.test(value)) {
|
|
5738
|
+
out.push(agentInvalid(path, `is not a usable ${what} name; names match ` +
|
|
5739
|
+
"[A-Za-z_][A-Za-z0-9_-]{0,63} (the rule both providers accept)"));
|
|
5740
|
+
return;
|
|
5741
|
+
}
|
|
5742
|
+
if (seen.has(value)) {
|
|
5743
|
+
out.push(agentInvalid(path, `repeats the ${what} name "${value}"; names are unique`));
|
|
5744
|
+
return;
|
|
5745
|
+
}
|
|
5746
|
+
seen.add(value);
|
|
5747
|
+
}
|
|
5748
|
+
function checkAgentTool(tool, index, names, out) {
|
|
5749
|
+
const path = `tools[${index}]`;
|
|
5750
|
+
if (!agentIsPlainObject(tool)) {
|
|
5751
|
+
out.push(agentInvalid(path, "must be a table declaring the tool"));
|
|
5752
|
+
return;
|
|
5753
|
+
}
|
|
5754
|
+
agentUnknownKeys(tool, AGENT_TOOL_KEYS, path, out);
|
|
5755
|
+
checkAgentName(tool.name, `${path}.name`, names, "tool", out);
|
|
5756
|
+
if (!agentIsNonEmptyString(tool.description)) {
|
|
5757
|
+
out.push(agentInvalid(`${path}.description`, "is required — the sentence the model reads to decide when to call it"));
|
|
5758
|
+
}
|
|
5759
|
+
const hasFunction = tool.function !== undefined;
|
|
5760
|
+
const hasRuns = tool.runs !== undefined;
|
|
5761
|
+
if (hasFunction === hasRuns) {
|
|
5762
|
+
out.push(agentInvalid(path, hasFunction
|
|
5763
|
+
? 'sets both function and runs; a tool is a server tool (function = "<key>") or a client tool (runs = "client"), not both'
|
|
5764
|
+
: 'needs function = "<key>" (a server tool) or runs = "client" (a client tool)'));
|
|
5765
|
+
}
|
|
5766
|
+
if (hasRuns && tool.runs !== "client") {
|
|
5767
|
+
out.push(agentInvalid(`${path}.runs`, `must be "client"; found ${JSON.stringify(tool.runs)}`));
|
|
5768
|
+
}
|
|
5769
|
+
if (hasFunction && !agentIsNonEmptyString(tool.function)) {
|
|
5770
|
+
out.push(agentInvalid(`${path}.function`, "must name a server function by key"));
|
|
5771
|
+
}
|
|
5772
|
+
const isClient = hasRuns && !hasFunction;
|
|
5773
|
+
if (tool.invoke !== undefined) {
|
|
5774
|
+
if (isClient) {
|
|
5775
|
+
out.push(agentInvalid(`${path}.invoke`, "is for server tools; a client tool is answered by a member's client"));
|
|
5776
|
+
}
|
|
5777
|
+
else if (!AGENT_TOOL_INVOKE.includes(tool.invoke)) {
|
|
5778
|
+
out.push(agentInvalid(`${path}.invoke`, `must be one of ${AGENT_TOOL_INVOKE.join(" | ")}`));
|
|
5779
|
+
}
|
|
5780
|
+
}
|
|
5781
|
+
for (const key of ["inputSchema", "outputSchema"]) {
|
|
5782
|
+
const schemaPath = `${path}.${key}`;
|
|
5783
|
+
if (isClient) {
|
|
5784
|
+
if (tool[key] === undefined) {
|
|
5785
|
+
out.push(agentInvalid(schemaPath, "is required on a client tool"));
|
|
5786
|
+
continue;
|
|
5787
|
+
}
|
|
5788
|
+
const error = agentSchemaError(tool[key], schemaPath);
|
|
5789
|
+
if (error)
|
|
5790
|
+
out.push(error);
|
|
5791
|
+
}
|
|
5792
|
+
else if (hasFunction && tool[key] !== undefined) {
|
|
5793
|
+
out.push(agentInvalid(schemaPath, "is not declared on a server tool; its function's schemas are the " +
|
|
5794
|
+
"tool's (one source of truth) — remove it"));
|
|
5795
|
+
}
|
|
5796
|
+
}
|
|
5797
|
+
if (tool.approval !== undefined && typeof tool.approval !== "boolean") {
|
|
5798
|
+
out.push(agentInvalid(`${path}.approval`, "must be true or false"));
|
|
5799
|
+
}
|
|
5800
|
+
if (tool.statusText !== undefined && !agentIsNonEmptyString(tool.statusText)) {
|
|
5801
|
+
out.push(agentInvalid(`${path}.statusText`, "must be a non-empty string"));
|
|
5802
|
+
}
|
|
5803
|
+
if (tool.historyScope !== undefined &&
|
|
5804
|
+
!AGENT_TOOL_HISTORY_SCOPES.includes(tool.historyScope)) {
|
|
5805
|
+
out.push(agentInvalid(`${path}.historyScope`, `must be one of ${AGENT_TOOL_HISTORY_SCOPES.join(" | ")}`));
|
|
5806
|
+
}
|
|
5807
|
+
}
|
|
5808
|
+
function checkAgentEvent(event, index, names, out) {
|
|
5809
|
+
const path = `events[${index}]`;
|
|
5810
|
+
if (!agentIsPlainObject(event)) {
|
|
5811
|
+
out.push(agentInvalid(path, "must be a table declaring the event kind"));
|
|
5812
|
+
return;
|
|
5813
|
+
}
|
|
5814
|
+
agentUnknownKeys(event, AGENT_EVENT_KEYS, path, out);
|
|
5815
|
+
checkAgentName(event.name, `${path}.name`, names, "event", out);
|
|
5816
|
+
if (event.schema === undefined) {
|
|
5817
|
+
out.push(agentInvalid(`${path}.schema`, "is required — the shape of the event's payload"));
|
|
5818
|
+
}
|
|
5819
|
+
else {
|
|
5820
|
+
const error = agentSchemaError(event.schema, `${path}.schema`);
|
|
5821
|
+
if (error)
|
|
5822
|
+
out.push(error);
|
|
5823
|
+
}
|
|
5824
|
+
}
|
|
5825
|
+
function checkAgentList(value, path, max, check, out) {
|
|
5826
|
+
if (value === undefined)
|
|
5827
|
+
return;
|
|
5828
|
+
if (!Array.isArray(value)) {
|
|
5829
|
+
out.push(agentInvalid(path, `must be a list of tables ([[prompt.agent.${path}]])`));
|
|
5830
|
+
return;
|
|
5831
|
+
}
|
|
5832
|
+
if (value.length > max) {
|
|
5833
|
+
out.push(agentInvalid(path, `declares ${value.length}; at most ${max} are allowed`));
|
|
5834
|
+
return;
|
|
5835
|
+
}
|
|
5836
|
+
const names = new Set();
|
|
5837
|
+
value.forEach((item, index) => check(item, index, names, out));
|
|
5838
|
+
}
|
|
5839
|
+
/** Every file-decidable agentRefusal of a parsed block. */
|
|
5840
|
+
function agentShapeRefusals(agent) {
|
|
5841
|
+
const out = [];
|
|
5842
|
+
agentUnknownKeys(agent, AGENT_TOP_LEVEL_KEYS, "", out);
|
|
5843
|
+
if (agent.answeredBy !== undefined &&
|
|
5844
|
+
!AGENT_ANSWERED_BY.includes(agent.answeredBy)) {
|
|
5845
|
+
out.push(agentInvalid("answeredBy", `must be one of ${AGENT_ANSWERED_BY.join(" | ")}`));
|
|
5846
|
+
}
|
|
5847
|
+
if (agent.maxSteps !== undefined) {
|
|
5848
|
+
agentIntegerInRange(agent.maxSteps, "maxSteps", AGENT_LIMITS.maxSteps, out);
|
|
5849
|
+
}
|
|
5850
|
+
if (agent.waitLimitSeconds !== undefined) {
|
|
5851
|
+
agentIntegerInRange(agent.waitLimitSeconds, "waitLimitSeconds", AGENT_LIMITS.waitLimitSeconds, out);
|
|
5852
|
+
}
|
|
5853
|
+
if (agent.turnContext !== undefined) {
|
|
5854
|
+
const turnContext = agent.turnContext;
|
|
5855
|
+
if (!agentIsPlainObject(turnContext)) {
|
|
5856
|
+
out.push(agentInvalid("turnContext", "must be a table: { schema, function? }"));
|
|
5857
|
+
}
|
|
5858
|
+
else {
|
|
5859
|
+
agentUnknownKeys(turnContext, AGENT_TURN_CONTEXT_KEYS, "turnContext", out);
|
|
5860
|
+
if (turnContext.schema === undefined) {
|
|
5861
|
+
out.push(agentInvalid("turnContext.schema", "is required — the shape of the context a send carries"));
|
|
5862
|
+
}
|
|
5863
|
+
else {
|
|
5864
|
+
const error = agentSchemaError(turnContext.schema, "turnContext.schema");
|
|
5865
|
+
if (error)
|
|
5866
|
+
out.push(error);
|
|
5867
|
+
}
|
|
5868
|
+
if (turnContext.function !== undefined &&
|
|
5869
|
+
!agentIsNonEmptyString(turnContext.function)) {
|
|
5870
|
+
out.push(agentInvalid("turnContext.function", "must name a server function by key"));
|
|
5871
|
+
}
|
|
5872
|
+
}
|
|
5873
|
+
}
|
|
5874
|
+
if (agent.history !== undefined) {
|
|
5875
|
+
const history = agent.history;
|
|
5876
|
+
if (!agentIsPlainObject(history)) {
|
|
5877
|
+
out.push(agentInvalid("history", "must be a table: { function } or { maxHistoryChars }"));
|
|
5878
|
+
}
|
|
5879
|
+
else {
|
|
5880
|
+
agentUnknownKeys(history, AGENT_HISTORY_KEYS, "history", out);
|
|
5881
|
+
const hasFunction = history.function !== undefined;
|
|
5882
|
+
const hasCap = history.maxHistoryChars !== undefined;
|
|
5883
|
+
if (hasFunction === hasCap) {
|
|
5884
|
+
out.push(agentInvalid("history", hasFunction
|
|
5885
|
+
? "sets both function and maxHistoryChars; a hook selects history or a cap trims it, not both"
|
|
5886
|
+
: "needs function = \"<key>\" (a hook) or maxHistoryChars (a cap)"));
|
|
5887
|
+
}
|
|
5888
|
+
if (hasFunction && !agentIsNonEmptyString(history.function)) {
|
|
5889
|
+
out.push(agentInvalid("history.function", "must name a server function by key"));
|
|
5890
|
+
}
|
|
5891
|
+
if (hasCap &&
|
|
5892
|
+
(typeof history.maxHistoryChars !== "number" ||
|
|
5893
|
+
!Number.isInteger(history.maxHistoryChars) ||
|
|
5894
|
+
history.maxHistoryChars < 1)) {
|
|
5895
|
+
out.push(agentInvalid("history.maxHistoryChars", "must be a positive integer"));
|
|
5896
|
+
}
|
|
5897
|
+
}
|
|
5898
|
+
}
|
|
5899
|
+
checkAgentList(agent.tools, "tools", AGENT_LIMITS.tools, checkAgentTool, out);
|
|
5900
|
+
checkAgentList(agent.events, "events", AGENT_LIMITS.events, checkAgentEvent, out);
|
|
5901
|
+
return out;
|
|
5902
|
+
}
|
|
5903
|
+
/** Every function reference, with the path that makes it. */
|
|
5904
|
+
function agentFunctionReferences(agent) {
|
|
5905
|
+
const refs = [];
|
|
5906
|
+
if (Array.isArray(agent.tools)) {
|
|
5907
|
+
agent.tools.forEach((tool, index) => {
|
|
5908
|
+
if (agentIsPlainObject(tool) && agentIsNonEmptyString(tool.function)) {
|
|
5909
|
+
refs.push({
|
|
5910
|
+
key: tool.function,
|
|
5911
|
+
tool: typeof tool.name === "string" ? tool.name : undefined,
|
|
5912
|
+
path: `tools[${index}].function`,
|
|
5913
|
+
needsSchemas: true,
|
|
5914
|
+
});
|
|
5915
|
+
}
|
|
5916
|
+
});
|
|
5917
|
+
}
|
|
5918
|
+
const turnContext = agent.turnContext;
|
|
5919
|
+
if (agentIsPlainObject(turnContext) && agentIsNonEmptyString(turnContext.function)) {
|
|
5920
|
+
refs.push({
|
|
5921
|
+
key: turnContext.function,
|
|
5922
|
+
path: "turnContext.function",
|
|
5923
|
+
needsSchemas: false,
|
|
5924
|
+
});
|
|
5925
|
+
}
|
|
5926
|
+
const history = agent.history;
|
|
5927
|
+
if (agentIsPlainObject(history) && agentIsNonEmptyString(history.function)) {
|
|
5928
|
+
refs.push({ key: history.function, path: "history.function", needsSchemas: false });
|
|
5929
|
+
}
|
|
5930
|
+
return refs;
|
|
5931
|
+
}
|
|
5932
|
+
/**
|
|
5933
|
+
* The function keys a declaration references (tools, turn context, history),
|
|
5934
|
+
* each once, in declaration order. What a caller resolves into the
|
|
5935
|
+
* `functions` facts before calling the resolver. `[]` for a block that is
|
|
5936
|
+
* absent or does not parse — the resolver refuses the latter itself.
|
|
5937
|
+
*/
|
|
5938
|
+
/**
|
|
5939
|
+
* `[prompt.agent]` as a value: a table as it is, one JSON string parsed,
|
|
5940
|
+
* `undefined` for absence or for anything that is not a JSON object (which
|
|
5941
|
+
* `resolveAgentDeclaration` refuses on its own).
|
|
5942
|
+
*
|
|
5943
|
+
* Exported for the sibling rules that read the same block — #3799's
|
|
5944
|
+
* `agentSchemaTranslationRefusals` — so a declaration written as one JSON
|
|
5945
|
+
* string is read identically by every rule that consumes it.
|
|
5946
|
+
*/
|
|
5947
|
+
export function parseAgentDeclarationBlock(agent) {
|
|
5948
|
+
return parseAgentBlock(agent);
|
|
5949
|
+
}
|
|
5950
|
+
export function agentDeclarationFunctionKeys(agent) {
|
|
5951
|
+
const parsed = parseAgentBlock(agent);
|
|
5952
|
+
if (!parsed)
|
|
5953
|
+
return [];
|
|
5954
|
+
return [...new Set(agentFunctionReferences(parsed).map((ref) => ref.key))];
|
|
5955
|
+
}
|
|
5956
|
+
function agentFunctionRefusals(agent, functions) {
|
|
5957
|
+
const out = [];
|
|
5958
|
+
for (const ref of agentFunctionReferences(agent)) {
|
|
5959
|
+
const subject = ref.tool
|
|
5960
|
+
? `tool "${ref.tool}" (${ref.path})`
|
|
5961
|
+
: ref.path;
|
|
5962
|
+
const facts = Object.prototype.hasOwnProperty.call(functions, ref.key)
|
|
5963
|
+
? functions[ref.key]
|
|
5964
|
+
: null;
|
|
5965
|
+
if (!facts) {
|
|
5966
|
+
out.push(agentRefusal("AGENT_TOOL_FUNCTION_MISSING", `${AGENT_BLOCK} ${subject} names function "${ref.key}", which this app ` +
|
|
5967
|
+
"does not have. Push the function first, or include its file in " +
|
|
5968
|
+
"this push"));
|
|
5969
|
+
continue;
|
|
5970
|
+
}
|
|
5971
|
+
if (!ref.needsSchemas)
|
|
5972
|
+
continue;
|
|
5973
|
+
const missing = [
|
|
5974
|
+
facts.hasInputSchema ? null : "inputSchema",
|
|
5975
|
+
facts.hasOutputSchema ? null : "outputSchema",
|
|
5976
|
+
].filter((name) => name !== null);
|
|
5977
|
+
if (missing.length > 0) {
|
|
5978
|
+
out.push(agentRefusal("AGENT_TOOL_FUNCTION_SCHEMA_MISSING", `${AGENT_BLOCK} ${subject} names function "${ref.key}", which declares ` +
|
|
5979
|
+
`no ${missing.join(" or ")}. A tool's schemas are its function's: ` +
|
|
5980
|
+
`declare ${missing.join(" and ")} in functions/${ref.key}.toml`));
|
|
5981
|
+
}
|
|
5982
|
+
}
|
|
5983
|
+
return out;
|
|
5984
|
+
}
|
|
5985
|
+
/**
|
|
5986
|
+
* The one rule: is this `[prompt.agent]` declaration (with the prompt's kind
|
|
5987
|
+
* and `outputSchema`) something the platform can run?
|
|
5988
|
+
*
|
|
5989
|
+
* `{ ok: true, declaration }` carries the AUTHORED value — parsed when the
|
|
5990
|
+
* block was one JSON string, otherwise unchanged — or `null` for an absent
|
|
5991
|
+
* block. Callers store exactly this.
|
|
5992
|
+
*/
|
|
5993
|
+
export function resolveAgentDeclaration(input) {
|
|
5994
|
+
const kind = agentKindOf(input.kind);
|
|
5995
|
+
const refusals = [];
|
|
5996
|
+
if (kind !== "agent") {
|
|
5997
|
+
if (agentIsUnset(input.agent))
|
|
5998
|
+
return { ok: true, declaration: null };
|
|
5999
|
+
return {
|
|
6000
|
+
ok: false,
|
|
6001
|
+
refusals: [
|
|
6002
|
+
agentInvalid("", `is declared on a prompt whose kind is "${kind}"; only a ` +
|
|
6003
|
+
'kind = "agent" prompt carries it. Remove the block, or create ' +
|
|
6004
|
+
"the agent under a new key"),
|
|
6005
|
+
],
|
|
6006
|
+
};
|
|
6007
|
+
}
|
|
6008
|
+
if (!agentIsUnset(input.outputSchema)) {
|
|
6009
|
+
refusals.push(agentRefusal("AGENT_OUTPUT_SCHEMA_NOT_ALLOWED", "[prompt].outputSchema is not declared on an agent: its final answer " +
|
|
6010
|
+
"is text for people, and anything structured is a tool call. Remove " +
|
|
6011
|
+
"outputSchema, or declare a tool whose schema carries the structure"));
|
|
6012
|
+
}
|
|
6013
|
+
let declaration = null;
|
|
6014
|
+
if (!agentIsUnset(input.agent)) {
|
|
6015
|
+
const parsed = parseAgentBlock(input.agent);
|
|
6016
|
+
if (!parsed) {
|
|
6017
|
+
refusals.push(agentInvalid("", "must be a table (or one JSON string encoding a table) declaring " +
|
|
6018
|
+
"the agent's tools, events, turn context, history and limits"));
|
|
6019
|
+
}
|
|
6020
|
+
else {
|
|
6021
|
+
declaration = parsed;
|
|
6022
|
+
refusals.push(...agentShapeRefusals(parsed));
|
|
6023
|
+
if (input.functions) {
|
|
6024
|
+
refusals.push(...agentFunctionRefusals(parsed, input.functions));
|
|
6025
|
+
}
|
|
6026
|
+
}
|
|
6027
|
+
}
|
|
6028
|
+
if (refusals.length > 0)
|
|
6029
|
+
return { ok: false, refusals };
|
|
6030
|
+
return { ok: true, declaration: declaration };
|
|
6031
|
+
}
|
|
6032
|
+
/**
|
|
6033
|
+
* The refusal of a single-shot run of an agent — `ctx.prompts.run`, the member
|
|
6034
|
+
* execute route, the admin execute and preview routes, the workflow
|
|
6035
|
+
* `prompt.execute` step and the executor itself (§D1: an agent runs turn by
|
|
6036
|
+
* turn in a session, never once).
|
|
6037
|
+
*/
|
|
6038
|
+
export function agentNotRunnableRefusal(promptKey) {
|
|
6039
|
+
const subject = promptKey === undefined ? "This prompt" : `Prompt "${promptKey}"`;
|
|
6040
|
+
return agentRefusal("AGENT_NOT_RUNNABLE", `${subject} is an agent (kind = "agent"), which runs turn by turn in a ` +
|
|
6041
|
+
"session and cannot be run single-shot. Run a chat or decisions " +
|
|
6042
|
+
"prompt here instead");
|
|
6043
|
+
}
|
|
6044
|
+
/**
|
|
6045
|
+
* The refusal of an agent round asked of a prompt that is not an agent —
|
|
6046
|
+
* #3800 (`PromptExecutionService.resolveAgentTurnConfig` and
|
|
6047
|
+
* `executeAgentRound`). The mirror of `agentNotRunnableRefusal`.
|
|
6048
|
+
*/
|
|
6049
|
+
export function agentRoundNotAgentRefusal(promptKey) {
|
|
6050
|
+
return agentRefusal("AGENT_ROUND_NOT_AGENT", `Prompt "${promptKey}" is not an agent (kind = "agent"), so it has no ` +
|
|
6051
|
+
"agent rounds. Run it with ctx.prompts.run, or name an agent prompt");
|
|
6052
|
+
}
|
|
6053
|
+
/**
|
|
6054
|
+
* The refusal of a prompt test case that involves an agent (§D1: prompt test
|
|
6055
|
+
* cases are refused for the kind in v1; multi-turn tests are deferred).
|
|
6056
|
+
* `subject` is a case written ON the agent, `evaluator` a case that names the
|
|
6057
|
+
* agent as its judge.
|
|
6058
|
+
*/
|
|
6059
|
+
export function agentTestCaseRefusal(promptKey, role) {
|
|
6060
|
+
return agentRefusal("AGENT_TEST_CASE_NOT_SUPPORTED", role === "subject"
|
|
6061
|
+
? `Prompt "${promptKey}" is an agent (kind = "agent"); prompt test ` +
|
|
6062
|
+
"cases are not supported for agents yet. Test its tools as functions"
|
|
6063
|
+
: `Prompt "${promptKey}" is an agent (kind = "agent") and cannot be an ` +
|
|
6064
|
+
"evaluator: an evaluator is run single-shot. Name a chat prompt");
|
|
6065
|
+
}
|
|
6066
|
+
/** A deep copy, so a reader's defaults never reach the caller's value. */
|
|
6067
|
+
function agentCopy(value) {
|
|
6068
|
+
return value === undefined ? value : JSON.parse(JSON.stringify(value));
|
|
6069
|
+
}
|
|
6070
|
+
/**
|
|
6071
|
+
* The declaration with every default applied: `answeredBy = "initiator"`, the
|
|
6072
|
+
* platform maximums for the limits, and per tool `invoke = "request"` (server
|
|
6073
|
+
* tools), `approval = false` and `historyScope = "session"`.
|
|
6074
|
+
*
|
|
6075
|
+
* The ONLY accessor for defaults — the stored value never carries them
|
|
6076
|
+
* (DSO-3798-001). Takes a value `resolveAgentDeclaration` accepted (or `null`)
|
|
6077
|
+
* and leaves it untouched.
|
|
6078
|
+
*/
|
|
6079
|
+
export function normalizeAgentDeclaration(declaration) {
|
|
6080
|
+
const source = agentCopy(declaration ?? {});
|
|
6081
|
+
return {
|
|
6082
|
+
answeredBy: source.answeredBy ?? "initiator",
|
|
6083
|
+
maxSteps: source.maxSteps ?? AGENT_LIMITS.maxSteps,
|
|
6084
|
+
waitLimitSeconds: source.waitLimitSeconds ?? AGENT_LIMITS.waitLimitSeconds,
|
|
6085
|
+
turnContext: source.turnContext ?? null,
|
|
6086
|
+
history: source.history ?? null,
|
|
6087
|
+
tools: (source.tools ?? []).map((tool) => ({
|
|
6088
|
+
...tool,
|
|
6089
|
+
...(tool.runs === "client" ? {} : { invoke: tool.invoke ?? "request" }),
|
|
6090
|
+
approval: tool.approval ?? false,
|
|
6091
|
+
historyScope: tool.historyScope ?? "session",
|
|
6092
|
+
})),
|
|
6093
|
+
events: source.events ?? [],
|
|
6094
|
+
};
|
|
6095
|
+
}
|
|
6096
|
+
/**
|
|
6097
|
+
* The keywords that constrain only a value of one type, and so belong to that
|
|
6098
|
+
* type's member when a type union is rewritten as `anyOf` (CSO-3799-003). A
|
|
6099
|
+
* bare `{ type: "object" }` or `{ type: "array" }` member would be the
|
|
6100
|
+
* free-form object and the untyped array Gemini cannot carry.
|
|
6101
|
+
*/
|
|
6102
|
+
const GEMINI_UNION_MEMBER_KEYWORDS = {
|
|
6103
|
+
object: [
|
|
6104
|
+
"properties",
|
|
6105
|
+
"required",
|
|
6106
|
+
"minProperties",
|
|
6107
|
+
"maxProperties",
|
|
6108
|
+
"propertyOrdering",
|
|
6109
|
+
],
|
|
6110
|
+
array: ["items", "minItems", "maxItems"],
|
|
6111
|
+
};
|
|
6112
|
+
/** The six `type` names Gemini's function `Schema` accepts. */
|
|
6113
|
+
const GEMINI_TYPES = [
|
|
6114
|
+
"string",
|
|
6115
|
+
"number",
|
|
6116
|
+
"integer",
|
|
6117
|
+
"boolean",
|
|
6118
|
+
"object",
|
|
6119
|
+
"array",
|
|
6120
|
+
];
|
|
6121
|
+
/**
|
|
6122
|
+
* Keywords Gemini spells the same way, with the same meaning. `required` is
|
|
6123
|
+
* here rather than under the structural keywords because Gemini reads it
|
|
6124
|
+
* verbatim.
|
|
6125
|
+
*/
|
|
6126
|
+
const GEMINI_PASS_KEYWORDS = [
|
|
6127
|
+
"description",
|
|
6128
|
+
"title",
|
|
6129
|
+
"default",
|
|
6130
|
+
"minimum",
|
|
6131
|
+
"maximum",
|
|
6132
|
+
"minLength",
|
|
6133
|
+
"maxLength",
|
|
6134
|
+
"pattern",
|
|
6135
|
+
"minItems",
|
|
6136
|
+
"maxItems",
|
|
6137
|
+
"minProperties",
|
|
6138
|
+
"maxProperties",
|
|
6139
|
+
"required",
|
|
6140
|
+
// Gemini's own spellings, accepted as authored.
|
|
6141
|
+
"nullable",
|
|
6142
|
+
"propertyOrdering",
|
|
6143
|
+
"example",
|
|
6144
|
+
];
|
|
6145
|
+
/** Annotations with no value meaning: dropped in silence (spec §3). */
|
|
6146
|
+
const GEMINI_DROPPED_KEYWORDS = ["$schema", "$id", "$comment"];
|
|
6147
|
+
/** Keywords handled by their own rule below, not by the tables. */
|
|
6148
|
+
const GEMINI_STRUCTURAL_KEYWORDS = [
|
|
6149
|
+
"type",
|
|
6150
|
+
"enum",
|
|
6151
|
+
"const",
|
|
6152
|
+
"format",
|
|
6153
|
+
"properties",
|
|
6154
|
+
"items",
|
|
6155
|
+
"anyOf",
|
|
6156
|
+
"oneOf",
|
|
6157
|
+
"additionalProperties",
|
|
6158
|
+
];
|
|
6159
|
+
/** `format` values Gemini documents. Anything else is refused. */
|
|
6160
|
+
const GEMINI_FORMATS = [
|
|
6161
|
+
"date-time",
|
|
6162
|
+
"enum",
|
|
6163
|
+
"int32",
|
|
6164
|
+
"int64",
|
|
6165
|
+
"float",
|
|
6166
|
+
"double",
|
|
6167
|
+
];
|
|
6168
|
+
/**
|
|
6169
|
+
* Keywords Gemini's function `Schema` has no spelling for. Each is refused
|
|
6170
|
+
* rather than dropped: dropping any of them widens what the model may produce.
|
|
6171
|
+
*/
|
|
6172
|
+
const GEMINI_REFUSED_KEYWORDS = {
|
|
6173
|
+
$ref: "Gemini's function Schema has no $ref — inline the referenced schema",
|
|
6174
|
+
$defs: "Gemini's function Schema has no $defs — inline each definition where it is used",
|
|
6175
|
+
definitions: "Gemini's function Schema has no definitions — inline each definition where it is used",
|
|
6176
|
+
allOf: "Gemini's function Schema has no allOf — merge the members into one schema",
|
|
6177
|
+
not: "Gemini's function Schema has no not — state what is allowed instead",
|
|
6178
|
+
if: "Gemini's function Schema has no conditional schemas (if/then/else) — declare a discriminated oneOf instead",
|
|
6179
|
+
then: "Gemini's function Schema has no conditional schemas (if/then/else) — declare a discriminated oneOf instead",
|
|
6180
|
+
else: "Gemini's function Schema has no conditional schemas (if/then/else) — declare a discriminated oneOf instead",
|
|
6181
|
+
patternProperties: "Gemini's function Schema has no patternProperties — name the properties",
|
|
6182
|
+
propertyNames: "Gemini's function Schema has no propertyNames — name the properties",
|
|
6183
|
+
dependencies: "Gemini's function Schema has no property dependencies — declare a discriminated oneOf instead",
|
|
6184
|
+
dependentRequired: "Gemini's function Schema has no property dependencies — declare a discriminated oneOf instead",
|
|
6185
|
+
dependentSchemas: "Gemini's function Schema has no property dependencies — declare a discriminated oneOf instead",
|
|
6186
|
+
prefixItems: "Gemini's function Schema has no tuple items (prefixItems) — declare one items schema",
|
|
6187
|
+
contains: "Gemini's function Schema has no contains — declare items instead",
|
|
6188
|
+
uniqueItems: "Gemini's function Schema has no uniqueItems, and dropping it would let the model repeat an entry",
|
|
6189
|
+
multipleOf: "Gemini's function Schema has no multipleOf, and dropping it would widen the allowed numbers",
|
|
6190
|
+
exclusiveMinimum: "Gemini's function Schema has only minimum (inclusive), and dropping the exclusive bound would widen the allowed numbers",
|
|
6191
|
+
exclusiveMaximum: "Gemini's function Schema has only maximum (inclusive), and dropping the exclusive bound would widen the allowed numbers",
|
|
6192
|
+
examples: "Gemini's function Schema spells a sample as example (singular) — use example",
|
|
6193
|
+
contentMediaType: "Gemini's function Schema has no contentMediaType — describe the encoding in description",
|
|
6194
|
+
contentEncoding: "Gemini's function Schema has no contentEncoding — describe the encoding in description",
|
|
6195
|
+
readOnly: "Gemini's function Schema has no readOnly — drop it, or say so in description",
|
|
6196
|
+
writeOnly: "Gemini's function Schema has no writeOnly — drop it, or say so in description",
|
|
6197
|
+
deprecated: "Gemini's function Schema has no deprecated — say so in description",
|
|
6198
|
+
};
|
|
6199
|
+
/**
|
|
6200
|
+
* Sibling keywords that make a `oneOf` node a MIXED schema rather than a clean
|
|
6201
|
+
* tagged union. Exactly `describeSchema`'s list
|
|
6202
|
+
* (`src/workflows/schema-descriptor.ts`) — the platform's own rule for what a
|
|
6203
|
+
* discriminated union is, restated here because that module is vendored
|
|
6204
|
+
* separately and cannot be imported from `src/config-surface/`.
|
|
6205
|
+
*/
|
|
6206
|
+
const ONEOF_SIBLING_KEYWORDS = [
|
|
6207
|
+
"type",
|
|
6208
|
+
"properties",
|
|
6209
|
+
"required",
|
|
6210
|
+
"additionalProperties",
|
|
6211
|
+
"items",
|
|
6212
|
+
"enum",
|
|
6213
|
+
];
|
|
6214
|
+
function isPlainSchemaObject(value) {
|
|
6215
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6216
|
+
}
|
|
6217
|
+
function schemaCopy(value) {
|
|
6218
|
+
return value === undefined ? value : JSON.parse(JSON.stringify(value));
|
|
6219
|
+
}
|
|
6220
|
+
/** A tag literal the platform validator can branch on with strict `===`. */
|
|
6221
|
+
function isScalarTag(value) {
|
|
6222
|
+
return (typeof value === "string" ||
|
|
6223
|
+
typeof value === "number" ||
|
|
6224
|
+
typeof value === "boolean");
|
|
6225
|
+
}
|
|
6226
|
+
/** `properties.amount` under `items` is `items.properties.amount`. */
|
|
6227
|
+
function childPath(path, step) {
|
|
6228
|
+
return path === "" ? step : `${path}.${step}`;
|
|
6229
|
+
}
|
|
6230
|
+
/** What a message calls the root. */
|
|
6231
|
+
export function schemaPathLabel(path) {
|
|
6232
|
+
return path === "" ? "the schema root" : path;
|
|
6233
|
+
}
|
|
6234
|
+
/**
|
|
6235
|
+
* The single-literal-tag properties of one `oneOf` member: `{ name → literal }`
|
|
6236
|
+
* for every property declared as a one-entry `enum` or a scalar `const`.
|
|
6237
|
+
*/
|
|
6238
|
+
function memberTagLiterals(member) {
|
|
6239
|
+
const tags = new Map();
|
|
6240
|
+
const properties = member.properties;
|
|
6241
|
+
if (!isPlainSchemaObject(properties))
|
|
6242
|
+
return tags;
|
|
6243
|
+
for (const [name, sub] of Object.entries(properties)) {
|
|
6244
|
+
if (!isPlainSchemaObject(sub))
|
|
6245
|
+
continue;
|
|
6246
|
+
if (Array.isArray(sub.enum) && sub.enum.length === 1 && isScalarTag(sub.enum[0])) {
|
|
6247
|
+
tags.set(name, sub.enum[0]);
|
|
6248
|
+
continue;
|
|
6249
|
+
}
|
|
6250
|
+
if (Object.prototype.hasOwnProperty.call(sub, "const") && isScalarTag(sub.const)) {
|
|
6251
|
+
tags.set(name, sub.const);
|
|
6252
|
+
}
|
|
6253
|
+
}
|
|
6254
|
+
return tags;
|
|
6255
|
+
}
|
|
6256
|
+
/**
|
|
6257
|
+
* Is this `oneOf` a discriminated union whose members are mutually exclusive
|
|
6258
|
+
* BY CONSTRUCTION, so that `anyOf` of the same members admits exactly the same
|
|
6259
|
+
* values (DSO-3799-003)?
|
|
6260
|
+
*
|
|
6261
|
+
* `describeSchema`'s rule, plus the one thing it does not check: the
|
|
6262
|
+
* discriminant must be listed under `required` in every member. Without that,
|
|
6263
|
+
* two members with optional tags both admit `{}`, which the authored `oneOf`
|
|
6264
|
+
* and the platform validator reject but an `anyOf` of the same members accepts
|
|
6265
|
+
* — so the rewrite would widen what the model may produce. The platform
|
|
6266
|
+
* validator already rejects a value whose tag is missing
|
|
6267
|
+
* (`src/workflows/schema-validation.ts`), so requiring it refuses nothing the
|
|
6268
|
+
* platform accepts at run time.
|
|
6269
|
+
*/
|
|
6270
|
+
function discriminatedOneOf(node, members) {
|
|
6271
|
+
const objects = members.filter((member) => isPlainSchemaObject(member) && member.type === "object");
|
|
6272
|
+
if (objects.length !== members.length || members.length < 2) {
|
|
6273
|
+
return {
|
|
6274
|
+
ok: false,
|
|
6275
|
+
reason: "Gemini's function Schema has no oneOf. Only a DISCRIMINATED oneOf can " +
|
|
6276
|
+
"be rewritten as anyOf without widening it, and that needs at least two " +
|
|
6277
|
+
'members, every one of them type = "object"',
|
|
6278
|
+
};
|
|
6279
|
+
}
|
|
6280
|
+
const sibling = ONEOF_SIBLING_KEYWORDS.filter((key) => Object.prototype.hasOwnProperty.call(node, key));
|
|
6281
|
+
if (sibling.length > 0) {
|
|
6282
|
+
return {
|
|
6283
|
+
ok: false,
|
|
6284
|
+
reason: "Gemini's function Schema has no oneOf, and this one is not a clean " +
|
|
6285
|
+
`discriminated union: it carries the sibling constraint${sibling.length > 1 ? "s" : ""} ` +
|
|
6286
|
+
`${sibling.join(", ")}. Move the shared constraints into each member`,
|
|
6287
|
+
};
|
|
6288
|
+
}
|
|
6289
|
+
if (Object.prototype.hasOwnProperty.call(node, "anyOf")) {
|
|
6290
|
+
return {
|
|
6291
|
+
ok: false,
|
|
6292
|
+
reason: "Gemini's function Schema has no oneOf, and this node already carries " +
|
|
6293
|
+
"an anyOf the rewrite would replace. Declare one or the other",
|
|
6294
|
+
};
|
|
6295
|
+
}
|
|
6296
|
+
const tags = objects.map(memberTagLiterals);
|
|
6297
|
+
const candidates = [];
|
|
6298
|
+
for (const name of tags[0].keys()) {
|
|
6299
|
+
if (!tags.every((map) => map.has(name)))
|
|
6300
|
+
continue;
|
|
6301
|
+
const literals = tags.map((map) => map.get(name));
|
|
6302
|
+
const distinct = new Set(literals.map((literal) => JSON.stringify(literal)));
|
|
6303
|
+
if (distinct.size === literals.length)
|
|
6304
|
+
candidates.push(name);
|
|
6305
|
+
}
|
|
6306
|
+
if (candidates.length !== 1) {
|
|
6307
|
+
return {
|
|
6308
|
+
ok: false,
|
|
6309
|
+
reason: candidates.length === 0
|
|
6310
|
+
? "Gemini's function Schema has no oneOf, and this one is not " +
|
|
6311
|
+
"discriminated: every member must share exactly one property " +
|
|
6312
|
+
"declared as a distinct single-literal enum (or const)"
|
|
6313
|
+
: "Gemini's function Schema has no oneOf, and this one's " +
|
|
6314
|
+
`discriminant is ambiguous: ${candidates.join(", ")} all qualify. ` +
|
|
6315
|
+
"Exactly one is required",
|
|
6316
|
+
};
|
|
6317
|
+
}
|
|
6318
|
+
const discriminant = candidates[0];
|
|
6319
|
+
const omitting = objects
|
|
6320
|
+
.map((member, index) => Array.isArray(member.required) && member.required.includes(discriminant)
|
|
6321
|
+
? null
|
|
6322
|
+
: index)
|
|
6323
|
+
.filter((index) => index !== null);
|
|
6324
|
+
if (omitting.length > 0) {
|
|
6325
|
+
return {
|
|
6326
|
+
ok: false,
|
|
6327
|
+
reason: `Gemini's function Schema has no oneOf. Rewriting this one as anyOf ` +
|
|
6328
|
+
`would widen it, because member${omitting.length > 1 ? "s" : ""} ` +
|
|
6329
|
+
`${omitting.join(", ")} do${omitting.length > 1 ? "" : "es"} not list ` +
|
|
6330
|
+
`the discriminant "${discriminant}" under required, so a value with no ` +
|
|
6331
|
+
`"${discriminant}" would match every member. List "${discriminant}" ` +
|
|
6332
|
+
"under required in every member",
|
|
6333
|
+
};
|
|
6334
|
+
}
|
|
6335
|
+
return { ok: true, discriminant };
|
|
6336
|
+
}
|
|
6337
|
+
/**
|
|
6338
|
+
* Translate one node into Gemini's `Schema`, appending every refusal it holds
|
|
6339
|
+
* to `out`. Returns the translated node, or `null` when the node itself is not
|
|
6340
|
+
* a schema object.
|
|
6341
|
+
*/
|
|
6342
|
+
function translateGeminiNode(node, path, out) {
|
|
6343
|
+
if (!isPlainSchemaObject(node)) {
|
|
6344
|
+
out.push({
|
|
6345
|
+
path,
|
|
6346
|
+
keyword: "(schema)",
|
|
6347
|
+
reason: "is not a JSON Schema object; Gemini's function Schema needs a table " +
|
|
6348
|
+
"declaring at least a type",
|
|
6349
|
+
});
|
|
6350
|
+
return null;
|
|
6351
|
+
}
|
|
6352
|
+
const result = {};
|
|
6353
|
+
/** Faults raised AT THIS NODE — nested ones must not suppress its own rules. */
|
|
6354
|
+
let nodeFaults = 0;
|
|
6355
|
+
const refuse = (keyword, reason) => {
|
|
6356
|
+
nodeFaults += 1;
|
|
6357
|
+
out.push({ path, keyword, reason });
|
|
6358
|
+
};
|
|
6359
|
+
// ── Pass one: the tables, in the author's own key order ────────────────
|
|
6360
|
+
const deferred = new Set();
|
|
6361
|
+
for (const key of Object.keys(node)) {
|
|
6362
|
+
if (GEMINI_DROPPED_KEYWORDS.includes(key))
|
|
6363
|
+
continue;
|
|
6364
|
+
if (key === "additionalProperties") {
|
|
6365
|
+
// Dropped for `false` and `true` alike: Gemini has no closed-object
|
|
6366
|
+
// spelling, and the platform validates the ORIGINAL schema (DSO-3799-001).
|
|
6367
|
+
const value = node[key];
|
|
6368
|
+
if (value === false || value === true)
|
|
6369
|
+
continue;
|
|
6370
|
+
refuse(key, isPlainSchemaObject(value)
|
|
6371
|
+
? "Gemini's function Schema has no additionalProperties, so a schema " +
|
|
6372
|
+
"for the extra properties cannot be carried — name the properties"
|
|
6373
|
+
: "additionalProperties must be true or false (Gemini carries neither, " +
|
|
6374
|
+
"but the platform validates the original schema)");
|
|
6375
|
+
continue;
|
|
6376
|
+
}
|
|
6377
|
+
if (Object.prototype.hasOwnProperty.call(GEMINI_REFUSED_KEYWORDS, key)) {
|
|
6378
|
+
refuse(key, GEMINI_REFUSED_KEYWORDS[key]);
|
|
6379
|
+
continue;
|
|
6380
|
+
}
|
|
6381
|
+
if (GEMINI_PASS_KEYWORDS.includes(key)) {
|
|
6382
|
+
result[key] = schemaCopy(node[key]);
|
|
6383
|
+
continue;
|
|
6384
|
+
}
|
|
6385
|
+
if (GEMINI_STRUCTURAL_KEYWORDS.includes(key)) {
|
|
6386
|
+
deferred.add(key);
|
|
6387
|
+
continue;
|
|
6388
|
+
}
|
|
6389
|
+
// Fail closed: a keyword nobody classified could mean anything.
|
|
6390
|
+
refuse(key, "is not a keyword Gemini's function Schema carries, and dropping a " +
|
|
6391
|
+
"keyword whose meaning the platform does not know could widen what the " +
|
|
6392
|
+
"model may produce");
|
|
6393
|
+
}
|
|
6394
|
+
// ── Pass two: the structural keywords, in a fixed order ────────────────
|
|
6395
|
+
if (deferred.has("enum")) {
|
|
6396
|
+
const values = node.enum;
|
|
6397
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
6398
|
+
refuse("enum", "must be a non-empty list of strings");
|
|
6399
|
+
}
|
|
6400
|
+
else if (values.every((value) => typeof value === "string")) {
|
|
6401
|
+
result.enum = schemaCopy(values);
|
|
6402
|
+
}
|
|
6403
|
+
else {
|
|
6404
|
+
refuse("enum", "Gemini's function Schema takes only STRING enum values; write the " +
|
|
6405
|
+
"allowed values as strings, or state the constraint in description");
|
|
6406
|
+
}
|
|
6407
|
+
}
|
|
6408
|
+
if (deferred.has("const")) {
|
|
6409
|
+
const value = node.const;
|
|
6410
|
+
if (typeof value !== "string") {
|
|
6411
|
+
refuse("const", "Gemini's function Schema has no const, and only a string one can be " +
|
|
6412
|
+
'rewritten as enum: ["…"]');
|
|
6413
|
+
}
|
|
6414
|
+
else if (deferred.has("enum")) {
|
|
6415
|
+
refuse("const", "Gemini's function Schema has no const; it is rewritten as a " +
|
|
6416
|
+
"single-literal enum, which this node already declares. Keep one of the two");
|
|
6417
|
+
}
|
|
6418
|
+
else {
|
|
6419
|
+
result.enum = [value];
|
|
6420
|
+
}
|
|
6421
|
+
}
|
|
6422
|
+
if (deferred.has("format")) {
|
|
6423
|
+
const value = node.format;
|
|
6424
|
+
if (typeof value === "string" &&
|
|
6425
|
+
GEMINI_FORMATS.includes(value)) {
|
|
6426
|
+
result.format = value;
|
|
6427
|
+
}
|
|
6428
|
+
else {
|
|
6429
|
+
refuse("format", `Gemini's function Schema documents only ${GEMINI_FORMATS.join(", ")}; ` +
|
|
6430
|
+
"state anything else in description");
|
|
6431
|
+
}
|
|
6432
|
+
}
|
|
6433
|
+
/** A type union to rewrite as `anyOf`, once its members' keywords are translated. */
|
|
6434
|
+
let unionTypes = null;
|
|
6435
|
+
if (deferred.has("type")) {
|
|
6436
|
+
const typeSpec = node.type;
|
|
6437
|
+
if (typeof typeSpec === "string") {
|
|
6438
|
+
if (GEMINI_TYPES.includes(typeSpec)) {
|
|
6439
|
+
result.type = typeSpec;
|
|
6440
|
+
}
|
|
6441
|
+
else {
|
|
6442
|
+
refuse("type", `Gemini's function Schema takes one of ${GEMINI_TYPES.join(", ")}; ` +
|
|
6443
|
+
`found ${JSON.stringify(typeSpec)}` +
|
|
6444
|
+
(typeSpec === "null"
|
|
6445
|
+
? '. A nullable value is spelled type: [T, "null"]'
|
|
6446
|
+
: ""));
|
|
6447
|
+
}
|
|
6448
|
+
}
|
|
6449
|
+
else if (Array.isArray(typeSpec)) {
|
|
6450
|
+
const nullable = typeSpec.includes("null");
|
|
6451
|
+
const named = typeSpec.filter((entry) => entry !== "null");
|
|
6452
|
+
const unknown = named.filter((entry) => typeof entry !== "string" ||
|
|
6453
|
+
!GEMINI_TYPES.includes(entry));
|
|
6454
|
+
if (unknown.length > 0) {
|
|
6455
|
+
refuse("type", `Gemini's function Schema takes one of ${GEMINI_TYPES.join(", ")}; ` +
|
|
6456
|
+
`found ${unknown.map((entry) => JSON.stringify(entry)).join(", ")}`);
|
|
6457
|
+
}
|
|
6458
|
+
else if (named.length === 0) {
|
|
6459
|
+
refuse("type", "Gemini's function Schema has no null type on its own; a nullable " +
|
|
6460
|
+
'value is spelled type: [T, "null"]');
|
|
6461
|
+
}
|
|
6462
|
+
else if (named.length === 1) {
|
|
6463
|
+
result.type = named[0];
|
|
6464
|
+
if (nullable)
|
|
6465
|
+
result.nullable = true;
|
|
6466
|
+
}
|
|
6467
|
+
else if (deferred.has("anyOf")) {
|
|
6468
|
+
refuse("type", "a type union is rewritten as anyOf of the single types, which this " +
|
|
6469
|
+
"node already declares. Write the union inside the anyOf instead");
|
|
6470
|
+
}
|
|
6471
|
+
else {
|
|
6472
|
+
unionTypes = named;
|
|
6473
|
+
if (nullable)
|
|
6474
|
+
result.nullable = true;
|
|
6475
|
+
}
|
|
6476
|
+
}
|
|
6477
|
+
else {
|
|
6478
|
+
refuse("type", `must be a type name or a list of them; found ${typeof typeSpec}`);
|
|
6479
|
+
}
|
|
6480
|
+
}
|
|
6481
|
+
if (deferred.has("properties")) {
|
|
6482
|
+
const properties = node.properties;
|
|
6483
|
+
if (!isPlainSchemaObject(properties)) {
|
|
6484
|
+
refuse("properties", "must be a table of property name to schema");
|
|
6485
|
+
}
|
|
6486
|
+
else {
|
|
6487
|
+
const translated = {};
|
|
6488
|
+
for (const [name, sub] of Object.entries(properties)) {
|
|
6489
|
+
const child = translateGeminiNode(sub, childPath(path, `properties.${name}`), out);
|
|
6490
|
+
// Defined, not assigned: a JSON-parsed schema may name a property
|
|
6491
|
+
// `__proto__`, and assigning it would set the prototype and drop the
|
|
6492
|
+
// property from what Gemini is sent (CSO-3799-006).
|
|
6493
|
+
if (child) {
|
|
6494
|
+
Object.defineProperty(translated, name, {
|
|
6495
|
+
value: child,
|
|
6496
|
+
enumerable: true,
|
|
6497
|
+
writable: true,
|
|
6498
|
+
configurable: true,
|
|
6499
|
+
});
|
|
6500
|
+
}
|
|
6501
|
+
}
|
|
6502
|
+
result.properties = translated;
|
|
6503
|
+
}
|
|
6504
|
+
}
|
|
6505
|
+
if (deferred.has("items")) {
|
|
6506
|
+
const child = translateGeminiNode(node.items, childPath(path, "items"), out);
|
|
6507
|
+
if (child)
|
|
6508
|
+
result.items = child;
|
|
6509
|
+
}
|
|
6510
|
+
if (deferred.has("anyOf")) {
|
|
6511
|
+
const members = node.anyOf;
|
|
6512
|
+
if (!Array.isArray(members) || members.length === 0) {
|
|
6513
|
+
refuse("anyOf", "must be a non-empty list of schemas");
|
|
6514
|
+
}
|
|
6515
|
+
else {
|
|
6516
|
+
const translated = [];
|
|
6517
|
+
members.forEach((member, index) => {
|
|
6518
|
+
const child = translateGeminiNode(member, childPath(path, `anyOf[${index}]`), out);
|
|
6519
|
+
if (child)
|
|
6520
|
+
translated.push(child);
|
|
6521
|
+
});
|
|
6522
|
+
result.anyOf = translated;
|
|
6523
|
+
}
|
|
6524
|
+
}
|
|
6525
|
+
if (deferred.has("oneOf")) {
|
|
6526
|
+
const members = node.oneOf;
|
|
6527
|
+
if (!Array.isArray(members)) {
|
|
6528
|
+
refuse("oneOf", "must be a list of schemas");
|
|
6529
|
+
}
|
|
6530
|
+
else {
|
|
6531
|
+
const verdict = discriminatedOneOf(node, members);
|
|
6532
|
+
if (verdict.ok === false) {
|
|
6533
|
+
refuse("oneOf", verdict.reason);
|
|
6534
|
+
}
|
|
6535
|
+
else {
|
|
6536
|
+
// A discriminated oneOf whose members all require their tag is mutually
|
|
6537
|
+
// exclusive by construction, so anyOf of the same members admits
|
|
6538
|
+
// exactly the same values (DSO-3799-003).
|
|
6539
|
+
const translated = [];
|
|
6540
|
+
members.forEach((member, index) => {
|
|
6541
|
+
const child = translateGeminiNode(member, childPath(path, `oneOf[${index}]`), out);
|
|
6542
|
+
if (child)
|
|
6543
|
+
translated.push(child);
|
|
6544
|
+
});
|
|
6545
|
+
result.anyOf = translated;
|
|
6546
|
+
}
|
|
6547
|
+
}
|
|
6548
|
+
}
|
|
6549
|
+
// A type union becomes `anyOf` of the single types, each member carrying the
|
|
6550
|
+
// keywords that constrain its type (CSO-3799-003): `{ type: ["object",
|
|
6551
|
+
// "string"], properties }` means an object WITH those properties, or a
|
|
6552
|
+
// string. Everything else stays on the node, beside the `anyOf`.
|
|
6553
|
+
if (unionTypes) {
|
|
6554
|
+
const members = unionTypes.map((type) => {
|
|
6555
|
+
const member = { type };
|
|
6556
|
+
for (const key of GEMINI_UNION_MEMBER_KEYWORDS[type] ?? []) {
|
|
6557
|
+
if (result[key] !== undefined)
|
|
6558
|
+
member[key] = result[key];
|
|
6559
|
+
}
|
|
6560
|
+
return member;
|
|
6561
|
+
});
|
|
6562
|
+
for (const type of unionTypes) {
|
|
6563
|
+
for (const key of GEMINI_UNION_MEMBER_KEYWORDS[type] ?? []) {
|
|
6564
|
+
delete result[key];
|
|
6565
|
+
}
|
|
6566
|
+
}
|
|
6567
|
+
result.anyOf = members;
|
|
6568
|
+
}
|
|
6569
|
+
// ── Pass three: what Gemini requires of a complete node ────────────────
|
|
6570
|
+
//
|
|
6571
|
+
// Only when this node raised no fault of its own: a node refused for `$ref`
|
|
6572
|
+
// or `allOf` would otherwise be refused a second time for the type that
|
|
6573
|
+
// keyword was carrying.
|
|
6574
|
+
if (nodeFaults === 0) {
|
|
6575
|
+
const hasBranches = deferred.has("anyOf") || deferred.has("oneOf");
|
|
6576
|
+
// A STRING enum carries its own type — every value it admits is a string —
|
|
6577
|
+
// so a node declaring one needs no `type` beside it. That is what keeps the
|
|
6578
|
+
// platform's own discriminated-union dialect inside Gemini's reach: a tag is
|
|
6579
|
+
// spelled `{ enum: ["cash"] }` with no type (`schema-descriptor.ts`), and
|
|
6580
|
+
// Gemini accepts exactly that on a `functionDeclarations[].parameters`
|
|
6581
|
+
// property (verified live, #3799 behavior 23). `result.enum` also covers a
|
|
6582
|
+
// string `const` rewritten above.
|
|
6583
|
+
const hasEnum = result.enum !== undefined;
|
|
6584
|
+
if (!deferred.has("type") && !hasBranches && !hasEnum) {
|
|
6585
|
+
refuse("type", "Gemini's function Schema needs a type on every schema (one of " +
|
|
6586
|
+
`${GEMINI_TYPES.join(", ")}), or an anyOf of typed members`);
|
|
6587
|
+
}
|
|
6588
|
+
else if (result.type === "object") {
|
|
6589
|
+
const properties = result.properties;
|
|
6590
|
+
if (!isPlainSchemaObject(properties) || Object.keys(properties).length === 0) {
|
|
6591
|
+
refuse("properties", "Gemini needs the object's properties: its function Schema has no " +
|
|
6592
|
+
"free-form object, and the existing clean-up rewrote one as a " +
|
|
6593
|
+
"string, which changes what the model may produce. Name the " +
|
|
6594
|
+
"properties the model may send");
|
|
6595
|
+
}
|
|
6596
|
+
}
|
|
6597
|
+
else if (result.type === "array" && result.items === undefined) {
|
|
6598
|
+
refuse("items", "Gemini needs the array's items: its function Schema has no untyped array");
|
|
6599
|
+
}
|
|
6600
|
+
// The members a type union was rewritten into are held to the same rule
|
|
6601
|
+
// as a single-typed node, so a union cannot carry the free-form object or
|
|
6602
|
+
// the untyped array past it (CSO-3799-003).
|
|
6603
|
+
if (unionTypes) {
|
|
6604
|
+
const members = result.anyOf;
|
|
6605
|
+
for (const member of members) {
|
|
6606
|
+
if (member.type === "object" &&
|
|
6607
|
+
(!isPlainSchemaObject(member.properties) ||
|
|
6608
|
+
Object.keys(member.properties).length === 0)) {
|
|
6609
|
+
refuse("properties", "Gemini needs the object's properties, for the object member of " +
|
|
6610
|
+
"this type union too: its function Schema has no free-form " +
|
|
6611
|
+
"object. Name the properties the model may send");
|
|
6612
|
+
}
|
|
6613
|
+
else if (member.type === "array" && member.items === undefined) {
|
|
6614
|
+
refuse("items", "Gemini needs the array's items, for the array member of this type " +
|
|
6615
|
+
"union too: its function Schema has no untyped array");
|
|
6616
|
+
}
|
|
6617
|
+
}
|
|
6618
|
+
}
|
|
6619
|
+
}
|
|
6620
|
+
return result;
|
|
6621
|
+
}
|
|
6622
|
+
/**
|
|
6623
|
+
* Translate one schema for one provider, or refuse it naming every keyword it
|
|
6624
|
+
* cannot carry.
|
|
6625
|
+
*
|
|
6626
|
+
* OpenRouter is identity — a deep copy of the input, so a caller that edits the
|
|
6627
|
+
* result never reaches the stored original. Gemini follows the table in spec
|
|
6628
|
+
* §4. Every refusal of one schema is returned at once, so an author fixes it in
|
|
6629
|
+
* one pass (principle 6).
|
|
6630
|
+
*/
|
|
6631
|
+
export function translateSchemaForProvider(schema, provider) {
|
|
6632
|
+
if (provider !== "gemini") {
|
|
6633
|
+
// Identity: the request carries the schema as declared and the model's
|
|
6634
|
+
// upstream owns its dialect.
|
|
6635
|
+
if (!isPlainSchemaObject(schema)) {
|
|
6636
|
+
return {
|
|
6637
|
+
ok: false,
|
|
6638
|
+
refusals: [
|
|
6639
|
+
{
|
|
6640
|
+
path: "",
|
|
6641
|
+
keyword: "(schema)",
|
|
6642
|
+
reason: "is not a JSON Schema object",
|
|
6643
|
+
},
|
|
6644
|
+
],
|
|
6645
|
+
};
|
|
6646
|
+
}
|
|
6647
|
+
return { ok: true, schema: schemaCopy(schema) };
|
|
6648
|
+
}
|
|
6649
|
+
const refusals = [];
|
|
6650
|
+
const translated = translateGeminiNode(schema, "", refusals);
|
|
6651
|
+
if (refusals.length > 0)
|
|
6652
|
+
return { ok: false, refusals };
|
|
6653
|
+
return { ok: true, schema: translated };
|
|
6654
|
+
}
|
|
6655
|
+
/** `{ gemini: ["default", "slow"] }` — the non-archived configs, by provider. */
|
|
6656
|
+
function configsByProvider(configs) {
|
|
6657
|
+
const order = [];
|
|
6658
|
+
const names = new Map();
|
|
6659
|
+
for (const config of configs) {
|
|
6660
|
+
if (String(config?.status ?? "active") === "archived")
|
|
6661
|
+
continue;
|
|
6662
|
+
const provider = String(config?.provider ?? "").toLowerCase();
|
|
6663
|
+
if (!provider)
|
|
6664
|
+
continue;
|
|
6665
|
+
if (!names.has(provider)) {
|
|
6666
|
+
names.set(provider, []);
|
|
6667
|
+
order.push(provider);
|
|
6668
|
+
}
|
|
6669
|
+
names.get(provider).push(String(config.configName ?? ""));
|
|
6670
|
+
}
|
|
6671
|
+
return order.map((provider) => ({
|
|
6672
|
+
provider,
|
|
6673
|
+
configNames: names.get(provider),
|
|
6674
|
+
}));
|
|
6675
|
+
}
|
|
6676
|
+
/** A stored schema value: an object as it is, a JSON string parsed. */
|
|
6677
|
+
function readStoredSchema(value) {
|
|
6678
|
+
if (value === undefined || value === null)
|
|
6679
|
+
return null;
|
|
6680
|
+
if (typeof value === "string") {
|
|
6681
|
+
if (value.trim() === "")
|
|
6682
|
+
return null;
|
|
6683
|
+
try {
|
|
6684
|
+
return { ok: true, schema: JSON.parse(value) };
|
|
6685
|
+
}
|
|
6686
|
+
catch {
|
|
6687
|
+
return { ok: false };
|
|
6688
|
+
}
|
|
6689
|
+
}
|
|
6690
|
+
return { ok: true, schema: value };
|
|
6691
|
+
}
|
|
6692
|
+
/**
|
|
6693
|
+
* Every schema of an agent that the model reads: each tool's `inputSchema` and
|
|
6694
|
+
* `outputSchema` (from the TOML for a client tool, from the function's own
|
|
6695
|
+
* schemas for a server tool) and each event's `schema`.
|
|
6696
|
+
*/
|
|
6697
|
+
function agentSchemaSubjects(declaration, functions) {
|
|
6698
|
+
const subjects = [];
|
|
6699
|
+
const tools = Array.isArray(declaration.tools) ? declaration.tools : [];
|
|
6700
|
+
tools.forEach((tool) => {
|
|
6701
|
+
if (!isPlainSchemaObject(tool))
|
|
6702
|
+
return;
|
|
6703
|
+
const name = typeof tool.name === "string" ? tool.name : "(unnamed)";
|
|
6704
|
+
const functionKey = typeof tool.function === "string" && tool.function.trim() !== ""
|
|
6705
|
+
? tool.function.trim()
|
|
6706
|
+
: null;
|
|
6707
|
+
if (functionKey === null) {
|
|
6708
|
+
// A client tool: its schemas are in the declaration.
|
|
6709
|
+
for (const which of ["inputSchema", "outputSchema"]) {
|
|
6710
|
+
if (tool[which] === undefined)
|
|
6711
|
+
continue;
|
|
6712
|
+
subjects.push({ subject: `tool "${name}"`, which, schema: tool[which] });
|
|
6713
|
+
}
|
|
6714
|
+
return;
|
|
6715
|
+
}
|
|
6716
|
+
// A server tool: its schemas are its function's. `undefined` facts are the
|
|
6717
|
+
// function-missing and schema-missing refusals of #3798, not this rule's.
|
|
6718
|
+
if (!functions)
|
|
6719
|
+
return;
|
|
6720
|
+
const facts = Object.prototype.hasOwnProperty.call(functions, functionKey)
|
|
6721
|
+
? functions[functionKey]
|
|
6722
|
+
: null;
|
|
6723
|
+
if (!facts)
|
|
6724
|
+
return;
|
|
6725
|
+
const subject = `tool "${name}" (function "${functionKey}")`;
|
|
6726
|
+
for (const which of ["inputSchema", "outputSchema"]) {
|
|
6727
|
+
const read = readStoredSchema(which === "inputSchema" ? facts.inputSchema : facts.outputSchema);
|
|
6728
|
+
if (read === null)
|
|
6729
|
+
continue;
|
|
6730
|
+
if (read.ok === false) {
|
|
6731
|
+
subjects.push({ subject, which, unparsable: functionKey });
|
|
6732
|
+
continue;
|
|
6733
|
+
}
|
|
6734
|
+
subjects.push({ subject, which, schema: read.schema });
|
|
6735
|
+
}
|
|
6736
|
+
});
|
|
6737
|
+
const events = Array.isArray(declaration.events) ? declaration.events : [];
|
|
6738
|
+
events.forEach((event) => {
|
|
6739
|
+
if (!isPlainSchemaObject(event))
|
|
6740
|
+
return;
|
|
6741
|
+
if (event.schema === undefined)
|
|
6742
|
+
return;
|
|
6743
|
+
const name = typeof event.name === "string" ? event.name : "(unnamed)";
|
|
6744
|
+
subjects.push({
|
|
6745
|
+
subject: `event "${name}"`,
|
|
6746
|
+
which: "schema",
|
|
6747
|
+
schema: event.schema,
|
|
6748
|
+
});
|
|
6749
|
+
});
|
|
6750
|
+
return subjects;
|
|
6751
|
+
}
|
|
6752
|
+
/** `config "default"` / `configs "default", "slow"`. */
|
|
6753
|
+
function configList(configNames) {
|
|
6754
|
+
const quoted = configNames.map((name) => `"${name}"`).join(", ");
|
|
6755
|
+
return `${configNames.length === 1 ? "config" : "configs"} ${quoted}`;
|
|
6756
|
+
}
|
|
6757
|
+
/**
|
|
6758
|
+
* Every `AGENT_SCHEMA_NOT_TRANSLATABLE` refusal an agent's declaration owes,
|
|
6759
|
+
* checked against the provider of every config that is not archived
|
|
6760
|
+
* (intent criterion 2).
|
|
6761
|
+
*
|
|
6762
|
+
* One refusal per (schema, path, keyword, provider): it names the tool or
|
|
6763
|
+
* event, which schema, the dotted path, the keyword, the provider, the config
|
|
6764
|
+
* names on that provider, and the remedy. Two configs on the same provider
|
|
6765
|
+
* produce ONE refusal naming both — the author's fix is the same either way.
|
|
6766
|
+
*
|
|
6767
|
+
* A declaration with no tools and no events reads no function schema at all.
|
|
6768
|
+
*/
|
|
6769
|
+
export function agentSchemaTranslationRefusals(input) {
|
|
6770
|
+
const declaration = parseAgentDeclarationBlock(input.declaration);
|
|
6771
|
+
if (!declaration)
|
|
6772
|
+
return [];
|
|
6773
|
+
const providers = configsByProvider(input.configs ?? []);
|
|
6774
|
+
if (providers.length === 0)
|
|
6775
|
+
return [];
|
|
6776
|
+
const subjects = agentSchemaSubjects(declaration, input.functions);
|
|
6777
|
+
if (subjects.length === 0)
|
|
6778
|
+
return [];
|
|
6779
|
+
const out = [];
|
|
6780
|
+
for (const subject of subjects) {
|
|
6781
|
+
if (subject.unparsable !== undefined) {
|
|
6782
|
+
out.push({
|
|
6783
|
+
code: "AGENT_SCHEMA_NOT_TRANSLATABLE",
|
|
6784
|
+
message: `${AGENT_BLOCK_LABEL} ${subject.subject} ${subject.which} is not a ` +
|
|
6785
|
+
`JSON Schema the platform can read, so it cannot be translated for ` +
|
|
6786
|
+
`any provider. Fix ${subject.which} in ` +
|
|
6787
|
+
`functions/${subject.unparsable}.toml`,
|
|
6788
|
+
});
|
|
6789
|
+
continue;
|
|
6790
|
+
}
|
|
6791
|
+
for (const { provider, configNames } of providers) {
|
|
6792
|
+
const result = translateSchemaForProvider(subject.schema, provider);
|
|
6793
|
+
if (result.ok === true)
|
|
6794
|
+
continue;
|
|
6795
|
+
for (const refusal of result.refusals) {
|
|
6796
|
+
out.push({
|
|
6797
|
+
code: "AGENT_SCHEMA_NOT_TRANSLATABLE",
|
|
6798
|
+
message: `${AGENT_BLOCK_LABEL} ${subject.subject} ${subject.which} at ` +
|
|
6799
|
+
`${schemaPathLabel(refusal.path)}: ${refusal.keyword} cannot be ` +
|
|
6800
|
+
`translated for ${provider} without losing meaning — ` +
|
|
6801
|
+
`${refusal.reason}. ${configList(configNames)} run on ${provider}; ` +
|
|
6802
|
+
`rewrite the schema, or archive every ${provider} config of this prompt`,
|
|
6803
|
+
});
|
|
6804
|
+
}
|
|
6805
|
+
}
|
|
6806
|
+
}
|
|
6807
|
+
return out;
|
|
6808
|
+
}
|
|
6809
|
+
// ── src/config-surface/prompt.ts ─────────────────────────────────────────
|
|
6810
|
+
/**
|
|
6811
|
+
* The `prompt` configuration object's definition (issue #2644, phase 2).
|
|
6812
|
+
*
|
|
6813
|
+
* `prompts/<key>.toml` carries TWO field tables: the `[prompt]` table backed by
|
|
6814
|
+
* `AppPrompt`, and the repeated `[[configs]]` table backed by `AppPromptConfig`
|
|
6815
|
+
* — one entry per named LLM configuration. Both are declared here, so a field
|
|
6816
|
+
* added to either model is covered by the same guard.
|
|
6817
|
+
*
|
|
6818
|
+
* The `[[configs]]` entries are the reason prompts have two tables rather than
|
|
6819
|
+
* a `structural` note: they are a real field surface the CLI pushes
|
|
6820
|
+
* (`createPromptConfig` / `updatePromptConfig`) and pulls, not an opaque blob.
|
|
6821
|
+
*/
|
|
6822
|
+
const PROMPT_HANDLER = "src/admin-api.ts";
|
|
6823
|
+
/**
|
|
6824
|
+
* #3798 — the blocks a model setting both kinds use is authored under: the
|
|
6825
|
+
* chat block, and the agent block that is the chat block less its template
|
|
6826
|
+
* and output keys (`AGENT_CONFIG_KEYS`).
|
|
6827
|
+
*/
|
|
6828
|
+
const SHARED_WITH_AGENT = ["chat", "agent"];
|
|
6829
|
+
/**
|
|
6830
|
+
* #3626 §4 — the nine chat keys, still accepted FLAT under `[[configs]]`.
|
|
6831
|
+
*
|
|
6832
|
+
* The blob-bucket `accessPolicy` precedent: declared `deprecated`, so
|
|
6833
|
+
* `config push` keeps taking a file written before the `[configs.chat]` block
|
|
6834
|
+
* existed, while `config pull` never writes the spelling again. One
|
|
6835
|
+
* pull-then-push migrates a file. `hoistDeprecatedChatKeys` moves them into the
|
|
6836
|
+
* group before the payload is built; rejecting the flat spelling outright is
|
|
6837
|
+
* #3642. #3801 — only the nine: a chat key added later (`strictOutput`) was
|
|
6838
|
+
* never written flat, so it has no deprecated spelling to accept.
|
|
6839
|
+
*/
|
|
6840
|
+
const DEPRECATED_FLAT_CHAT_KEYS = Object.fromEntries(LEGACY_FLAT_CHAT_KEYS.map((key) => [
|
|
6841
|
+
key,
|
|
6842
|
+
{
|
|
6843
|
+
kind: "deprecated",
|
|
6844
|
+
note: `#3626 — the pre-kind spelling of \`[configs.chat].${key}\`. Still ` +
|
|
6845
|
+
"accepted flat so no existing prompt file breaks (push hoists it " +
|
|
6846
|
+
"into the block and warns); `config pull` writes only the grouped " +
|
|
6847
|
+
"spelling, so one pull-then-push migrates the file. Rejecting the " +
|
|
6848
|
+
"flat spelling is #3642.",
|
|
6849
|
+
},
|
|
6850
|
+
]));
|
|
6851
|
+
export const PROMPT_SURFACE = {
|
|
6852
|
+
label: "prompt",
|
|
6853
|
+
tables: [
|
|
6854
|
+
{
|
|
6855
|
+
tomlPath: ["prompt"],
|
|
6856
|
+
repeated: false,
|
|
6857
|
+
model: "AppPrompt",
|
|
6858
|
+
// Declaration order reproduces the pre-#2644 serializer's key order.
|
|
6859
|
+
fields: [
|
|
6860
|
+
// #3626 — WHAT this prompt runs. `chat` is today's prompt (a chat
|
|
6861
|
+
// completion rendered from templates); `decisions` names OpenRouter's
|
|
6862
|
+
// `/api/alpha/decisions` endpoint, which takes a `state` value plus
|
|
6863
|
+
// named typed questions. It lives on the PROMPT because it decides the
|
|
6864
|
+
// input envelope, the output type, the meaning of `inputSchema` /
|
|
6865
|
+
// `outputSchema` and the shape of every test case's `inputVariables` —
|
|
6866
|
+
// all of which are asserted per prompt KEY.
|
|
6867
|
+
//
|
|
6868
|
+
// `emit: "always"` so a pulled file says which kind it is; an omitted
|
|
6869
|
+
// key is `chat`, so no file written before this changes meaning, and
|
|
6870
|
+
// `serverDefault` is what lets push send `chat` for an absent key and
|
|
6871
|
+
// `config diff` read both spellings as one value.
|
|
6872
|
+
//
|
|
6873
|
+
// CREATE_ONLY, and immutable in the strong sense (D3626-008): changing
|
|
6874
|
+
// a prompt's kind under its key would change the contract of every
|
|
6875
|
+
// caller compiled against it, so the update handler never reads it and
|
|
6876
|
+
// the push gate declines the change as `immutable` — the `ttlTier`
|
|
6877
|
+
// precedent. The prompt comparator projects `[prompt]` in CREATE mode
|
|
6878
|
+
// for exactly that reason (SO3626-008).
|
|
6879
|
+
{
|
|
6880
|
+
field: "kind",
|
|
6881
|
+
tomlKey: "kind",
|
|
6882
|
+
type: "string",
|
|
6883
|
+
emit: "always",
|
|
6884
|
+
writableOn: CREATE_ONLY,
|
|
6885
|
+
serverDefault: "chat",
|
|
6886
|
+
validation: handledBy("src/config-surface/prompt-kinds.ts", "resolvePromptKind", "apply"),
|
|
6887
|
+
},
|
|
6888
|
+
{
|
|
6889
|
+
field: "displayName",
|
|
6890
|
+
tomlKey: "displayName",
|
|
6891
|
+
type: "string",
|
|
6892
|
+
emit: "always",
|
|
6893
|
+
writableOn: BOTH,
|
|
6894
|
+
validation: "passthrough",
|
|
6895
|
+
},
|
|
6896
|
+
{
|
|
6897
|
+
field: "description",
|
|
6898
|
+
tomlKey: "description",
|
|
6899
|
+
type: "string",
|
|
6900
|
+
emit: "whenSet",
|
|
6901
|
+
writableOn: BOTH,
|
|
6902
|
+
validation: "passthrough",
|
|
6903
|
+
},
|
|
6904
|
+
// JSON Schema objects. Stored as JSON strings, returned parsed; TOML
|
|
6905
|
+
// may present them as an inline table or a JSON string.
|
|
6906
|
+
{
|
|
6907
|
+
field: "inputSchema",
|
|
6908
|
+
tomlKey: "inputSchema",
|
|
6909
|
+
type: "json",
|
|
6910
|
+
emit: "whenSet",
|
|
6911
|
+
writableOn: BOTH,
|
|
6912
|
+
validation: handledBy(PROMPT_HANDLER, "AdminAPI", "apply"),
|
|
6913
|
+
},
|
|
6914
|
+
// #2644 — accepted by both handlers and returned by the detail, but
|
|
6915
|
+
// never written to TOML before this: a prompt's declared output shape
|
|
6916
|
+
// was invisible to config-as-code.
|
|
6917
|
+
{
|
|
6918
|
+
field: "outputSchema",
|
|
6919
|
+
tomlKey: "outputSchema",
|
|
6920
|
+
type: "json",
|
|
6921
|
+
emit: "whenSet",
|
|
6922
|
+
writableOn: BOTH,
|
|
6923
|
+
validation: handledBy(PROMPT_HANDLER, "AdminAPI", "apply"),
|
|
6924
|
+
},
|
|
6925
|
+
// #2652 — the CEL rule gating who may execute the prompt. Required at
|
|
6926
|
+
// create; an explicit null/"" on update clears it (fail-closed). This
|
|
6927
|
+
// declaration is the whole authoring channel: the admin routes' unknown
|
|
6928
|
+
// key guard reads the same table, so it is what makes `accessRule` an
|
|
6929
|
+
// accepted body key as well as a pulled and pushed TOML line.
|
|
6930
|
+
{
|
|
6931
|
+
field: "accessRule",
|
|
6932
|
+
tomlKey: "accessRule",
|
|
6933
|
+
type: "string",
|
|
5230
6934
|
emit: "whenSet",
|
|
5231
6935
|
writableOn: BOTH,
|
|
5232
6936
|
validation: handledBy(PROMPT_HANDLER, "AdminAPI", "apply"),
|
|
5233
6937
|
},
|
|
6938
|
+
// #3798 — an AGENT's contract: its tools, event kinds, turn context,
|
|
6939
|
+
// history selection, who answers a paused call, and its limits. One
|
|
6940
|
+
// prompt-level block because they belong to the prompt, like
|
|
6941
|
+
// `inputSchema`, not to a named config (intent §D1).
|
|
6942
|
+
//
|
|
6943
|
+
// A `json` field like the schemas: authored as nested tables
|
|
6944
|
+
// (`[prompt.agent]`, `[[prompt.agent.tools]]`) or as one JSON string,
|
|
6945
|
+
// stored as the AUTHORED value with no defaults filled in and returned
|
|
6946
|
+
// parsed, so `config diff`'s value comparison reads a pushed file
|
|
6947
|
+
// Synced (DSO-3798-001). Only a `kind = "agent"` prompt may carry it.
|
|
6948
|
+
{
|
|
6949
|
+
field: "agent",
|
|
6950
|
+
tomlKey: "agent",
|
|
6951
|
+
type: "json",
|
|
6952
|
+
emit: "whenSet",
|
|
6953
|
+
writableOn: BOTH,
|
|
6954
|
+
validation: handledBy("src/config-surface/agent-declaration.ts", "resolveAgentDeclaration", "apply"),
|
|
6955
|
+
},
|
|
5234
6956
|
],
|
|
5235
6957
|
notExposed: {
|
|
5236
6958
|
promptId: {
|
|
@@ -5342,6 +7064,12 @@ export const PROMPT_SURFACE = {
|
|
|
5342
7064
|
"named typed questions OpenRouter's /api/alpha/decisions endpoint " +
|
|
5343
7065
|
"answers. Authored under `[configs.decisions.questions.<name>]`.",
|
|
5344
7066
|
},
|
|
7067
|
+
agent: {
|
|
7068
|
+
note: "#3798 — the model settings of a `kind = \"agent\"` prompt: the " +
|
|
7069
|
+
"six keys it shares with chat (systemPrompt, temperature, topP, " +
|
|
7070
|
+
"maxTokens, reasoningEffort, reasoningBudget), and no template, " +
|
|
7071
|
+
"outputFormat or outputSchema. Authored under `[configs.agent]`.",
|
|
7072
|
+
},
|
|
5345
7073
|
},
|
|
5346
7074
|
fields: [
|
|
5347
7075
|
{
|
|
@@ -5356,9 +7084,16 @@ export const PROMPT_SURFACE = {
|
|
|
5356
7084
|
// #2645 — a config's status is configuration: `prompts configs update
|
|
5357
7085
|
// --status archived` used to be the only way to retire one, and pull
|
|
5358
7086
|
// emitted nothing, so the file could not say which of its configs the
|
|
5359
|
-
// server still runs.
|
|
5360
|
-
//
|
|
5361
|
-
//
|
|
7087
|
+
// server still runs.
|
|
7088
|
+
//
|
|
7089
|
+
// #3799 (DSO-3799-005) — writable on CREATE too. It was update-only,
|
|
7090
|
+
// which meant `config push` created every config live and archived it
|
|
7091
|
+
// afterwards; a pulled tree whose retired config is on a provider the
|
|
7092
|
+
// new schema-translation check refuses could therefore not be pushed
|
|
7093
|
+
// into a fresh environment at all, because the refusal fired on a state
|
|
7094
|
+
// the file never asked for. Both create paths honor `active |
|
|
7095
|
+
// archived` now (default `active`, so a body that omits it is
|
|
7096
|
+
// unchanged), and the post-create status patches are retired.
|
|
5362
7097
|
//
|
|
5363
7098
|
// #3201 — emitted only when it is NOT the create default, so a pulled
|
|
5364
7099
|
// file says `status = "archived"` about a retired config and says
|
|
@@ -5373,7 +7108,7 @@ export const PROMPT_SURFACE = {
|
|
|
5373
7108
|
tomlKey: "status",
|
|
5374
7109
|
type: "string",
|
|
5375
7110
|
emit: "whenSet",
|
|
5376
|
-
writableOn:
|
|
7111
|
+
writableOn: BOTH,
|
|
5377
7112
|
validation: handledBy(PROMPT_HANDLER, "AdminAPI", "apply"),
|
|
5378
7113
|
serverDefault: "active",
|
|
5379
7114
|
},
|
|
@@ -5405,7 +7140,7 @@ export const PROMPT_SURFACE = {
|
|
|
5405
7140
|
{
|
|
5406
7141
|
field: "systemPrompt",
|
|
5407
7142
|
tomlKey: "systemPrompt",
|
|
5408
|
-
tomlGroup:
|
|
7143
|
+
tomlGroup: SHARED_WITH_AGENT,
|
|
5409
7144
|
type: "string",
|
|
5410
7145
|
emit: "whenSet",
|
|
5411
7146
|
writableOn: BOTH,
|
|
@@ -5425,7 +7160,7 @@ export const PROMPT_SURFACE = {
|
|
|
5425
7160
|
{
|
|
5426
7161
|
field: "temperature",
|
|
5427
7162
|
tomlKey: "temperature",
|
|
5428
|
-
tomlGroup:
|
|
7163
|
+
tomlGroup: SHARED_WITH_AGENT,
|
|
5429
7164
|
type: "number",
|
|
5430
7165
|
emit: "whenSet",
|
|
5431
7166
|
writableOn: BOTH,
|
|
@@ -5443,7 +7178,7 @@ export const PROMPT_SURFACE = {
|
|
|
5443
7178
|
{
|
|
5444
7179
|
field: "topP",
|
|
5445
7180
|
tomlKey: "topP",
|
|
5446
|
-
tomlGroup:
|
|
7181
|
+
tomlGroup: SHARED_WITH_AGENT,
|
|
5447
7182
|
type: "number",
|
|
5448
7183
|
emit: "whenSet",
|
|
5449
7184
|
writableOn: BOTH,
|
|
@@ -5455,7 +7190,7 @@ export const PROMPT_SURFACE = {
|
|
|
5455
7190
|
{
|
|
5456
7191
|
field: "maxTokens",
|
|
5457
7192
|
tomlKey: "maxTokens",
|
|
5458
|
-
tomlGroup:
|
|
7193
|
+
tomlGroup: SHARED_WITH_AGENT,
|
|
5459
7194
|
type: "number",
|
|
5460
7195
|
emit: "whenSet",
|
|
5461
7196
|
writableOn: BOTH,
|
|
@@ -5504,7 +7239,7 @@ export const PROMPT_SURFACE = {
|
|
|
5504
7239
|
{
|
|
5505
7240
|
field: "reasoningEffort",
|
|
5506
7241
|
tomlKey: "reasoningEffort",
|
|
5507
|
-
tomlGroup:
|
|
7242
|
+
tomlGroup: SHARED_WITH_AGENT,
|
|
5508
7243
|
type: "string",
|
|
5509
7244
|
emit: "whenSet",
|
|
5510
7245
|
writableOn: BOTH,
|
|
@@ -5513,12 +7248,29 @@ export const PROMPT_SURFACE = {
|
|
|
5513
7248
|
{
|
|
5514
7249
|
field: "reasoningBudget",
|
|
5515
7250
|
tomlKey: "reasoningBudget",
|
|
5516
|
-
tomlGroup:
|
|
7251
|
+
tomlGroup: SHARED_WITH_AGENT,
|
|
5517
7252
|
type: "number",
|
|
5518
7253
|
emit: "whenSet",
|
|
5519
7254
|
writableOn: BOTH,
|
|
5520
7255
|
validation: handledBy("src/config-surface/prompt-reasoning.ts", "resolvePromptReasoning", "apply"),
|
|
5521
7256
|
},
|
|
7257
|
+
// #3801 — the opt-in that sends `[prompt].outputSchema` as a strict
|
|
7258
|
+
// `json_schema` on OpenRouter instead of JSON mode checked afterwards
|
|
7259
|
+
// (§D6). A chat key only: an agent has no output schema (§D1), and a
|
|
7260
|
+
// decisions config's answer is typed by its questions. No flat
|
|
7261
|
+
// spelling. The rule — one boolean, never beside a config-level
|
|
7262
|
+
// `outputSchema`, OpenRouter only, never with `outputFormat = "text"`,
|
|
7263
|
+
// and a prompt schema to send when the config is not archived — is
|
|
7264
|
+
// `resolvePromptStrictOutput`, read by the routes and the preflight.
|
|
7265
|
+
{
|
|
7266
|
+
field: "strictOutput",
|
|
7267
|
+
tomlKey: "strictOutput",
|
|
7268
|
+
tomlGroup: "chat",
|
|
7269
|
+
type: "boolean",
|
|
7270
|
+
emit: "whenSet",
|
|
7271
|
+
writableOn: BOTH,
|
|
7272
|
+
validation: handledBy("src/config-surface/prompt-strict-output.ts", "resolvePromptStrictOutput", "apply"),
|
|
7273
|
+
},
|
|
5522
7274
|
// #3626 — the named typed questions a DECISIONS config asks. A `json`
|
|
5523
7275
|
// field like `outputSchema`: stored as a JSON string, returned parsed,
|
|
5524
7276
|
// and authored either as nested tables
|
|
@@ -6776,6 +8528,11 @@ export const DATABASE_TYPE_SURFACE = {
|
|
|
6776
8528
|
writableOn: BOTH,
|
|
6777
8529
|
// CEL, parsed before storing (#1336). The legacy `metadataAccess`
|
|
6778
8530
|
// spelling stays readable on push — see tomlOnlyKeys.
|
|
8531
|
+
deprecated: {
|
|
8532
|
+
note: "one CEL expression gates both read and update of the database " +
|
|
8533
|
+
"CEL context; use a resource metadata category, which has " +
|
|
8534
|
+
"separate readRule and writeRule",
|
|
8535
|
+
},
|
|
6779
8536
|
validation: handledBy(DB_TYPE_HANDLER, "DatabaseTypeConfigController", "apply"),
|
|
6780
8537
|
},
|
|
6781
8538
|
{
|
|
@@ -8578,6 +10335,7 @@ export const GENERATED_CONFIG_MODEL_FIELDS = {
|
|
|
8578
10335
|
"kind",
|
|
8579
10336
|
"displayName",
|
|
8580
10337
|
"description",
|
|
10338
|
+
"agent",
|
|
8581
10339
|
"copiedFromCatalogId",
|
|
8582
10340
|
"inputSchema",
|
|
8583
10341
|
"outputSchema",
|
|
@@ -8611,6 +10369,7 @@ export const GENERATED_CONFIG_MODEL_FIELDS = {
|
|
|
8611
10369
|
"questions",
|
|
8612
10370
|
"reasoningEffort",
|
|
8613
10371
|
"reasoningBudget",
|
|
10372
|
+
"strictOutput",
|
|
8614
10373
|
"status",
|
|
8615
10374
|
"createdBy",
|
|
8616
10375
|
"createdAt",
|