primitive-admin 1.2.0-alpha.3 → 1.2.0-alpha.4
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/dist/src/commands/agent-sessions.js +4 -0
- package/dist/src/commands/agent-sessions.js.map +1 -1
- package/dist/src/commands/databases.js +13 -38
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/documents.d.ts +1 -1
- package/dist/src/commands/documents.js +70 -33
- package/dist/src/commands/documents.js.map +1 -1
- package/dist/src/commands/prompts.d.ts +7 -0
- package/dist/src/commands/prompts.js +44 -0
- package/dist/src/commands/prompts.js.map +1 -1
- package/dist/src/commands/sync.d.ts +12 -29
- package/dist/src/commands/sync.js +128 -138
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/lib/aggregate-output.d.ts +25 -0
- package/dist/src/lib/aggregate-output.js +53 -0
- package/dist/src/lib/aggregate-output.js.map +1 -0
- package/dist/src/lib/api-client.d.ts +29 -16
- package/dist/src/lib/api-client.js +30 -28
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/config-object-descriptor.js +14 -5
- package/dist/src/lib/config-object-descriptor.js.map +1 -1
- package/dist/src/lib/db-codegen/dbGenerator.js +5 -16
- package/dist/src/lib/db-codegen/dbGenerator.js.map +1 -1
- package/dist/src/lib/db-codegen/dbNaming.d.ts +3 -12
- package/dist/src/lib/db-codegen/dbNaming.js +3 -16
- package/dist/src/lib/db-codegen/dbNaming.js.map +1 -1
- package/dist/src/lib/db-codegen/dbTemplates.d.ts +7 -56
- package/dist/src/lib/db-codegen/dbTemplates.js +25 -125
- package/dist/src/lib/db-codegen/dbTemplates.js.map +1 -1
- package/dist/src/lib/db-codegen/dbTypeIR.d.ts +1 -1
- package/dist/src/lib/function-sync.d.ts +3 -16
- package/dist/src/lib/function-sync.js +14 -18
- package/dist/src/lib/function-sync.js.map +1 -1
- package/dist/src/lib/generated-config-surfaces.d.ts +172 -47
- package/dist/src/lib/generated-config-surfaces.js +599 -90
- 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/prompt-schema-codegen.js +5 -2
- package/dist/src/lib/prompt-schema-codegen.js.map +1 -1
- package/dist/src/lib/swift-codegen/dbGenerator.d.ts +4 -3
- package/dist/src/lib/swift-codegen/dbGenerator.js +14 -117
- package/dist/src/lib/swift-codegen/dbGenerator.js.map +1 -1
- package/dist/src/lib/swift-codegen/generator.d.ts +5 -5
- package/dist/src/lib/swift-codegen/generator.js +8 -13
- package/dist/src/lib/swift-codegen/generator.js.map +1 -1
- package/dist/src/lib/workflow-codegen/generator.d.ts +7 -7
- package/dist/src/lib/workflow-codegen/generator.js +16 -23
- package/dist/src/lib/workflow-codegen/generator.js.map +1 -1
- package/dist/src/lib/workflow-codegen/invokerIR.d.ts +6 -12
- package/dist/src/lib/workflow-codegen/invokerIR.js +4 -5
- package/dist/src/lib/workflow-codegen/invokerIR.js.map +1 -1
- package/dist/src/lib/workflow-toml-validator.d.ts +2 -0
- package/dist/src/lib/workflow-toml-validator.js +13 -0
- package/dist/src/lib/workflow-toml-validator.js.map +1 -1
- package/package.json +2 -2
|
@@ -2386,6 +2386,21 @@ export const RETIRED_CONFIG_KEYS = {
|
|
|
2386
2386
|
"`admin-only` (app admins; `personal-uploads` is the per-uploader " +
|
|
2387
2387
|
"preset).",
|
|
2388
2388
|
},
|
|
2389
|
+
// #4283 — #4116 made the bucket author its rule set by name and kept the
|
|
2390
|
+
// id spelling as a deprecated key. Its own sentence rather than the type
|
|
2391
|
+
// configs' shared one, which says a pulled file already spells the name:
|
|
2392
|
+
// a bucket file pulled before #4116 does not. The `request` half is never
|
|
2393
|
+
// surfaced, for the reason given at `RULE_SET_ID_RETIRED`.
|
|
2394
|
+
ruleSetId: {
|
|
2395
|
+
toml: "`ruleSetId` is no longer authored in TOML (#4283). A rule-set id " +
|
|
2396
|
+
"names a row in one app and resolves to nothing in another, so the " +
|
|
2397
|
+
"reference is authored by name: replace this line with " +
|
|
2398
|
+
'`ruleSetName = "<name>"`, where the name is the `name` the rule ' +
|
|
2399
|
+
"set's own `rule-sets/<key>.toml` declares, and push again, or " +
|
|
2400
|
+
"`config pull` rewrites the file.",
|
|
2401
|
+
request: "`ruleSetId` is the live request key on this surface; only its TOML " +
|
|
2402
|
+
"spelling is retired (#4283). Send the rule set's id.",
|
|
2403
|
+
},
|
|
2389
2404
|
},
|
|
2390
2405
|
// #2645 — pausing moved off `state` onto its own field, so `state` stopped
|
|
2391
2406
|
// being author-writable rather than becoming a second way to say "paused".
|
|
@@ -2423,8 +2438,8 @@ export const RETIRED_CONFIG_KEYS = {
|
|
|
2423
2438
|
},
|
|
2424
2439
|
},
|
|
2425
2440
|
// #3997 — keyed by each table's TOML prefix. Only `database-type-config`
|
|
2426
|
-
// uses `[type]`, so that entry cannot reach another surface
|
|
2427
|
-
// bucket's `[bucket] ruleSetId`
|
|
2441
|
+
// uses `[type]`, so that entry cannot reach another surface. The blob
|
|
2442
|
+
// bucket's `[bucket] ruleSetId` has its own entry under `bucket` (#4283).
|
|
2428
2443
|
type: { ruleSetId: RULE_SET_ID_RETIRED },
|
|
2429
2444
|
groupTypeConfig: { ruleSetId: RULE_SET_ID_RETIRED },
|
|
2430
2445
|
collectionTypeConfig: { ruleSetId: RULE_SET_ID_RETIRED },
|
|
@@ -3336,7 +3351,8 @@ export function parseFunctionManifest(value) {
|
|
|
3336
3351
|
// ── src/config-surface/function-retired-keys.ts ──────────────────────────
|
|
3337
3352
|
/**
|
|
3338
3353
|
* The runtime keys a `functions/<key>.toml` no longer decides anything with —
|
|
3339
|
-
* #3482, project `server-functions` phase
|
|
3354
|
+
* #3482 retired them, and #3188 (project `server-functions` phase 5) refuses
|
|
3355
|
+
* them.
|
|
3340
3356
|
*
|
|
3341
3357
|
* The sponsor settled it on 2026-09-15 (the intent, "Mode vocabulary?",
|
|
3342
3358
|
* amended again): the config says NOTHING about how a function runs. A
|
|
@@ -3345,44 +3361,50 @@ export function parseFunctionManifest(value) {
|
|
|
3345
3361
|
* and a function that must not run under one runtime tests for it in code with
|
|
3346
3362
|
* `assertRuntime`.
|
|
3347
3363
|
*
|
|
3348
|
-
* ──
|
|
3364
|
+
* ── Accepted through phase 4, refused from phase 5 ────────────────────────
|
|
3349
3365
|
*
|
|
3350
|
-
* Every tree pushed since #3454 that declares a cron trigger
|
|
3351
|
-
* `mode = …` ON THE ENTRY, because `cron-trigger-mode-required` demanded it
|
|
3352
|
-
*
|
|
3353
|
-
*
|
|
3354
|
-
*
|
|
3355
|
-
*
|
|
3366
|
+
* Every tree pushed since #3454 that declares a cron trigger carried
|
|
3367
|
+
* `mode = …` ON THE ENTRY, because `cron-trigger-mode-required` demanded it,
|
|
3368
|
+
* so #3482 accepted the keys and warned rather than refusing them one day
|
|
3369
|
+
* after the platform insisted on them. Phase 5 is the declared breaking phase
|
|
3370
|
+
* that ends the grace: `mode`, `durable` and a cron entry's `mode` are refused
|
|
3371
|
+
* with one sentence per key, naming the key, saying the caller picks the
|
|
3372
|
+
* runtime, and saying to delete the line. `config pull` still strips them
|
|
3373
|
+
* (`stripRetiredFunctionKeys` in the CLI reads this scanner), so a tree pulled
|
|
3374
|
+
* from a version pushed before the retirement pushes again unchanged.
|
|
3356
3375
|
*
|
|
3357
|
-
*
|
|
3358
|
-
*
|
|
3359
|
-
*
|
|
3360
|
-
* nothing left to validate it against, and refusing a bad value for a key
|
|
3361
|
-
* nobody reads would be a refusal about spelling alone.
|
|
3376
|
+
* One rule id, one handler: `refusedFunctionKeys` is declared `preflight` on
|
|
3377
|
+
* the `function` surface and the CLI's push preflight runs it before the first
|
|
3378
|
+
* request. The value is not validated — the key's PRESENCE is the refusal.
|
|
3362
3379
|
*
|
|
3363
|
-
* Pure: no models, no tenant context, no `env` (see `types.ts` §Purity).
|
|
3364
|
-
*
|
|
3365
|
-
*
|
|
3366
|
-
*
|
|
3380
|
+
* Pure: no models, no tenant context, no `env` (see `types.ts` §Purity). The
|
|
3381
|
+
* CLI preflight reaches this module through the vendored copy
|
|
3382
|
+
* (`cli/scripts/gen-config-surfaces.mjs`), so the sentence an author reads from
|
|
3383
|
+
* `config push` is the one this module writes.
|
|
3367
3384
|
*/
|
|
3368
3385
|
/** The two `[function]` keys that no longer say anything (#3482). */
|
|
3369
3386
|
export const RETIRED_FUNCTION_RUNTIME_KEYS = ["mode", "durable"];
|
|
3387
|
+
/**
|
|
3388
|
+
* The rule ids this module owns, declared on the `function` surface (#3374's
|
|
3389
|
+
* anchor). One id covers the three spellings: they are one rule — the config
|
|
3390
|
+
* does not choose the runtime — met in three places.
|
|
3391
|
+
*/
|
|
3392
|
+
export const RETIRED_RUNTIME_KEY_RULE_IDS = ["retired-runtime-key"];
|
|
3393
|
+
/** The one refusal constructor (#3373's anchor). */
|
|
3394
|
+
function refuseRuntimeKey(ruleId, path, message) {
|
|
3395
|
+
return { ruleId, path, message };
|
|
3396
|
+
}
|
|
3370
3397
|
const RUNTIME_IS_THE_CALLERS = "a function is a function, and the caller picks the runtime at each call — " +
|
|
3371
3398
|
"`invoke` runs it inside the request, `start` runs it as a task";
|
|
3372
|
-
const FROM_PHASE_5 = "Remove the line; from phase 5 it is refused.";
|
|
3373
3399
|
/**
|
|
3374
|
-
* Every retired runtime key a `[function]` table
|
|
3400
|
+
* Every retired runtime key a `[function]` table carries.
|
|
3375
3401
|
*
|
|
3376
3402
|
* Reads the table the way the author wrote it: the two top-level keys, and a
|
|
3377
|
-
* `mode` on each `[[function.triggers.cron]]` entry.
|
|
3378
|
-
*
|
|
3379
|
-
*
|
|
3380
|
-
* gone).
|
|
3381
|
-
*
|
|
3382
|
-
* Order is the order an author reads their file in: the top-level keys first,
|
|
3383
|
-
* then the cron entries in declaration order.
|
|
3403
|
+
* `mode` on each `[[function.triggers.cron]]` entry. Order is the order an
|
|
3404
|
+
* author reads their file in: the top-level keys first, then the cron entries
|
|
3405
|
+
* in declaration order.
|
|
3384
3406
|
*/
|
|
3385
|
-
|
|
3407
|
+
function retiredKeySites(functionTable) {
|
|
3386
3408
|
const table = functionTable;
|
|
3387
3409
|
if (!table || typeof table !== "object" || Array.isArray(table))
|
|
3388
3410
|
return [];
|
|
@@ -3390,10 +3412,7 @@ export function ignoredFunctionKeys(functionTable) {
|
|
|
3390
3412
|
for (const key of RETIRED_FUNCTION_RUNTIME_KEYS) {
|
|
3391
3413
|
if (table[key] === undefined)
|
|
3392
3414
|
continue;
|
|
3393
|
-
found.push({
|
|
3394
|
-
path: key,
|
|
3395
|
-
message: `\`${key}\` is ignored (#3482): ${RUNTIME_IS_THE_CALLERS}. ${FROM_PHASE_5}`,
|
|
3396
|
-
});
|
|
3415
|
+
found.push({ path: key, cronEntry: null, key });
|
|
3397
3416
|
}
|
|
3398
3417
|
const triggers = table.triggers;
|
|
3399
3418
|
const rawCron = triggers && typeof triggers === "object" && !Array.isArray(triggers)
|
|
@@ -3411,19 +3430,38 @@ export function ignoredFunctionKeys(functionTable) {
|
|
|
3411
3430
|
return;
|
|
3412
3431
|
const rawName = entry.name;
|
|
3413
3432
|
const name = typeof rawName === "string" && rawName ? rawName : `#${index + 1}`;
|
|
3414
|
-
found.push({
|
|
3415
|
-
path: `triggers.cron.${name}.mode`,
|
|
3416
|
-
message: `\`mode\` on cron trigger '${name}' is ignored (#3482): every cron ` +
|
|
3417
|
-
`fire starts a task run, and ${RUNTIME_IS_THE_CALLERS}. ${FROM_PHASE_5}`,
|
|
3418
|
-
});
|
|
3433
|
+
found.push({ path: `triggers.cron.${name}.mode`, cronEntry: name, key: "mode" });
|
|
3419
3434
|
});
|
|
3420
3435
|
return found;
|
|
3421
3436
|
}
|
|
3437
|
+
/**
|
|
3438
|
+
* Every retired runtime key the table carries, each as the refusal the author
|
|
3439
|
+
* reads. A cron entry's sentence says the extra thing its author needs to hear
|
|
3440
|
+
* — that the schedule no longer chooses, because every fire starts a task run.
|
|
3441
|
+
*/
|
|
3442
|
+
export function refusedFunctionKeys(functionTable) {
|
|
3443
|
+
return retiredKeySites(functionTable).map((site) => refuseRuntimeKey("retired-runtime-key", site.path, site.cronEntry === null
|
|
3444
|
+
? `\`${site.key}\` is not a key of a function: ${RUNTIME_IS_THE_CALLERS}. ` +
|
|
3445
|
+
"Delete the line."
|
|
3446
|
+
: `\`mode\` on cron trigger '${site.cronEntry}' is not a key of a ` +
|
|
3447
|
+
`function: every cron fire starts a task run, and ${RUNTIME_IS_THE_CALLERS}. ` +
|
|
3448
|
+
"Delete the line."));
|
|
3449
|
+
}
|
|
3450
|
+
/**
|
|
3451
|
+
* The same refusals, rendered for one file — the shape `config push` and
|
|
3452
|
+
* `config diff` print.
|
|
3453
|
+
*
|
|
3454
|
+
* `label` is what the reader can act on: `functions/<key>.toml` from the CLI,
|
|
3455
|
+
* and the function's key from the API, which has no file to name.
|
|
3456
|
+
*/
|
|
3457
|
+
export function refusedFunctionKeyMessages(label, functionTable) {
|
|
3458
|
+
return refusedFunctionKeys(functionTable).map((refusal) => `${label}: ${refusal.message}`);
|
|
3459
|
+
}
|
|
3422
3460
|
/**
|
|
3423
3461
|
* The database types a `[function]` table's retired trigger block names —
|
|
3424
3462
|
* #3483.
|
|
3425
3463
|
*
|
|
3426
|
-
* Beside `
|
|
3464
|
+
* Beside `refusedFunctionKeys` because it answers the same question about the
|
|
3427
3465
|
* same table, for the same reader: `config pull` writes a version's AUTHORED
|
|
3428
3466
|
* bytes, and a version pushed before the removal carries the block. Without
|
|
3429
3467
|
* this the pulled tree would be refused on its next push — a tree an author
|
|
@@ -3484,16 +3522,6 @@ function retiredDatabaseTriggerValue(functionTable) {
|
|
|
3484
3522
|
}
|
|
3485
3523
|
return triggers.database;
|
|
3486
3524
|
}
|
|
3487
|
-
/**
|
|
3488
|
-
* The same scan, rendered for one file — the shape `config push` and
|
|
3489
|
-
* `config diff` print and the admin route answers with.
|
|
3490
|
-
*
|
|
3491
|
-
* `label` is what the reader can act on: `functions/<key>.toml` from the CLI,
|
|
3492
|
-
* and the function's key from the API, which has no file to name.
|
|
3493
|
-
*/
|
|
3494
|
-
export function ignoredFunctionKeyWarnings(label, functionTable) {
|
|
3495
|
-
return ignoredFunctionKeys(functionTable).map((ignored) => `${label}: ${ignored.message}`);
|
|
3496
|
-
}
|
|
3497
3525
|
export const EMPTY_TRIGGER_DECLARATION = {
|
|
3498
3526
|
webhook: null,
|
|
3499
3527
|
cron: [],
|
|
@@ -4339,6 +4367,10 @@ export const WORKFLOW_SURFACE = {
|
|
|
4339
4367
|
// Accepted on BOTH modes — #807's deferred second PATCH is an update
|
|
4340
4368
|
// call. The CLI's create-only send is a sequencing invariant owned by
|
|
4341
4369
|
// `applyWorkflowBody`, not a statement about what the server accepts.
|
|
4370
|
+
// #3188 retired the synchronous run route this flag opted into; the
|
|
4371
|
+
// field still round-trips (pull, diff, push) until the workflow
|
|
4372
|
+
// surface itself retires, and `config push` notes a `true` as
|
|
4373
|
+
// ignored. Its step-kind rule went with the route.
|
|
4342
4374
|
{
|
|
4343
4375
|
field: "syncCallable",
|
|
4344
4376
|
tomlKey: "syncCallable",
|
|
@@ -4594,8 +4626,6 @@ export const WORKFLOW_SURFACE = {
|
|
|
4594
4626
|
workflowApplyRule("access-rule-required", "validateWorkflowIdentityConfig", "On create, a caller-mode workflow must state its `accessRule` (`\"true\"` " +
|
|
4595
4627
|
"for any app member), so a locked workflow is a decision rather than a " +
|
|
4596
4628
|
"default."),
|
|
4597
|
-
workflowApplyRule("sync-callable-step-kind", "validateSyncCallableSteps", "A `syncCallable = true` workflow may not carry a step kind that only " +
|
|
4598
|
-
"the durable runtime can execute (`delay`, `event.wait`)."),
|
|
4599
4629
|
],
|
|
4600
4630
|
};
|
|
4601
4631
|
const FIELD_RANGES = [
|
|
@@ -5697,6 +5727,9 @@ export const AGENT_ERROR_CODES = [
|
|
|
5697
5727
|
"AGENT_TOOL_ARGUMENTS_INVALID",
|
|
5698
5728
|
"AGENT_ROUND_TRUNCATED",
|
|
5699
5729
|
"AGENT_ROUND_EMPTY",
|
|
5730
|
+
// #4262 — a single-round test case whose seeded history, context or
|
|
5731
|
+
// assertions cannot be a round of this agent (`agent-test-case.ts`).
|
|
5732
|
+
"AGENT_TEST_CASE_INVALID",
|
|
5700
5733
|
];
|
|
5701
5734
|
/**
|
|
5702
5735
|
* The platform maximums. An agent may lower `maxSteps`, `waitLimitSeconds`
|
|
@@ -5721,6 +5754,20 @@ export const AGENT_NAME = /^[A-Za-z_][A-Za-z0-9_-]{0,63}$/;
|
|
|
5721
5754
|
export const AGENT_ANSWERED_BY = ["initiator", "participants"];
|
|
5722
5755
|
export const AGENT_TOOL_INVOKE = ["request", "task"];
|
|
5723
5756
|
export const AGENT_TOOL_HISTORY_SCOPES = ["session", "turn"];
|
|
5757
|
+
/**
|
|
5758
|
+
* The result the platform records for a call to a client tool with
|
|
5759
|
+
* `endsTurn = true`: the call is final when its response is committed, and
|
|
5760
|
+
* later rounds read this as its tool result.
|
|
5761
|
+
*/
|
|
5762
|
+
export const AGENT_TERMINAL_CALL_OUTPUT = Object.freeze({
|
|
5763
|
+
delivered: true,
|
|
5764
|
+
});
|
|
5765
|
+
/** The schema of `AGENT_TERMINAL_CALL_OUTPUT`: a terminal client tool's output type. */
|
|
5766
|
+
export const AGENT_TERMINAL_CALL_OUTPUT_SCHEMA = Object.freeze({
|
|
5767
|
+
type: "object",
|
|
5768
|
+
properties: { delivered: { type: "boolean" } },
|
|
5769
|
+
required: ["delivered"],
|
|
5770
|
+
});
|
|
5724
5771
|
const AGENT_TOP_LEVEL_KEYS = [
|
|
5725
5772
|
"answeredBy",
|
|
5726
5773
|
"maxSteps",
|
|
@@ -5742,6 +5789,7 @@ const AGENT_TOOL_KEYS = [
|
|
|
5742
5789
|
"approval",
|
|
5743
5790
|
"statusText",
|
|
5744
5791
|
"historyScope",
|
|
5792
|
+
"endsTurn",
|
|
5745
5793
|
"inputSchema",
|
|
5746
5794
|
"outputSchema",
|
|
5747
5795
|
];
|
|
@@ -5870,8 +5918,22 @@ function checkAgentTool(tool, index, names, out) {
|
|
|
5870
5918
|
out.push(agentInvalid(`${path}.invoke`, `must be one of ${AGENT_TOOL_INVOKE.join(" | ")}`));
|
|
5871
5919
|
}
|
|
5872
5920
|
}
|
|
5921
|
+
const terminal = tool.endsTurn === true;
|
|
5922
|
+
if (tool.endsTurn !== undefined && typeof tool.endsTurn !== "boolean") {
|
|
5923
|
+
out.push(agentInvalid(`${path}.endsTurn`, "must be true or false"));
|
|
5924
|
+
}
|
|
5925
|
+
if (terminal && tool.approval === true) {
|
|
5926
|
+
out.push(agentInvalid(`${path}.endsTurn`, "cannot be set with approval = true: a terminal call never pauses"));
|
|
5927
|
+
}
|
|
5873
5928
|
for (const key of ["inputSchema", "outputSchema"]) {
|
|
5874
5929
|
const schemaPath = `${path}.${key}`;
|
|
5930
|
+
if (isClient && terminal && key === "outputSchema") {
|
|
5931
|
+
if (tool[key] !== undefined) {
|
|
5932
|
+
out.push(agentInvalid(schemaPath, "is not declared on a client tool with endsTurn = true: the " +
|
|
5933
|
+
"platform records a terminal client call's result — remove it"));
|
|
5934
|
+
}
|
|
5935
|
+
continue;
|
|
5936
|
+
}
|
|
5875
5937
|
if (isClient) {
|
|
5876
5938
|
if (tool[key] === undefined) {
|
|
5877
5939
|
out.push(agentInvalid(schemaPath, "is required on a client tool"));
|
|
@@ -6146,17 +6208,14 @@ export function agentRoundNotAgentRefusal(promptKey) {
|
|
|
6146
6208
|
"agent rounds. Run it with ctx.prompts.run, or name an agent prompt");
|
|
6147
6209
|
}
|
|
6148
6210
|
/**
|
|
6149
|
-
* The refusal of a prompt test case that
|
|
6150
|
-
*
|
|
6151
|
-
*
|
|
6152
|
-
*
|
|
6211
|
+
* The refusal of a prompt test case that names an agent as its judge (§D1: an
|
|
6212
|
+
* evaluator runs single-shot, which an agent never does). A case written ON an
|
|
6213
|
+
* agent is a single-round test since #4262 (`agent-test-case.ts`), so
|
|
6214
|
+
* `evaluator` is the one role left.
|
|
6153
6215
|
*/
|
|
6154
|
-
export function agentTestCaseRefusal(promptKey,
|
|
6155
|
-
return agentRefusal("AGENT_TEST_CASE_NOT_SUPPORTED",
|
|
6156
|
-
|
|
6157
|
-
"cases are not supported for agents yet. Test its tools as functions"
|
|
6158
|
-
: `Prompt "${promptKey}" is an agent (kind = "agent") and cannot be an ` +
|
|
6159
|
-
"evaluator: an evaluator is run single-shot. Name a chat prompt");
|
|
6216
|
+
export function agentTestCaseRefusal(promptKey, _role = "evaluator") {
|
|
6217
|
+
return agentRefusal("AGENT_TEST_CASE_NOT_SUPPORTED", `Prompt "${promptKey}" is an agent (kind = "agent") and cannot be an ` +
|
|
6218
|
+
"evaluator: an evaluator is run single-shot. Name a chat prompt");
|
|
6160
6219
|
}
|
|
6161
6220
|
/** A deep copy, so a reader's defaults never reach the caller's value. */
|
|
6162
6221
|
function agentCopy(value) {
|
|
@@ -6165,7 +6224,7 @@ function agentCopy(value) {
|
|
|
6165
6224
|
/**
|
|
6166
6225
|
* The declaration with every default applied: `answeredBy = "initiator"`, the
|
|
6167
6226
|
* platform maximums for the limits, and per tool `invoke = "request"` (server
|
|
6168
|
-
* tools), `approval = false
|
|
6227
|
+
* tools), `approval = false`, `historyScope = "session"` and `endsTurn = false`.
|
|
6169
6228
|
*
|
|
6170
6229
|
* The ONLY accessor for defaults — the stored value never carries them
|
|
6171
6230
|
* (DSO-3798-001). Takes a value `resolveAgentDeclaration` accepted (or `null`)
|
|
@@ -6185,6 +6244,7 @@ export function normalizeAgentDeclaration(declaration) {
|
|
|
6185
6244
|
...(tool.runs === "client" ? {} : { invoke: tool.invoke ?? "request" }),
|
|
6186
6245
|
approval: tool.approval ?? false,
|
|
6187
6246
|
historyScope: tool.historyScope ?? "session",
|
|
6247
|
+
endsTurn: tool.endsTurn ?? false,
|
|
6188
6248
|
})),
|
|
6189
6249
|
events: source.events ?? [],
|
|
6190
6250
|
};
|
|
@@ -6902,6 +6962,365 @@ export function agentSchemaTranslationRefusals(input) {
|
|
|
6902
6962
|
}
|
|
6903
6963
|
return out;
|
|
6904
6964
|
}
|
|
6965
|
+
// ── src/config-surface/agent-test-case.ts ────────────────────────────────
|
|
6966
|
+
/**
|
|
6967
|
+
* What an agent test case may say — issue #4262 (single-round prompt tests
|
|
6968
|
+
* from a seeded history; design-docs/agents.md §D1).
|
|
6969
|
+
*
|
|
6970
|
+
* A test case on an agent prompt carries one `[test.agent]` block: a seeded
|
|
6971
|
+
* `history` (JSON text), the round's turn `context`, `timezone` and `locale`,
|
|
6972
|
+
* and two assertions on the round's calls (`expectedToolCall`,
|
|
6973
|
+
* `expectedNoToolCall`). On the wire and in storage those are six flat
|
|
6974
|
+
* `BlockTestCase` fields, `agentHistory` … `agentExpectedNoToolCall`.
|
|
6975
|
+
*
|
|
6976
|
+
* This module is the whole FILE-DECIDABLE rule for them: the history's shape,
|
|
6977
|
+
* its turns, its call and result pairing, where its events sit, the
|
|
6978
|
+
* assertions' shapes, and — when the caller passes the agent's declaration —
|
|
6979
|
+
* that the event kinds and the asserted tool are declared. The admin test-case
|
|
6980
|
+
* routes call it, and so does the CLI's push preflight through the vendored
|
|
6981
|
+
* artifact, so a refusal at push and a refusal at the route cannot disagree.
|
|
6982
|
+
* What needs app state or a schema validator (the input, context and event
|
|
6983
|
+
* schemas, the send route's zone and tag rules) is `agent-test-round.ts`'s.
|
|
6984
|
+
*
|
|
6985
|
+
* Every refusal carries `AGENT_TEST_CASE_INVALID` and the caller gets every
|
|
6986
|
+
* offending item at once, as the declaration resolver does.
|
|
6987
|
+
*
|
|
6988
|
+
* Pure and dependency-free apart from the agent declaration's types, like
|
|
6989
|
+
* every module under `src/config-surface/`.
|
|
6990
|
+
*/
|
|
6991
|
+
/** The six wire and stored fields, in the order the surface declares them. */
|
|
6992
|
+
export const AGENT_TEST_CASE_FIELDS = [
|
|
6993
|
+
"agentHistory",
|
|
6994
|
+
"agentContext",
|
|
6995
|
+
"agentTimezone",
|
|
6996
|
+
"agentLocale",
|
|
6997
|
+
"agentExpectedToolCall",
|
|
6998
|
+
"agentExpectedNoToolCall",
|
|
6999
|
+
];
|
|
7000
|
+
function agentTestRefusal(message) {
|
|
7001
|
+
return { code: "AGENT_TEST_CASE_INVALID", message: `[test.agent] ${message}` };
|
|
7002
|
+
}
|
|
7003
|
+
function agentTestIsObject(value) {
|
|
7004
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7005
|
+
}
|
|
7006
|
+
function agentTestIsText(value) {
|
|
7007
|
+
return typeof value === "string" && value.trim() !== "";
|
|
7008
|
+
}
|
|
7009
|
+
/** `null` and `undefined` are absence; so is `""` for a text field. */
|
|
7010
|
+
function agentTestIsUnset(value) {
|
|
7011
|
+
if (value === null || value === undefined)
|
|
7012
|
+
return true;
|
|
7013
|
+
return typeof value === "string" && value.trim() === "";
|
|
7014
|
+
}
|
|
7015
|
+
/** A JSON field: the value as given, or the JSON text a sidecar carries parsed. */
|
|
7016
|
+
function agentTestJsonValue(value) {
|
|
7017
|
+
if (typeof value !== "string")
|
|
7018
|
+
return { ok: true, value };
|
|
7019
|
+
try {
|
|
7020
|
+
return { ok: true, value: JSON.parse(value) };
|
|
7021
|
+
}
|
|
7022
|
+
catch {
|
|
7023
|
+
return { ok: false };
|
|
7024
|
+
}
|
|
7025
|
+
}
|
|
7026
|
+
/** The names a declaration declares, or `null` when the caller passed none. */
|
|
7027
|
+
function agentTestDeclaredNames(declaration, given) {
|
|
7028
|
+
if (!given)
|
|
7029
|
+
return null;
|
|
7030
|
+
let block = declaration;
|
|
7031
|
+
if (typeof block === "string") {
|
|
7032
|
+
try {
|
|
7033
|
+
block = JSON.parse(block);
|
|
7034
|
+
}
|
|
7035
|
+
catch {
|
|
7036
|
+
block = null;
|
|
7037
|
+
}
|
|
7038
|
+
}
|
|
7039
|
+
const names = (list) => new Set((Array.isArray(list) ? list : [])
|
|
7040
|
+
.map((item) => (agentTestIsObject(item) ? item.name : undefined))
|
|
7041
|
+
.filter((name) => typeof name === "string"));
|
|
7042
|
+
const source = agentTestIsObject(block) ? block : {};
|
|
7043
|
+
return { tools: names(source.tools), events: names(source.events) };
|
|
7044
|
+
}
|
|
7045
|
+
const AGENT_TEST_ENTRY_KEYS = {
|
|
7046
|
+
user: ["role", "text"],
|
|
7047
|
+
assistant: ["role", "text", "reasoning", "calls"],
|
|
7048
|
+
tool: ["role", "callId", "output", "error"],
|
|
7049
|
+
event: ["role", "kind", "refersTo", "data"],
|
|
7050
|
+
};
|
|
7051
|
+
/**
|
|
7052
|
+
* The history's entries and every refusal of their shape. A user entry
|
|
7053
|
+
* starts a turn and the last one's turn is the current one; every call is
|
|
7054
|
+
* answered exactly once, by a tool entry that follows its assistant entry;
|
|
7055
|
+
* an event precedes the current turn's user entry (a live event reaches the
|
|
7056
|
+
* model only in a later turn's history).
|
|
7057
|
+
*/
|
|
7058
|
+
function agentTestReadHistory(raw, names, out) {
|
|
7059
|
+
if (agentTestIsUnset(raw)) {
|
|
7060
|
+
out.push(agentTestRefusal("history is required on an agent case: a JSON array of user, assistant, tool and event entries, oldest first"));
|
|
7061
|
+
return null;
|
|
7062
|
+
}
|
|
7063
|
+
if (typeof raw !== "string") {
|
|
7064
|
+
out.push(agentTestRefusal("history must be JSON text (a JSON array written as one string)"));
|
|
7065
|
+
return null;
|
|
7066
|
+
}
|
|
7067
|
+
let parsed;
|
|
7068
|
+
try {
|
|
7069
|
+
parsed = JSON.parse(raw);
|
|
7070
|
+
}
|
|
7071
|
+
catch (error) {
|
|
7072
|
+
out.push(agentTestRefusal(`history is not valid JSON (${error?.message ?? "parse failed"})`));
|
|
7073
|
+
return null;
|
|
7074
|
+
}
|
|
7075
|
+
if (!Array.isArray(parsed)) {
|
|
7076
|
+
out.push(agentTestRefusal("history must be a JSON array of entries"));
|
|
7077
|
+
return null;
|
|
7078
|
+
}
|
|
7079
|
+
const entries = [];
|
|
7080
|
+
const calls = new Map();
|
|
7081
|
+
/** The calls of the last assistant entry, which tool entries may answer. */
|
|
7082
|
+
let open = new Set();
|
|
7083
|
+
let seenUser = false;
|
|
7084
|
+
const lastUser = parsed.reduce((at, entry, index) => (agentTestIsObject(entry) && entry.role === "user" ? index : at), -1);
|
|
7085
|
+
if (lastUser === -1) {
|
|
7086
|
+
out.push(agentTestRefusal("history needs at least one user entry; the last one is the round's current turn"));
|
|
7087
|
+
}
|
|
7088
|
+
parsed.forEach((entry, index) => {
|
|
7089
|
+
const at = `history[${index}]`;
|
|
7090
|
+
if (!agentTestIsObject(entry)) {
|
|
7091
|
+
out.push(agentTestRefusal(`${at} must be an object with a role`));
|
|
7092
|
+
return;
|
|
7093
|
+
}
|
|
7094
|
+
// An own-property read: a role such as "constructor" or "__proto__" must
|
|
7095
|
+
// not resolve to an inherited member of the table.
|
|
7096
|
+
const allowed = typeof entry.role === "string" && Object.prototype.hasOwnProperty.call(AGENT_TEST_ENTRY_KEYS, entry.role)
|
|
7097
|
+
? AGENT_TEST_ENTRY_KEYS[entry.role]
|
|
7098
|
+
: undefined;
|
|
7099
|
+
if (!allowed) {
|
|
7100
|
+
out.push(agentTestRefusal(`${at}.role must be "user", "assistant", "tool" or "event"; found ${JSON.stringify(entry.role)}`));
|
|
7101
|
+
return;
|
|
7102
|
+
}
|
|
7103
|
+
const unknown = Object.keys(entry).filter((key) => !allowed.includes(key));
|
|
7104
|
+
if (unknown.length > 0) {
|
|
7105
|
+
out.push(agentTestRefusal(`${at} has ${unknown.map((k) => `"${k}"`).join(", ")}, which a ${entry.role} entry does not carry; allowed: ${allowed.join(", ")}`));
|
|
7106
|
+
}
|
|
7107
|
+
if (entry.role === "user") {
|
|
7108
|
+
if (typeof entry.text !== "string" || entry.text === "") {
|
|
7109
|
+
out.push(agentTestRefusal(`${at}.text must be a non-empty string`));
|
|
7110
|
+
return;
|
|
7111
|
+
}
|
|
7112
|
+
open = new Set();
|
|
7113
|
+
seenUser = true;
|
|
7114
|
+
entries.push({ role: "user", text: entry.text });
|
|
7115
|
+
return;
|
|
7116
|
+
}
|
|
7117
|
+
if (entry.role === "assistant") {
|
|
7118
|
+
if (!seenUser) {
|
|
7119
|
+
out.push(agentTestRefusal(`${at} is an assistant entry before the first user entry; a turn starts with a user entry`));
|
|
7120
|
+
}
|
|
7121
|
+
const hasText = entry.text !== undefined;
|
|
7122
|
+
const hasCalls = entry.calls !== undefined;
|
|
7123
|
+
if (hasText && typeof entry.text !== "string") {
|
|
7124
|
+
out.push(agentTestRefusal(`${at}.text must be a string`));
|
|
7125
|
+
}
|
|
7126
|
+
if (entry.reasoning !== undefined && typeof entry.reasoning !== "string") {
|
|
7127
|
+
out.push(agentTestRefusal(`${at}.reasoning must be a string`));
|
|
7128
|
+
}
|
|
7129
|
+
const read = [];
|
|
7130
|
+
if (hasCalls) {
|
|
7131
|
+
if (!Array.isArray(entry.calls) || entry.calls.length === 0) {
|
|
7132
|
+
out.push(agentTestRefusal(`${at}.calls must be a non-empty array of { callId, name, arguments }`));
|
|
7133
|
+
}
|
|
7134
|
+
else {
|
|
7135
|
+
entry.calls.forEach((call, callIndex) => {
|
|
7136
|
+
const callAt = `${at}.calls[${callIndex}]`;
|
|
7137
|
+
if (!agentTestIsObject(call) ||
|
|
7138
|
+
!agentTestIsText(call.callId) ||
|
|
7139
|
+
!agentTestIsText(call.name) ||
|
|
7140
|
+
!("arguments" in call) ||
|
|
7141
|
+
Object.keys(call).some((key) => !["callId", "name", "arguments"].includes(key))) {
|
|
7142
|
+
out.push(agentTestRefusal(`${callAt} must be { callId, name, arguments }`));
|
|
7143
|
+
return;
|
|
7144
|
+
}
|
|
7145
|
+
if (calls.has(call.callId)) {
|
|
7146
|
+
out.push(agentTestRefusal(`${callAt}: call id "${call.callId}" is used twice; call ids are unique across the history`));
|
|
7147
|
+
return;
|
|
7148
|
+
}
|
|
7149
|
+
calls.set(call.callId, { answered: false });
|
|
7150
|
+
read.push({ callId: call.callId, name: call.name, arguments: call.arguments });
|
|
7151
|
+
});
|
|
7152
|
+
}
|
|
7153
|
+
}
|
|
7154
|
+
if (!(typeof entry.text === "string" && entry.text !== "") && read.length === 0 && !hasCalls) {
|
|
7155
|
+
out.push(agentTestRefusal(`${at} needs text or calls`));
|
|
7156
|
+
}
|
|
7157
|
+
open = new Set(read.map((call) => call.callId));
|
|
7158
|
+
entries.push({
|
|
7159
|
+
role: "assistant",
|
|
7160
|
+
...(typeof entry.text === "string" ? { text: entry.text } : {}),
|
|
7161
|
+
...(typeof entry.reasoning === "string" ? { reasoning: entry.reasoning } : {}),
|
|
7162
|
+
...(read.length > 0 ? { calls: read } : {}),
|
|
7163
|
+
});
|
|
7164
|
+
return;
|
|
7165
|
+
}
|
|
7166
|
+
if (entry.role === "tool") {
|
|
7167
|
+
if (!agentTestIsText(entry.callId)) {
|
|
7168
|
+
out.push(agentTestRefusal(`${at}.callId must name a call of an earlier assistant entry`));
|
|
7169
|
+
return;
|
|
7170
|
+
}
|
|
7171
|
+
const hasOutput = "output" in entry;
|
|
7172
|
+
const hasError = "error" in entry;
|
|
7173
|
+
if (hasOutput === hasError) {
|
|
7174
|
+
out.push(agentTestRefusal(`${at} must carry exactly one of output or error`));
|
|
7175
|
+
return;
|
|
7176
|
+
}
|
|
7177
|
+
const call = calls.get(entry.callId);
|
|
7178
|
+
if (!call) {
|
|
7179
|
+
out.push(agentTestRefusal(`${at} answers call "${entry.callId}", which no earlier assistant entry makes`));
|
|
7180
|
+
return;
|
|
7181
|
+
}
|
|
7182
|
+
if (call.answered) {
|
|
7183
|
+
out.push(agentTestRefusal(`${at} is a second result for call "${entry.callId}"; every call has exactly one`));
|
|
7184
|
+
return;
|
|
7185
|
+
}
|
|
7186
|
+
if (!open.has(entry.callId)) {
|
|
7187
|
+
out.push(agentTestRefusal(`${at} answers call "${entry.callId}", which is not a call of the assistant entry just before it; ` +
|
|
7188
|
+
"a result follows its call"));
|
|
7189
|
+
return;
|
|
7190
|
+
}
|
|
7191
|
+
call.answered = true;
|
|
7192
|
+
if (hasError) {
|
|
7193
|
+
const error = entry.error;
|
|
7194
|
+
if (!agentTestIsObject(error) || !agentTestIsText(error.code) || typeof error.message !== "string") {
|
|
7195
|
+
out.push(agentTestRefusal(`${at}.error must be { code, message }`));
|
|
7196
|
+
return;
|
|
7197
|
+
}
|
|
7198
|
+
entries.push({ role: "tool", callId: entry.callId, error: { code: error.code, message: error.message } });
|
|
7199
|
+
return;
|
|
7200
|
+
}
|
|
7201
|
+
entries.push({ role: "tool", callId: entry.callId, output: entry.output });
|
|
7202
|
+
return;
|
|
7203
|
+
}
|
|
7204
|
+
// An event.
|
|
7205
|
+
if (!agentTestIsText(entry.kind)) {
|
|
7206
|
+
out.push(agentTestRefusal(`${at}.kind must name an event kind the agent declares`));
|
|
7207
|
+
return;
|
|
7208
|
+
}
|
|
7209
|
+
if (names && !names.events.has(entry.kind)) {
|
|
7210
|
+
out.push(agentTestRefusal(`${at} has event kind "${entry.kind}", which the agent does not declare` +
|
|
7211
|
+
(names.events.size > 0 ? ` (declared: ${[...names.events].join(", ")})` : "")));
|
|
7212
|
+
}
|
|
7213
|
+
if (entry.refersTo !== undefined && (!agentTestIsText(entry.refersTo) || !calls.has(entry.refersTo))) {
|
|
7214
|
+
out.push(agentTestRefusal(`${at}.refersTo names call ${JSON.stringify(entry.refersTo)}, which no earlier assistant entry makes`));
|
|
7215
|
+
}
|
|
7216
|
+
if (lastUser !== -1 && index > lastUser) {
|
|
7217
|
+
out.push(agentTestRefusal(`${at} is an event after the current turn's user entry; a live event reaches the model only in a ` +
|
|
7218
|
+
"later turn, so place it before the last user entry"));
|
|
7219
|
+
}
|
|
7220
|
+
entries.push({
|
|
7221
|
+
role: "event",
|
|
7222
|
+
kind: entry.kind,
|
|
7223
|
+
...(typeof entry.refersTo === "string" ? { refersTo: entry.refersTo } : {}),
|
|
7224
|
+
data: entry.data ?? null,
|
|
7225
|
+
});
|
|
7226
|
+
});
|
|
7227
|
+
for (const [callId, call] of calls) {
|
|
7228
|
+
if (!call.answered) {
|
|
7229
|
+
out.push(agentTestRefusal(`call "${callId}" has no result; write one tool entry for it (an unanswered call as an error, ` +
|
|
7230
|
+
'e.g. { "code": "superseded", "message": "…" })'));
|
|
7231
|
+
}
|
|
7232
|
+
}
|
|
7233
|
+
const last = parsed[parsed.length - 1];
|
|
7234
|
+
if (agentTestIsObject(last) && last.role === "assistant") {
|
|
7235
|
+
out.push(agentTestRefusal("history ends with an assistant entry; a round starts from a user entry or a tool result"));
|
|
7236
|
+
}
|
|
7237
|
+
return entries;
|
|
7238
|
+
}
|
|
7239
|
+
function agentTestReadExpectedToolCall(raw, names, out) {
|
|
7240
|
+
if (agentTestIsUnset(raw))
|
|
7241
|
+
return null;
|
|
7242
|
+
const parsed = agentTestJsonValue(raw);
|
|
7243
|
+
const value = parsed.ok ? parsed.value : undefined;
|
|
7244
|
+
if (!agentTestIsObject(value) ||
|
|
7245
|
+
!agentTestIsText(value.name) ||
|
|
7246
|
+
Object.keys(value).some((key) => key !== "name" && key !== "arguments")) {
|
|
7247
|
+
out.push(agentTestRefusal("expectedToolCall must be { name, arguments? } (arguments is matched as a subset)"));
|
|
7248
|
+
return null;
|
|
7249
|
+
}
|
|
7250
|
+
if (names && !names.tools.has(value.name)) {
|
|
7251
|
+
out.push(agentTestRefusal(`expectedToolCall names tool "${value.name}", which the agent does not declare` +
|
|
7252
|
+
(names.tools.size > 0 ? ` (declared: ${[...names.tools].join(", ")})` : "")));
|
|
7253
|
+
}
|
|
7254
|
+
return { name: value.name, ...("arguments" in value ? { arguments: value.arguments } : {}) };
|
|
7255
|
+
}
|
|
7256
|
+
/**
|
|
7257
|
+
* The one rule: is this case's agent block something a round can be built
|
|
7258
|
+
* from? `{ ok: true, testCase }` carries it read (`null` on a case whose
|
|
7259
|
+
* prompt is not an agent). On a chat or decisions case, every agent key
|
|
7260
|
+
* holding a non-null value is refused by name; an explicit `null` is unset on
|
|
7261
|
+
* any case, which is how a full-state update clears one.
|
|
7262
|
+
*/
|
|
7263
|
+
export function resolveAgentTestCase(input) {
|
|
7264
|
+
const { fields } = input;
|
|
7265
|
+
const kind = agentTestIsUnset(input.kind) ? "chat" : String(input.kind).trim();
|
|
7266
|
+
const refusals = [];
|
|
7267
|
+
if (kind !== "agent") {
|
|
7268
|
+
const owner = kind === "chat" || kind === "decisions" ? `a ${kind} prompt` : `a ${kind}`;
|
|
7269
|
+
for (const key of AGENT_TEST_CASE_FIELDS) {
|
|
7270
|
+
const value = fields[key];
|
|
7271
|
+
if (value !== null && value !== undefined) {
|
|
7272
|
+
refusals.push(agentTestRefusal(`${key} is set on a case of ${owner}; only an agent prompt's case carries the agent block. ` +
|
|
7273
|
+
"Remove it, or send it as null"));
|
|
7274
|
+
}
|
|
7275
|
+
}
|
|
7276
|
+
return refusals.length > 0 ? { ok: false, refusals } : { ok: true, testCase: null };
|
|
7277
|
+
}
|
|
7278
|
+
const names = agentTestDeclaredNames(input.declaration, "declaration" in input);
|
|
7279
|
+
const history = agentTestReadHistory(fields.agentHistory, names, refusals);
|
|
7280
|
+
let context = null;
|
|
7281
|
+
if (!agentTestIsUnset(fields.agentContext)) {
|
|
7282
|
+
const parsed = agentTestJsonValue(fields.agentContext);
|
|
7283
|
+
if (parsed.ok && agentTestIsObject(parsed.value))
|
|
7284
|
+
context = parsed.value;
|
|
7285
|
+
else
|
|
7286
|
+
refusals.push(agentTestRefusal("context must be a JSON object (turn.* for the round)"));
|
|
7287
|
+
}
|
|
7288
|
+
const text = (key, label, example) => {
|
|
7289
|
+
const value = fields[key];
|
|
7290
|
+
if (value === null || value === undefined)
|
|
7291
|
+
return null;
|
|
7292
|
+
if (typeof value !== "string" || value === "") {
|
|
7293
|
+
refusals.push(agentTestRefusal(`${label} must be ${example}`));
|
|
7294
|
+
return null;
|
|
7295
|
+
}
|
|
7296
|
+
return value;
|
|
7297
|
+
};
|
|
7298
|
+
const timezone = text("agentTimezone", "timezone", 'an IANA time zone name such as "America/New_York"');
|
|
7299
|
+
const locale = text("agentLocale", "locale", 'a BCP 47 language tag such as "en-US"');
|
|
7300
|
+
const expectedToolCall = agentTestReadExpectedToolCall(fields.agentExpectedToolCall, names, refusals);
|
|
7301
|
+
let expectedNoToolCall = false;
|
|
7302
|
+
const noCall = fields.agentExpectedNoToolCall;
|
|
7303
|
+
if (noCall !== null && noCall !== undefined) {
|
|
7304
|
+
if (typeof noCall !== "boolean")
|
|
7305
|
+
refusals.push(agentTestRefusal("expectedNoToolCall must be true or false"));
|
|
7306
|
+
else
|
|
7307
|
+
expectedNoToolCall = noCall;
|
|
7308
|
+
}
|
|
7309
|
+
if (expectedToolCall && expectedNoToolCall) {
|
|
7310
|
+
refusals.push(agentTestRefusal("sets both expectedToolCall and expectedNoToolCall = true; a round cannot both call a tool and call none"));
|
|
7311
|
+
}
|
|
7312
|
+
const subset = fields.expectedJsonSubset;
|
|
7313
|
+
if (!agentTestIsUnset(subset) && !(agentTestIsObject(subset) && Object.keys(subset).length === 0) && subset !== "{}") {
|
|
7314
|
+
refusals.push(agentTestRefusal("expectedJsonSubset is not used on an agent case: an agent's answer is text, and anything structured is a " +
|
|
7315
|
+
"tool call. Assert it with expectedToolCall"));
|
|
7316
|
+
}
|
|
7317
|
+
if (refusals.length > 0 || !history)
|
|
7318
|
+
return { ok: false, refusals };
|
|
7319
|
+
return {
|
|
7320
|
+
ok: true,
|
|
7321
|
+
testCase: { history, context, timezone, locale, expectedToolCall, expectedNoToolCall },
|
|
7322
|
+
};
|
|
7323
|
+
}
|
|
6905
7324
|
// ── src/config-surface/prompt.ts ─────────────────────────────────────────
|
|
6906
7325
|
/**
|
|
6907
7326
|
* The `prompt` configuration object's definition (issue #2644, phase 2).
|
|
@@ -8246,8 +8665,8 @@ export const CRON_TRIGGER_SURFACE = {
|
|
|
8246
8665
|
* halves of the access model (#1020), and the only access keys: at most one may
|
|
8247
8666
|
* change per request, which is the controller's rule and stays there. The rule
|
|
8248
8667
|
* set is authored by name (`ruleSetName`) and sent as `ruleSetId`, as the type
|
|
8249
|
-
* configs do (#4116)
|
|
8250
|
-
* `accessPolicy` spelling
|
|
8668
|
+
* configs do (#4116). The id spelling in TOML (#4283) and the pre-#1020
|
|
8669
|
+
* `accessPolicy` spelling (#3996) are retired keys (`retired-keys.ts`).
|
|
8251
8670
|
*/
|
|
8252
8671
|
const BLOB_BUCKET_HANDLER = "src/app-api/controllers/blob-buckets-controller.ts";
|
|
8253
8672
|
export const BLOB_BUCKET_SURFACE = {
|
|
@@ -8303,7 +8722,7 @@ export const BLOB_BUCKET_SURFACE = {
|
|
|
8303
8722
|
// Authored as a name, sent as an id (#4116), so a tree copied into
|
|
8304
8723
|
// another app binds that app's rule set. Existence-checked against
|
|
8305
8724
|
// AccessRuleSet; mutually exclusive with a preset change in the same
|
|
8306
|
-
// request.
|
|
8725
|
+
// request. The TOML spelling of the id is a retired key (#4283).
|
|
8307
8726
|
validation: handledBy(BLOB_BUCKET_HANDLER, "BlobBucketsController", "apply"),
|
|
8308
8727
|
},
|
|
8309
8728
|
],
|
|
@@ -8353,14 +8772,6 @@ export const BLOB_BUCKET_SURFACE = {
|
|
|
8353
8772
|
note: "The bucket key. `config pull` writes it for readability; push takes " +
|
|
8354
8773
|
"the identity from the file name and attaches `bucketKey` itself.",
|
|
8355
8774
|
},
|
|
8356
|
-
ruleSetId: {
|
|
8357
|
-
kind: "deprecated",
|
|
8358
|
-
note: "The id spelling of the rule-set reference (#4116). An id names a " +
|
|
8359
|
-
"rule set in one app, so a copied tree cannot resolve it; " +
|
|
8360
|
-
"`ruleSetName` is the authored key. Still accepted on push, with " +
|
|
8361
|
-
"an advisory line, and `config pull` rewrites the file to " +
|
|
8362
|
-
"`ruleSetName`. Retiring it is a later change of its own.",
|
|
8363
|
-
},
|
|
8364
8775
|
},
|
|
8365
8776
|
responseOnlyKeys: {},
|
|
8366
8777
|
},
|
|
@@ -9219,7 +9630,7 @@ export const TRANSFORM_SURFACE = {
|
|
|
9219
9630
|
* config version (`ServerFunctionConfig`), but it is ONE table in the file: an
|
|
9220
9631
|
* author does not write a version, they write a function and push it. The
|
|
9221
9632
|
* definition therefore declares the header's field surface and lists the
|
|
9222
|
-
* version-carried keys (`entry`, `
|
|
9633
|
+
* version-carried keys (`entry`, `limits`) in `tomlOnlyKeys` beside
|
|
9223
9634
|
* `key` — they are authored, they are not fields of the header model, and they
|
|
9224
9635
|
* travel on the config-version route rather than in the header body. Declaring
|
|
9225
9636
|
* a second table over `ServerFunctionConfig` at the same `tomlPath` is not
|
|
@@ -9307,6 +9718,20 @@ const TRIGGER_DECLARATION_RULES = functionRules("preflight", "src/config-surface
|
|
|
9307
9718
|
["cron-trigger-expression-required", "A cron entry carries a `cron` expression."],
|
|
9308
9719
|
["cron-trigger-overlap-policy", "`overlapPolicy` is `skip` or `allow`."],
|
|
9309
9720
|
]);
|
|
9721
|
+
/**
|
|
9722
|
+
* The retired runtime keys (#3188). `mode`, `durable` and a cron entry's
|
|
9723
|
+
* `mode` were accepted with a warning through phase 4 (#3482); phase 5 refuses
|
|
9724
|
+
* them, first in the preflight's order, so an author reads them in the order
|
|
9725
|
+
* their file carries them.
|
|
9726
|
+
*/
|
|
9727
|
+
const RETIRED_RUNTIME_KEY_RULES = functionRules("preflight", "src/config-surface/function-retired-keys.ts", "refusedFunctionKeys", [
|
|
9728
|
+
[
|
|
9729
|
+
"retired-runtime-key",
|
|
9730
|
+
"`mode`, `durable` and a cron entry's `mode` are refused: the caller " +
|
|
9731
|
+
"picks the runtime at each call (`invoke` or `start`) and every cron " +
|
|
9732
|
+
"fire starts a task run, so the file has nothing to say. Delete the line.",
|
|
9733
|
+
],
|
|
9734
|
+
]);
|
|
9310
9735
|
/**
|
|
9311
9736
|
* The file-decidable half of the webhook `signingSecret` rule (#3320). The
|
|
9312
9737
|
* SAME handler under the SAME ids is declared `apply` on the standalone
|
|
@@ -9508,24 +9933,21 @@ export const SERVER_FUNCTION_SURFACE = {
|
|
|
9508
9933
|
},
|
|
9509
9934
|
mode: {
|
|
9510
9935
|
kind: "structural",
|
|
9511
|
-
note: "RETIRED (#3482)
|
|
9512
|
-
"
|
|
9513
|
-
"
|
|
9514
|
-
"
|
|
9515
|
-
"
|
|
9516
|
-
"
|
|
9517
|
-
"
|
|
9518
|
-
"
|
|
9519
|
-
"
|
|
9520
|
-
"under one runtime says so in code with `assertRuntime`.",
|
|
9936
|
+
note: "RETIRED (#3482) and REFUSED from phase 5 (#3188) by the " +
|
|
9937
|
+
"`retired-runtime-key` rule, whose sentence names the key and " +
|
|
9938
|
+
"says to delete the line. A function is a function: the CALLER " +
|
|
9939
|
+
"picks the runtime at each call, with `invoke` running it inside " +
|
|
9940
|
+
"the request and `start` running it as a task, cron fires always " +
|
|
9941
|
+
"starting a task run and webhook deliveries always running as a " +
|
|
9942
|
+
"request. Listed here so the refusal is the rule's sentence rather " +
|
|
9943
|
+
"than the generic unknown-key one; `config pull` removes it from a " +
|
|
9944
|
+
"version pushed before the retirement.",
|
|
9521
9945
|
},
|
|
9522
9946
|
durable: {
|
|
9523
9947
|
kind: "structural",
|
|
9524
|
-
note: "RETIRED (#3482)
|
|
9525
|
-
"
|
|
9526
|
-
"function, and there is no such thing any more.
|
|
9527
|
-
"carries it pushes with a warning, `config pull` removes it, and " +
|
|
9528
|
-
"phase 5 refuses it.",
|
|
9948
|
+
note: "RETIRED (#3482) and REFUSED from phase 5 (#3188) — the same as " +
|
|
9949
|
+
"`mode`, whose alias it was, by the same rule. It named one runner " +
|
|
9950
|
+
"for the whole function, and there is no such thing any more.",
|
|
9529
9951
|
},
|
|
9530
9952
|
limits: {
|
|
9531
9953
|
kind: "structural",
|
|
@@ -9563,11 +9985,12 @@ export const SERVER_FUNCTION_SURFACE = {
|
|
|
9563
9985
|
tomlDocumentKeys: {},
|
|
9564
9986
|
// ── Cross-field rules (#3374) ─────────────────────────────────────────────
|
|
9565
9987
|
//
|
|
9566
|
-
// Single-field refusals inside these modules (`mode`
|
|
9988
|
+
// Single-field refusals inside these modules (a retired `mode` key, a webhook
|
|
9567
9989
|
// block's `verificationScheme`) are declared here too: this is the only
|
|
9568
9990
|
// id-bearing slot until #3375 gives `ConfigField.validation` a stage, and a
|
|
9569
9991
|
// module's list must equal its declarations in both directions.
|
|
9570
9992
|
crossFieldRules: [
|
|
9993
|
+
...RETIRED_RUNTIME_KEY_RULES,
|
|
9571
9994
|
...TRIGGER_DECLARATION_RULES,
|
|
9572
9995
|
...SIGNING_SECRET_RULES,
|
|
9573
9996
|
// #3376 — after the signing-secret group, mirroring the order
|
|
@@ -9601,8 +10024,23 @@ export const SERVER_FUNCTION_SURFACE = {
|
|
|
9601
10024
|
* WIRE shape is the parsed value. That asymmetry is the handler's, not the
|
|
9602
10025
|
* definition's: `parseTestCaseToml` parses on push and the admin API
|
|
9603
10026
|
* re-stringifies on store.
|
|
10027
|
+
*
|
|
10028
|
+
* #4262 — an agent prompt's case carries one more block, `[test.agent]`: the
|
|
10029
|
+
* seeded history its single round starts from, the round's turn context, zone
|
|
10030
|
+
* and tag, and two assertions on the round's calls. Six flat fields on the
|
|
10031
|
+
* wire, each `whenSet`, so a chat case's sidecar never grows an empty block.
|
|
10032
|
+
* `history` is typed `string` — JSON text in TOML, on the wire and in storage —
|
|
10033
|
+
* because `isEmptyForEmit` reads every array as empty for a `json` field, so a
|
|
10034
|
+
* `whenSet` array would never pull and would compare equal to anything.
|
|
9604
10035
|
*/
|
|
9605
10036
|
const TEST_CASE_HANDLER = "src/admin-api.ts";
|
|
10037
|
+
/**
|
|
10038
|
+
* #4262 — the agent block's one rule, vendored: the admin routes call it on
|
|
10039
|
+
* create and update, the CLI's preflight on an agent prompt's sidecar. Its
|
|
10040
|
+
* schema half (input, context and event schemas) needs the app's agent, so the
|
|
10041
|
+
* stage is `apply`.
|
|
10042
|
+
*/
|
|
10043
|
+
const AGENT_TEST_CASE_RULE = handledBy("src/config-surface/agent-test-case.ts", "resolveAgentTestCase", "apply");
|
|
9606
10044
|
/**
|
|
9607
10045
|
* ── Why this module's CLI handler is declared `apply` (#3375) ─────────────
|
|
9608
10046
|
*
|
|
@@ -9704,7 +10142,72 @@ export const TEST_CASE_SURFACE = {
|
|
|
9704
10142
|
// JSON object, deep-partial-matched against the parsed output.
|
|
9705
10143
|
validation: handledBy(CLI_SYNC, "parseTestCaseToml", "apply"),
|
|
9706
10144
|
},
|
|
10145
|
+
// #4262 — `[test.agent]`, an agent prompt's case only.
|
|
10146
|
+
{
|
|
10147
|
+
field: "agentHistory",
|
|
10148
|
+
tomlKey: "history",
|
|
10149
|
+
tomlGroup: "agent",
|
|
10150
|
+
// JSON text of the seeded history, stored as authored.
|
|
10151
|
+
type: "string",
|
|
10152
|
+
emit: "whenSet",
|
|
10153
|
+
writableOn: BOTH,
|
|
10154
|
+
validation: AGENT_TEST_CASE_RULE,
|
|
10155
|
+
},
|
|
10156
|
+
{
|
|
10157
|
+
field: "agentContext",
|
|
10158
|
+
tomlKey: "context",
|
|
10159
|
+
tomlGroup: "agent",
|
|
10160
|
+
// `turn.*` for the round; TOML carries the JSON text.
|
|
10161
|
+
type: "json",
|
|
10162
|
+
emit: "whenSet",
|
|
10163
|
+
writableOn: BOTH,
|
|
10164
|
+
validation: AGENT_TEST_CASE_RULE,
|
|
10165
|
+
},
|
|
10166
|
+
{
|
|
10167
|
+
field: "agentTimezone",
|
|
10168
|
+
tomlKey: "timezone",
|
|
10169
|
+
tomlGroup: "agent",
|
|
10170
|
+
type: "string",
|
|
10171
|
+
emit: "whenSet",
|
|
10172
|
+
writableOn: BOTH,
|
|
10173
|
+
validation: AGENT_TEST_CASE_RULE,
|
|
10174
|
+
},
|
|
10175
|
+
{
|
|
10176
|
+
field: "agentLocale",
|
|
10177
|
+
tomlKey: "locale",
|
|
10178
|
+
tomlGroup: "agent",
|
|
10179
|
+
type: "string",
|
|
10180
|
+
emit: "whenSet",
|
|
10181
|
+
writableOn: BOTH,
|
|
10182
|
+
validation: AGENT_TEST_CASE_RULE,
|
|
10183
|
+
},
|
|
10184
|
+
{
|
|
10185
|
+
field: "agentExpectedToolCall",
|
|
10186
|
+
tomlKey: "expectedToolCall",
|
|
10187
|
+
tomlGroup: "agent",
|
|
10188
|
+
// `{ name, arguments? }`; TOML carries the JSON text.
|
|
10189
|
+
type: "json",
|
|
10190
|
+
emit: "whenSet",
|
|
10191
|
+
writableOn: BOTH,
|
|
10192
|
+
validation: AGENT_TEST_CASE_RULE,
|
|
10193
|
+
},
|
|
10194
|
+
{
|
|
10195
|
+
field: "agentExpectedNoToolCall",
|
|
10196
|
+
tomlKey: "expectedNoToolCall",
|
|
10197
|
+
tomlGroup: "agent",
|
|
10198
|
+
type: "boolean",
|
|
10199
|
+
emit: "whenSet",
|
|
10200
|
+
writableOn: BOTH,
|
|
10201
|
+
validation: AGENT_TEST_CASE_RULE,
|
|
10202
|
+
},
|
|
9707
10203
|
],
|
|
10204
|
+
tomlGroups: {
|
|
10205
|
+
agent: {
|
|
10206
|
+
note: "#4262 — `[test.agent]`: the seeded history, turn context and " +
|
|
10207
|
+
"tool-call assertions of a single-round test on an agent prompt. " +
|
|
10208
|
+
"Refused on any other case.",
|
|
10209
|
+
},
|
|
10210
|
+
},
|
|
9708
10211
|
notExposed: {
|
|
9709
10212
|
testCaseId: {
|
|
9710
10213
|
kind: "server-owned",
|
|
@@ -10561,6 +11064,12 @@ export const GENERATED_CONFIG_MODEL_FIELDS = {
|
|
|
10561
11064
|
"evaluatorPromptId",
|
|
10562
11065
|
"evaluatorConfigId",
|
|
10563
11066
|
"attachments",
|
|
11067
|
+
"agentHistory",
|
|
11068
|
+
"agentContext",
|
|
11069
|
+
"agentTimezone",
|
|
11070
|
+
"agentLocale",
|
|
11071
|
+
"agentExpectedToolCall",
|
|
11072
|
+
"agentExpectedNoToolCall",
|
|
10564
11073
|
"createdBy",
|
|
10565
11074
|
"createdAt",
|
|
10566
11075
|
"modifiedAt",
|