@m6d/cortex-cli 0.22.0 → 0.24.0

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.
@@ -1,6 +1,9 @@
1
1
  import { z } from "zod";
2
2
  import { datasetIdentifierSchema } from "../datasets/index";
3
3
  import { canCarrySecret } from "../egress-policy/index";
4
+ import { isRecord } from "../json/index";
5
+ import { linearPattern } from "../pattern/index";
6
+ import { triggerScheduleSchema } from "../schedule/index";
4
7
  import {
5
8
  displayNameSchema,
6
9
  EVENT_HEADER,
@@ -11,13 +14,13 @@ import {
11
14
  refuseBlockedApprovalTitle,
12
15
  serverToolSchema,
13
16
  } from "../server/index";
14
- import { MESSAGE_TRIGGER_NAME } from "../triggers/index";
15
17
  import {
16
18
  BASE_LOCALE,
17
19
  byLocale,
18
20
  DEFAULT_MEDIA_FORMS,
19
21
  MEDIA_FORMS,
20
22
  TOOL_SURFACES,
23
+ WHILE_BUSY,
21
24
  type Greeting,
22
25
  type Suggestions,
23
26
  } from "../wire/index";
@@ -502,7 +505,7 @@ export function headerListSchema<TEntry extends z.ZodType<{ header: string }>>(
502
505
  });
503
506
  }
504
507
 
505
- /** A variable by name: one the agent's server declares, or a built-in (`timezone`, `locale`, `utcTime`). */
508
+ /** A variable by name: one the agent's server declares, or a built-in (`timezone`, `locale`, `utcTime`, `threadId`). */
506
509
  const variableSourceSchema = z.object({
507
510
  kind: z.literal("variable"),
508
511
  name: z.string().trim().min(1).max(64),
@@ -522,7 +525,7 @@ const constantHeaderSourceSchema = z.object({
522
525
 
523
526
  /**
524
527
  * Where a header's value comes from: a credential, the run's own token (the
525
- * end user's in a chat, the trigger's in a firing), a variable, or a
528
+ * end user's in a chat, the trigger's in a trigger run), a variable, or a
526
529
  * constant. A run token or a variable needs a run; a call without one (a
527
530
  * webhook delivery, a catalog read) presents only credentials and constants.
528
531
  */
@@ -676,47 +679,251 @@ const propertyOrderSchema = z
676
679
  .refine((names) => new Set(names).size === names.length, "a name repeats")
677
680
  .optional();
678
681
 
679
- /** What an array holds: a scalar, or an object with properties of its own. */
680
- const itemsSchema = z.strictObject({
681
- type: z.enum([...SCALAR_TYPES, "object"]),
682
- get properties() {
683
- return z.record(propertyNameSchema, schemaPropertySchema).optional();
684
- },
685
- required: z.array(z.string()).optional(),
686
- "x-order": propertyOrderSchema,
682
+ const countSchema = z.number().int().min(0).optional();
683
+
684
+ /**
685
+ * The JSON Schema keywords that bound one value beyond its type and enum:
686
+ * a string's `pattern` (one RE2 can match, as Cortex checks it) and lengths,
687
+ * a scalar's `format` (kept for the model, not checked), a number's range and step, an
688
+ * array's length and uniqueness, and a scalar's `const`.
689
+ */
690
+ export const valueKeywordsSchema = z.strictObject({
691
+ pattern: z
692
+ .string()
693
+ .min(1)
694
+ .max(1_000)
695
+ .refine((pattern) => linearPattern(pattern) !== undefined, "a pattern Cortex cannot match")
696
+ .optional(),
697
+ minLength: countSchema,
698
+ maxLength: countSchema,
699
+ format: z.string().trim().min(1).max(100).optional(),
700
+ minimum: z.number().optional(),
701
+ maximum: z.number().optional(),
702
+ exclusiveMinimum: z.number().optional(),
703
+ exclusiveMaximum: z.number().optional(),
704
+ multipleOf: z.number().positive().optional(),
705
+ minItems: countSchema,
706
+ maxItems: countSchema,
707
+ uniqueItems: z.boolean().optional(),
708
+ const: z.union([z.string().max(4_000), z.number(), z.boolean()]).optional(),
687
709
  });
710
+ export type ValueKeywords = z.infer<typeof valueKeywordsSchema>;
711
+
712
+ const NUMERIC_TYPES = ["number", "integer"];
713
+
714
+ /** The types each value keyword bounds; on any other it means nothing. */
715
+ export const VALUE_KEYWORD_TYPES = {
716
+ pattern: ["string"],
717
+ minLength: ["string"],
718
+ maxLength: ["string"],
719
+ format: SCALAR_TYPES,
720
+ minimum: NUMERIC_TYPES,
721
+ maximum: NUMERIC_TYPES,
722
+ exclusiveMinimum: NUMERIC_TYPES,
723
+ exclusiveMaximum: NUMERIC_TYPES,
724
+ multipleOf: NUMERIC_TYPES,
725
+ minItems: ["array"],
726
+ maxItems: ["array"],
727
+ uniqueItems: ["array"],
728
+ const: SCALAR_TYPES,
729
+ } satisfies Record<keyof ValueKeywords, ReadonlyArray<string>>;
730
+
731
+ /** The types a rule tests a value for: `"null"` too, since a rule may tell a null apart. */
732
+ const ruleTypeSchema = z.enum([...SCALAR_TYPES, "object", "array", "null"]);
733
+ type RuleType = z.infer<typeof ruleTypeSchema>;
734
+
735
+ /**
736
+ * An object's cross-field rules: which properties go together (`anyOf`,
737
+ * `oneOf`, `allOf`, `not`, `if`/`then`/`else`, `dependentRequired`,
738
+ * `dependentSchemas`) and how many it holds. A rule names the object's own
739
+ * properties only.
740
+ */
741
+ export type ObjectRules = {
742
+ anyOf?: Condition[];
743
+ oneOf?: Condition[];
744
+ allOf?: Condition[];
745
+ not?: Condition;
746
+ if?: Condition;
747
+ then?: Condition;
748
+ else?: Condition;
749
+ dependentRequired?: Record<string, string[]>;
750
+ dependentSchemas?: Record<string, Condition>;
751
+ minProperties?: number;
752
+ maxProperties?: number;
753
+ };
754
+
755
+ /**
756
+ * A schema a rule checks its object against, in the part of JSON Schema a
757
+ * rule reads: what it requires, its properties' types, enums and value
758
+ * keywords, an array's items, and rules of its own.
759
+ */
760
+ type Condition = ValueKeywords &
761
+ ObjectRules & {
762
+ type?: RuleType | RuleType[];
763
+ enum?: Array<string | number | boolean | null>;
764
+ required?: string[];
765
+ properties?: Record<string, Condition>;
766
+ items?: Condition;
767
+ };
768
+
769
+ const conditionSchema: z.ZodType<Condition, Condition> = z
770
+ .lazy(() =>
771
+ z.strictObject({
772
+ type: z.union([ruleTypeSchema, z.array(ruleTypeSchema).min(1)]).optional(),
773
+ enum: z
774
+ .array(z.union([z.string(), z.number(), z.boolean(), z.null()]))
775
+ .min(1)
776
+ .optional(),
777
+ ...valueKeywordsSchema.shape,
778
+ required: z.array(z.string()).optional(),
779
+ properties: z.record(z.string(), conditionSchema).optional(),
780
+ items: conditionSchema.optional(),
781
+ ...objectRulesSchema.shape,
782
+ }),
783
+ )
784
+ .meta({ id: "SchemaCondition" });
785
+
786
+ /** The members of a rule's `anyOf`, `oneOf` or `allOf`. */
787
+ const conditionsSchema = z.array(conditionSchema).min(1).meta({ id: "SchemaConditions" });
788
+
789
+ export const objectRulesSchema = z.strictObject({
790
+ anyOf: conditionsSchema.optional(),
791
+ oneOf: conditionsSchema.optional(),
792
+ allOf: conditionsSchema.optional(),
793
+ not: conditionSchema.optional(),
794
+ if: conditionSchema.optional(),
795
+ then: conditionSchema.optional(),
796
+ else: conditionSchema.optional(),
797
+ dependentRequired: z.record(z.string(), z.array(z.string())).optional(),
798
+ dependentSchemas: z.record(z.string(), conditionSchema).optional(),
799
+ minProperties: countSchema,
800
+ maxProperties: countSchema,
801
+ }) satisfies z.ZodType<ObjectRules>;
802
+
803
+ /** The keywords that check an object's properties against each other. */
804
+ export const OBJECT_RULE_KEYWORDS = objectRulesSchema.keyof().options;
805
+
806
+ /** A rule keyword's array value, or none when it holds no array. */
807
+ function listed(value: unknown) {
808
+ return Array.isArray(value) ? (value as unknown[]) : [];
809
+ }
810
+
811
+ /** A rule keyword's map value, or an empty one when it holds no map. */
812
+ function keyed(value: unknown) {
813
+ return isRecord(value) ? value : {};
814
+ }
815
+
816
+ /**
817
+ * A rule and every rule nested in it that checks the same object, through
818
+ * its combinators, `not`, `if`/`then`/`else` and `dependentSchemas`, but
819
+ * not inside a property's own value.
820
+ */
821
+ export function ruleNodes(rule: unknown): Array<Record<string, unknown>> {
822
+ if (!isRecord(rule)) return [];
823
+ return [
824
+ rule,
825
+ ...[
826
+ ...["anyOf", "oneOf", "allOf"].flatMap((keyword) => listed(rule[keyword])),
827
+ rule.not,
828
+ rule.if,
829
+ rule.then,
830
+ rule.else,
831
+ ...Object.values(keyed(rule.dependentSchemas)),
832
+ ].flatMap(ruleNodes),
833
+ ];
834
+ }
835
+
836
+ /**
837
+ * The property names a rule reaches on the object it checks: those any of
838
+ * its nodes (`ruleNodes`) requires, reads or makes depend on each other.
839
+ */
840
+ export function ruleNames(rule: unknown) {
841
+ return ruleNodes(rule)
842
+ .flatMap((node) => [
843
+ ...listed(node.required),
844
+ ...Object.keys(keyed(node.properties)),
845
+ ...Object.entries(keyed(node.dependentRequired)).flatMap(([name, names]) => [
846
+ name,
847
+ ...listed(names),
848
+ ]),
849
+ ...Object.keys(keyed(node.dependentSchemas)),
850
+ ])
851
+ .filter((name): name is string => typeof name === "string");
852
+ }
853
+
854
+ /** What an array holds: a scalar (a string's values listed in `enum`, as a property's), or an object with properties of its own; with `"null"` beside its type when an item may be null. */
855
+ type SchemaItems = ValueKeywords &
856
+ ObjectRules & {
857
+ type: ItemType | [ItemType, "null"];
858
+ enum?: Array<string | null>;
859
+ properties?: Record<string, SchemaProperty>;
860
+ required?: string[];
861
+ "x-order"?: string[];
862
+ };
688
863
 
689
864
  /**
690
865
  * A JSON Schema property, in the part of JSON Schema a field editor holds:
691
866
  * a type (with `"null"` beside it when the value may be null), an `enum` of
692
867
  * strings for a string, an object's `properties`, `required` and the
693
- * `x-order` they are shown in, an array's `items`, a `description`, and
694
- * the extensions `x-sensitive` (redacted from logs), `x-source` (top-level
695
- * scalar fields of a tool's input only) and `x-target` (top-level fields of
696
- * a tool's input only).
868
+ * `x-order` they are shown in, an array's `items`, a `description`, the
869
+ * value keywords its type takes (`VALUE_KEYWORD_TYPES`), an object's rules,
870
+ * and the extensions `x-sensitive` (redacted from logs), `x-source`
871
+ * (top-level scalar fields of a tool's input only) and `x-target` (top-level
872
+ * fields of a tool's input only).
697
873
  */
698
- const schemaPropertySchema = z.strictObject({
699
- type: z.union([schemaTypeSchema, z.tuple([schemaTypeSchema, z.literal("null")])]),
700
- description: z.string().trim().max(FIELD_LIMITS.descriptionLength).optional(),
701
- /** A nullable enum lists `null` beside its values, as the console stores one. */
702
- enum: z
703
- .array(z.union([z.string().trim().min(1), z.null()]))
704
- .min(1)
705
- .max(FIELD_LIMITS.enumValues + 1)
706
- .optional(),
707
- get properties() {
708
- return z.record(propertyNameSchema, schemaPropertySchema).optional();
709
- },
710
- required: z.array(z.string()).optional(),
711
- "x-order": propertyOrderSchema,
712
- get items() {
713
- return itemsSchema.optional();
714
- },
715
- "x-sensitive": z.boolean().optional(),
716
- "x-source": fieldSourceSchema.optional(),
717
- "x-target": fieldTargetSchema.optional(),
718
- });
719
- type SchemaProperty = z.infer<typeof schemaPropertySchema>;
874
+ type SchemaProperty = ValueKeywords &
875
+ ObjectRules & {
876
+ type: SchemaType | [SchemaType, "null"];
877
+ description?: string;
878
+ /** A nullable enum lists `null` beside its values, as the console stores one. */
879
+ enum?: Array<string | null>;
880
+ properties?: Record<string, SchemaProperty>;
881
+ required?: string[];
882
+ "x-order"?: string[];
883
+ items?: SchemaItems;
884
+ "x-sensitive"?: boolean;
885
+ "x-source"?: FieldSource;
886
+ "x-target"?: FieldTarget;
887
+ };
888
+ type SchemaType = z.infer<typeof schemaTypeSchema>;
889
+ const itemTypeSchema = z.enum([...SCALAR_TYPES, "object"]);
890
+ type ItemType = z.infer<typeof itemTypeSchema>;
891
+
892
+ /** A nullable enum lists `null` beside its values, as the console stores one. */
893
+ const enumSchema = z
894
+ .array(z.union([z.string().trim().min(1), z.null()]))
895
+ .min(1)
896
+ .max(FIELD_LIMITS.enumValues + 1)
897
+ .optional();
898
+
899
+ const itemsSchema: z.ZodType<SchemaItems, SchemaItems> = z.lazy(() =>
900
+ z.strictObject({
901
+ type: z.union([itemTypeSchema, z.tuple([itemTypeSchema, z.literal("null")])]),
902
+ enum: enumSchema,
903
+ properties: z.record(propertyNameSchema, schemaPropertySchema).optional(),
904
+ required: z.array(z.string()).optional(),
905
+ "x-order": propertyOrderSchema,
906
+ ...valueKeywordsSchema.shape,
907
+ ...objectRulesSchema.shape,
908
+ }),
909
+ );
910
+
911
+ const schemaPropertySchema: z.ZodType<SchemaProperty, SchemaProperty> = z.lazy(() =>
912
+ z.strictObject({
913
+ type: z.union([schemaTypeSchema, z.tuple([schemaTypeSchema, z.literal("null")])]),
914
+ description: z.string().trim().max(FIELD_LIMITS.descriptionLength).optional(),
915
+ enum: enumSchema,
916
+ properties: z.record(propertyNameSchema, schemaPropertySchema).optional(),
917
+ required: z.array(z.string()).optional(),
918
+ "x-order": propertyOrderSchema,
919
+ items: itemsSchema.optional(),
920
+ "x-sensitive": z.boolean().optional(),
921
+ "x-source": fieldSourceSchema.optional(),
922
+ "x-target": fieldTargetSchema.optional(),
923
+ ...valueKeywordsSchema.shape,
924
+ ...objectRulesSchema.shape,
925
+ }),
926
+ );
720
927
 
721
928
  /** A node's own type, without the `"null"` beside it. */
722
929
  function typeOf(node: { type: SchemaProperty["type"] }) {
@@ -727,29 +934,45 @@ function typeOf(node: { type: SchemaProperty["type"] }) {
727
934
  type RowIssue = { at: Array<string | number>; message: string };
728
935
 
729
936
  /** A node holding properties: a property, an array's items, or an input or output schema. */
730
- type ObjectNode = {
731
- type: SchemaProperty["type"];
732
- properties?: Record<string, SchemaProperty>;
733
- required?: string[];
734
- "x-order"?: string[];
735
- };
937
+ type ObjectNode = ObjectRules &
938
+ ValueKeywords & {
939
+ type: SchemaProperty["type"];
940
+ properties?: Record<string, SchemaProperty>;
941
+ required?: string[];
942
+ "x-order"?: string[];
943
+ };
736
944
 
737
- /** What a node's own keywords break: `properties`, `required` or `x-order` off an object, or the last two naming no property. */
945
+ /**
946
+ * What a node's own keywords break: `properties`, `required`, `x-order` or
947
+ * a rule off an object, the next two or a rule naming no property, and a
948
+ * value keyword off the types it bounds.
949
+ */
738
950
  function nodeIssues(node: ObjectNode) {
739
951
  const issues = new Array<RowIssue>();
740
- const object = typeOf(node) === "object";
741
- for (const keyword of ["properties", "required", "x-order"] as const) {
952
+ const type = typeOf(node);
953
+ const object = type === "object";
954
+ for (const keyword of ["properties", "required", "x-order", ...OBJECT_RULE_KEYWORDS] as const) {
742
955
  if (!object && node[keyword] !== undefined) {
743
956
  issues.push({ at: [keyword], message: "only an object has it" });
744
957
  }
745
958
  }
959
+ const named = (name: string) => Object.hasOwn(node.properties ?? {}, name);
746
960
  for (const keyword of ["required", "x-order"] as const) {
747
961
  node[keyword]?.forEach((name, index) => {
748
- if (!Object.hasOwn(node.properties ?? {}, name)) {
749
- issues.push({ at: [keyword, index], message: "names no property" });
750
- }
962
+ if (!named(name)) issues.push({ at: [keyword, index], message: "names no property" });
751
963
  });
752
964
  }
965
+ for (const keyword of OBJECT_RULE_KEYWORDS) {
966
+ if (object && !ruleNames({ [keyword]: node[keyword] }).every(named)) {
967
+ issues.push({ at: [keyword], message: "names no property" });
968
+ }
969
+ }
970
+ for (const [keyword, types] of Object.entries(VALUE_KEYWORD_TYPES)) {
971
+ if (node[keyword as keyof ValueKeywords] === undefined) continue;
972
+ if (!types.some((bounded) => bounded === type)) {
973
+ issues.push({ at: [keyword], message: `only a ${types.join(" or ")} has it` });
974
+ }
975
+ }
753
976
  return issues;
754
977
  }
755
978
 
@@ -758,7 +981,7 @@ function nodeIssues(node: ObjectNode) {
758
981
  * them, beside `null` exactly when the type allows null, as the console
759
982
  * stores a nullable one.
760
983
  */
761
- function enumIssueOf(property: SchemaProperty, type: string | undefined) {
984
+ function enumIssueOf(property: Pick<SchemaProperty, "enum" | "type">, type: string | undefined) {
762
985
  if (property.enum === undefined) return undefined;
763
986
  if (type !== "string") return "an enum lists a string's values";
764
987
  const values = property.enum.filter((value) => value !== null);
@@ -853,7 +1076,10 @@ function heldByRows(
853
1076
  const at = [...path, "properties", name];
854
1077
  report(at, propertyIssues(property, bindings));
855
1078
  heldByRows(property, context, at, undefined);
856
- if (property.items) heldByRows(property.items, context, [...at, "items"], undefined);
1079
+ if (!property.items) continue;
1080
+ const enumIssue = enumIssueOf(property.items, typeOf(property.items));
1081
+ if (enumIssue) report([...at, "items"], [{ at: ["enum"], message: enumIssue }]);
1082
+ heldByRows(property.items, context, [...at, "items"], undefined);
857
1083
  }
858
1084
  }
859
1085
 
@@ -861,7 +1087,8 @@ function heldByRows(
861
1087
  * An object's JSON Schema, the shape of a tool's or trigger's input and
862
1088
  * output, and of what the console's field rows write. `bindings`, given for
863
1089
  * a tool's input, are the extensions its top-level fields may carry:
864
- * `x-source` on a scalar, `x-target` on any field.
1090
+ * `x-source` on a scalar, `x-target` on any field. Only a tool's input holds
1091
+ * rules at its root, as the console keeps them beside its rows.
865
1092
  */
866
1093
  export function objectJsonSchemaOf(bindings?: ReadonlyArray<Binding>) {
867
1094
  return z
@@ -870,8 +1097,20 @@ export function objectJsonSchemaOf(bindings?: ReadonlyArray<Binding>) {
870
1097
  properties: z.record(propertyNameSchema, schemaPropertySchema).optional(),
871
1098
  required: z.array(z.string()).optional(),
872
1099
  "x-order": propertyOrderSchema,
1100
+ ...objectRulesSchema.shape,
873
1101
  })
874
- .superRefine((schema, context) => heldByRows(schema, context, [], bindings));
1102
+ .superRefine((schema, context) => {
1103
+ heldByRows(schema, context, [], bindings);
1104
+ if (bindings !== undefined) return;
1105
+ for (const keyword of OBJECT_RULE_KEYWORDS) {
1106
+ if (schema[keyword] === undefined) continue;
1107
+ context.addIssue({
1108
+ code: "custom",
1109
+ path: [keyword],
1110
+ message: "only a tool's input has rules at its root",
1111
+ });
1112
+ }
1113
+ });
875
1114
  }
876
1115
  const objectSchema = objectJsonSchemaOf();
877
1116
 
@@ -1070,63 +1309,134 @@ const manifestSkillSchema = z.strictObject({
1070
1309
  export type ManifestSkill = z.input<typeof manifestSkillSchema>;
1071
1310
 
1072
1311
  /**
1073
- * Which thread a firing lands in. `single`: one thread for the trigger.
1074
- * `per-firing`: a fresh thread every time. `ephemeral`: a fresh thread that
1312
+ * Which thread a fire lands in. `single`: one thread for the trigger.
1313
+ * `per-input`: a fresh thread every time. `ephemeral`: a fresh thread that
1075
1314
  * is deleted once the run completes, for one-shot calls: the answer reaches
1076
- * the caller as the firing's output or through the live stream, and nothing
1315
+ * the caller as the run's output or through the live stream, and nothing
1077
1316
  * else outlives the run; a failed run keeps its thread for inspection. `keyed`:
1078
1317
  * one thread per distinct value at a JSON pointer into the payload (e.g.
1079
- * `/pull_request/number`).
1318
+ * `/pull_request/number`). `thread`: the existing thread of the agent's whose
1319
+ * id is at the pointer, a user's among them, such as one a tool passed its
1320
+ * backend. Its run acts as the thread's user without their token, which
1321
+ * only a server that allows delegation hears; the trigger's credential is
1322
+ * never presented. Its runs offer no tool that needs approval: nobody
1323
+ * decides the trigger's calls in a user's chat. With `wake: false` no run
1324
+ * starts: the fire leaves its instructions in the thread as a note, its
1325
+ * payload references filled, in its turn behind any run there.
1080
1326
  */
1081
1327
  export const triggerThreadPolicySchema = z.discriminatedUnion("policy", [
1082
1328
  z.object({ policy: z.literal("single") }),
1083
- z.object({ policy: z.literal("per-firing") }),
1329
+ z.object({ policy: z.literal("per-input") }),
1084
1330
  z.object({ policy: z.literal("ephemeral") }),
1085
1331
  z.object({ policy: z.literal("keyed"), key: jsonPointerSchema }),
1332
+ z.object({
1333
+ policy: z.literal("thread"),
1334
+ key: jsonPointerSchema,
1335
+ wake: z.boolean(),
1336
+ }),
1086
1337
  ]);
1087
1338
  export type TriggerThreadPolicy = z.infer<typeof triggerThreadPolicySchema>;
1088
1339
 
1340
+ /** Whether a policy reads its thread from the payload, which a fire with none cannot do. */
1341
+ export function threadFromPayload(policy: TriggerThreadPolicy) {
1342
+ return policy.policy === "keyed" || policy.policy === "thread";
1343
+ }
1344
+
1345
+ /**
1346
+ * What a trigger with a schedule may not have, by the part it sits in: it
1347
+ * fires with no payload and no caller, so it takes no input fields, no fire
1348
+ * credential and no thread read from a payload. Each caller words the
1349
+ * issues itself. None for one without.
1350
+ */
1351
+ export function scheduledTriggerIssues(trigger: {
1352
+ scheduled: boolean;
1353
+ inputFields: number;
1354
+ fireCredential: boolean;
1355
+ threadPolicy: TriggerThreadPolicy;
1356
+ }) {
1357
+ if (!trigger.scheduled) return [];
1358
+ return [
1359
+ trigger.inputFields > 0 && ("input" as const),
1360
+ trigger.fireCredential && ("fireCredential" as const),
1361
+ threadFromPayload(trigger.threadPolicy) && ("threadPolicy" as const),
1362
+ ].filter((issue) => issue !== false);
1363
+ }
1364
+
1089
1365
  /** A trigger's description and instructions in characters. The console caps its editor at the same. */
1090
1366
  export const TRIGGER_LIMITS = { descriptionLength: 500, instructionsLength: 20_000 } as const;
1091
1367
 
1092
- /** Fired with its secret, which the apply that creates it answers once and no manifest holds. */
1093
- const manifestTriggerSchema = z.strictObject({
1094
- /** `message` is the built-in trigger every agent has. */
1095
- name: nameSchema.refine((name) => name !== MESSAGE_TRIGGER_NAME, "the built-in trigger's name"),
1096
- displayName: displayNameSchema.default({}),
1097
- description: z.string().trim().min(1).max(TRIGGER_LIMITS.descriptionLength),
1098
- inputSchema: objectSchema.default(EMPTY_OBJECT),
1099
- /** The shape the run answers in; absent, the run answers in free text. */
1100
- outputSchema: objectSchema
1101
- .refine(
1102
- (schema) => Object.keys(schema.properties ?? {}).length > 0,
1103
- "declares a property; leave it out for a free-text answer",
1104
- )
1105
- .optional(),
1106
- /** Rich text: it may mention tools, skills and datasets, and read `{{variables}}` and `{{/payload/pointer}}`s. */
1107
- instructions: z.string().trim().min(1).max(TRIGGER_LIMITS.instructionsLength),
1108
- threadPolicy: triggerThreadPolicySchema.default({ policy: "single" }),
1109
- /** Firings a day per agent; absent, no limit. */
1110
- budgetPerDay: z.number().int().positive().optional(),
1111
- /** Consecutive failed or rejected firings before the trigger disables itself; it stays off until an operator enables it. */
1112
- disableAfterFailures: setting(SETTING_BOUNDS.trigger.disableAfterFailures),
1113
- /** The credential the run acts with, presented wherever the end user's token would go. */
1114
- credential: nameSchema.optional(),
1115
- /** A static credential whose value callers fire with, in place of a minted secret. */
1116
- fireCredential: nameSchema.optional(),
1117
- /** The model its runs talk to, over each agent's. */
1118
- model: modelSchema.optional(),
1119
- audience: audienceSchema,
1120
- overridable: overridable([
1121
- "displayName",
1122
- "description",
1123
- "instructions",
1124
- "threadPolicy",
1125
- "budgetPerDay",
1126
- "disableAfterFailures",
1127
- "model",
1128
- ]),
1129
- });
1368
+ /**
1369
+ * Fired with its secret, which the apply that creates it answers once and no
1370
+ * manifest holds; or, with a `schedule`, by Cortex itself on its cron, with
1371
+ * no payload and no caller, so it takes no input fields, no fire credential
1372
+ * and no thread read from a payload. Whether it has a schedule is fixed once it
1373
+ * exists.
1374
+ */
1375
+ const manifestTriggerSchema = z
1376
+ .strictObject({
1377
+ name: nameSchema,
1378
+ displayName: displayNameSchema.default({}),
1379
+ description: z.string().trim().min(1).max(TRIGGER_LIMITS.descriptionLength),
1380
+ inputSchema: objectSchema.default(EMPTY_OBJECT),
1381
+ /** The shape the run answers in; absent, the run answers in free text. */
1382
+ outputSchema: objectSchema
1383
+ .refine(
1384
+ (schema) => Object.keys(schema.properties ?? {}).length > 0,
1385
+ "declares a property; leave it out for a free-text answer",
1386
+ )
1387
+ .optional(),
1388
+ /** Rich text: it may mention tools, skills and datasets, and read `{{variables}}` and `{{/payload/pointer}}`s. */
1389
+ instructions: z.string().trim().min(1).max(TRIGGER_LIMITS.instructionsLength),
1390
+ threadPolicy: triggerThreadPolicySchema.default({ policy: "single" }),
1391
+ /** A fire that finds its thread busy: queued as the next turn (the default), steering a running turn of this trigger's, or refused. */
1392
+ whileBusy: z.enum(WHILE_BUSY).default("queue"),
1393
+ /** Fires a day per agent; absent, no limit. */
1394
+ budgetPerDay: z.number().int().positive().optional(),
1395
+ /** Consecutive failed runs or rejected fires before the trigger disables itself; it stays off until an operator enables it. */
1396
+ disableAfterFailures: setting(SETTING_BOUNDS.trigger.disableAfterFailures),
1397
+ /** The credential the run acts with, presented wherever the end user's token would go. */
1398
+ credential: nameSchema.optional(),
1399
+ /** A static credential whose value callers fire with, in place of a minted secret. */
1400
+ fireCredential: nameSchema.optional(),
1401
+ /** The model its runs talk to, over each agent's. */
1402
+ model: modelSchema.optional(),
1403
+ /** When Cortex fires it itself; absent, it fires when a caller calls it. */
1404
+ schedule: triggerScheduleSchema.optional(),
1405
+ audience: audienceSchema,
1406
+ overridable: overridable([
1407
+ "displayName",
1408
+ "description",
1409
+ "instructions",
1410
+ "threadPolicy",
1411
+ "whileBusy",
1412
+ "budgetPerDay",
1413
+ "disableAfterFailures",
1414
+ "model",
1415
+ "schedule",
1416
+ ]),
1417
+ })
1418
+ .superRefine((trigger, context) => {
1419
+ const issues = {
1420
+ input: [
1421
+ "inputSchema",
1422
+ "a scheduled trigger fires with no payload, so it takes no input fields",
1423
+ ],
1424
+ fireCredential: ["fireCredential", "a scheduled trigger has no caller to fire it"],
1425
+ threadPolicy: [
1426
+ "threadPolicy",
1427
+ "a scheduled trigger has no payload to read its thread from",
1428
+ ],
1429
+ } as const;
1430
+ for (const issue of scheduledTriggerIssues({
1431
+ scheduled: trigger.schedule !== undefined,
1432
+ inputFields: Object.keys(trigger.inputSchema.properties ?? {}).length,
1433
+ fireCredential: trigger.fireCredential !== undefined,
1434
+ threadPolicy: trigger.threadPolicy,
1435
+ })) {
1436
+ const [path, message] = issues[issue];
1437
+ context.addIssue({ code: "custom", path: [path], message });
1438
+ }
1439
+ });
1130
1440
  export type ManifestTrigger = z.input<typeof manifestTriggerSchema>;
1131
1441
 
1132
1442
  /** The datasets `pinned` lists that `expose` does not: a host pins only what it exposes. */
@@ -1502,6 +1812,10 @@ export const agentSettingsSchema = z.strictObject({
1502
1812
  toolRegistryEnabled: z.boolean().default(false),
1503
1813
  /** Lets `searchRegistries` search the services the agent sees. */
1504
1814
  serviceRegistryEnabled: z.boolean().default(false),
1815
+ /** Lets the agent hand a task to a copy of itself (`delegateTask`), in its turn or in the background. */
1816
+ subagentsEnabled: z.boolean().default(false),
1817
+ /** Lets the agent schedule a check-back on a user's thread (`scheduleWake`): it comes back later on its own. */
1818
+ checkBacksEnabled: z.boolean().default(false),
1505
1819
  attachmentsEnabled: z.boolean().default(true),
1506
1820
  /** Hosts the built-in `fetchImage` tool may pull images from; none, no such tool. */
1507
1821
  fetchImageHostnames: z
@@ -1512,7 +1826,9 @@ export const agentSettingsSchema = z.strictObject({
1512
1826
  /** The fenced media forms the agent may draw, which its prompt then describes. */
1513
1827
  media: z.array(z.enum(MEDIA_FORMS)).default([...DEFAULT_MEDIA_FORMS]),
1514
1828
  /** Off, end users cannot message the agent: only its triggers reach it. */
1515
- messageTrigger: z.boolean().default(true),
1829
+ messages: z.boolean().default(true),
1830
+ /** A message sent while the agent works: refused, queued, or steering the running turn. */
1831
+ whileBusy: z.enum(WHILE_BUSY).default("refuse"),
1516
1832
  defaultLocale: localeSchema.default(BASE_LOCALE),
1517
1833
  context: z
1518
1834
  .strictObject({