Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ builder-reliability findings, or design reasoning).

Field types are `string`, `number`, `integer`, `boolean`, and the multi-pick array
`{type: "array", items: {type: "string", enum: [...]}}`. No nested objects. Formats: `date`,
`date-time`, `email`, `uri`, `multiline` (date fields ignore defaults).
`date-time`, `email`, `uri`, `multiline`, `cron` (a recurring schedule; renders a
friendly builder, the value is a 5-field cron expression; date fields ignore defaults).

## Verification (against our runtime)

Expand Down
8 changes: 5 additions & 3 deletions api/oss/src/core/workflows/static_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,9 +192,11 @@ def _request_input_revision() -> WorkflowRevision:
"in one click. Enum options are SUGGESTIONS, not a hard constraint — "
"the form lets the user type their own value, so keep enums short and "
"likely rather than exhaustive. Supported `format` values: "
"'date', 'date-time', 'email', 'uri', and 'multiline' — use 'multiline' "
"for any long or free-form text field (notes, a description, a message "
'body). For a form with SEVERAL questions, set "x-ag-stepper": true on '
"'date', 'date-time', 'email', 'uri', 'multiline', and 'cron' — use "
"'multiline' for any long or free-form text field (notes, a "
"description, a message body) and 'cron' for a recurring schedule "
"(renders a friendly schedule builder; the value is a 5-field cron "
'expression). For a form with SEVERAL questions, set "x-ag-stepper": true on '
"requestedSchema to present one question at a time with a final "
"review step. NEVER request secrets "
"(passwords, API keys, tokens); use request_connection for credentials. "
Expand Down
5 changes: 4 additions & 1 deletion api/oss/tests/pytest/unit/workflows/test_static_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -981,10 +981,13 @@ def _assert_one_of(one_of, name):
assert isinstance(prop["default"], (str, int, float, bool)), name
assert set(requested.get("required", [])) <= set(requested["properties"])
# The golden must exercise the dialect's optional shapes: a prefilled field (#5190),
# a multi-select array, and context-ful oneOf options (choice cards).
# a multi-select array, context-ful oneOf options (choice cards), and a cron field.
assert any("default" in prop for prop in requested["properties"].values())
assert any(prop["type"] == "array" for prop in requested["properties"].values())
assert any("oneOf" in prop for prop in requested["properties"].values())
assert any(
prop.get("format") == "cron" for prop in requested["properties"].values()
)


def test_request_input_matches_golden_response_fixture():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ elicitation form supports real defaults and two richer field shapes
- **Context-ful options** use `oneOf: [{const, title, description}]` (single fields and array
items); options with descriptions render as selectable choice cards.
- **Field leaves are string, number, integer, boolean** (plus the string-array multi-select).
No nested objects. Formats: date, date-time, email, uri, multiline.
No nested objects. Formats: date, date-time, email, uri, multiline, cron.
- **Secrets are refused.** Credentials go through `request_connection`, never `request_input`.

## The index.md match table
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
useState,
} from "react"

import {cronToBuilder, describeBuilder} from "@agenta/entities/gatewayTrigger"
import {buildFormFieldsFromSchema, type FormFieldDescriptor} from "@agenta/shared/utils"
import {Editor} from "@agenta/ui/editor"
import {CaretLeft, CaretRight, Check, MinusCircle, Plus} from "@phosphor-icons/react"
Expand All @@ -25,9 +26,13 @@ import {
} from "antd"
import type {FormInstance, InputRef} from "antd"

import {ScheduleBuilderField} from "../../gatewayTrigger/drawers/ScheduleBuilderField"

import {
DEFAULT_CRON,
OTHER_ENUM_OPTION,
commitCustomValue,
cronInitialValue,
enumOptionsOf,
isOffOptionsValue,
partitionCustomValues,
Expand Down Expand Up @@ -869,9 +874,22 @@ function ChoiceCards({
)
}

/** ScheduleBuilderField is controlled on a cron STRING and cannot take undefined. The form value
* is seeded via cronInitialValue, so the fallback only covers a post-mount clear (stepper Skip). */
function CronField({value, onChange}: {value?: string; onChange?: (cron: string) => void}) {
return <ScheduleBuilderField value={value || DEFAULT_CRON} onChange={(c) => onChange?.(c)} />
}

/** Compact review-row value: option labels, joined chips, Yes/No, formatted dates. */
function formatReviewValue(field: FormFieldDescriptor, value: unknown): string {
if (value === undefined || value === null || value === "") return "\u2014"
if (field.format === "cron" && typeof value === "string") {
try {
return describeBuilder(cronToBuilder(value).state)
} catch {
return value
}
}
if (Array.isArray(value)) return value.map(String).join(", ") || "\u2014"
if (typeof value === "object" && typeof (value as {format?: unknown}).format === "function")
return (value as {format: (f: string) => string}).format("YYYY-MM-DD HH:mm")
Expand Down Expand Up @@ -1037,6 +1055,20 @@ function SchemaFormField({

default:
// Format-aware controls appear only when the host opted in via `formats`.
if (field.format === "cron") {
// Seed the displayed schedule as the value — the builder has no empty state,
// so an unseeded required field would look answered while Accept stays disabled.
return (
<Form.Item
name={field.name.split(".")}
label={label}
rules={rules}
initialValue={cronInitialValue(field.default)}
>
<CronField />
</Form.Item>
)
}
if (field.format === "date" || field.format === "date-time") {
// No initialValue: a wire default is an ISO STRING and DatePicker requires dayjs —
// a string value crashes it. Date fields render empty; other types prefill.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ import type {FormFieldDescriptor} from "@agenta/shared/utils"

export const OTHER_ENUM_OPTION = "__ag_enum_other__"

/** ScheduleBuilderField has no empty state, so a cron field is born answered: the schedule the
* builder displays must also be the seeded form value (WYSIWYG), never just a visual fallback. */
export const DEFAULT_CRON = "0 9 * * *"

/** Form seed for a cron field: the wire default when present, else the builder's display default. */
export const cronInitialValue = (fieldDefault: unknown): string =>
typeof fieldDefault === "string" && fieldDefault ? fieldDefault : DEFAULT_CRON

/** A renderable option: bare enum values get {value}, oneOf options add label/description. */
export interface EnumOption {
value: string
Expand Down
17 changes: 17 additions & 0 deletions web/packages/agenta-entity-ui/tests/unit/schemaFormOptions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import {describe, expect, it} from "vitest"
import type {FormFieldDescriptor} from "@agenta/shared/utils"

import {
DEFAULT_CRON,
OTHER_ENUM_OPTION,
commitCustomValue,
cronInitialValue,
digitKeyIndex,
enumOptionsOf,
isOffOptionsValue,
Expand Down Expand Up @@ -223,3 +225,18 @@ describe("digitKeyIndex / resolveDigitSelection (choice-card hotkeys)", () => {
expect(resolveDigitSelection("x", options)).toBeNull()
})
})

describe("cronInitialValue", () => {
it("a cron field without a wire default is born answered with the builder's display default", () => {
expect(cronInitialValue(undefined)).toBe(DEFAULT_CRON)
})

it("a wire default wins over the display default", () => {
expect(cronInitialValue("30 8 * * 1")).toBe("30 8 * * 1")
})

it("empty or non-string defaults fall back to the display default", () => {
expect(cronInitialValue("")).toBe(DEFAULT_CRON)
expect(cronInitialValue(5)).toBe(DEFAULT_CRON)
})
})
10 changes: 9 additions & 1 deletion web/packages/agenta-shared/src/utils/elicitation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,14 @@ export const SECRET_FIELD_PATTERN =
const FIELD_TYPES = new Set(["string", "number", "integer", "boolean"])

/** `format` values the renderer maps to dedicated controls; unknown formats fall back to text. */
export const KNOWN_STRING_FORMATS = new Set(["date", "date-time", "email", "uri", "multiline"])
export const KNOWN_STRING_FORMATS = new Set([
"date",
"date-time",
"email",
"uri",
"multiline",
"cron",
])

/** Natural aliases an author (often an LLM) emits for a known format. */
const STRING_FORMAT_ALIASES: Record<string, string> = {
Expand All @@ -38,6 +45,7 @@ const STRING_FORMAT_ALIASES: Record<string, string> = {
long_text: "multiline",
datetime: "date-time",
url: "uri",
crontab: "cron",
}

/** Resolve a schema `format` to a renderer-known format (aliases → canonical), or undefined. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@
}
]
},
"run_schedule": {
"type": "string",
"title": "Run schedule",
"format": "cron",
"default": "0 9 * * *"
},
"time_of_day": {
"type": "string",
"title": "At what time",
Expand Down
3 changes: 2 additions & 1 deletion web/packages/agenta-shared/tests/unit/elicitation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ describe("SECRET_FIELD_PATTERN", () => {
})

describe("normalizeStringFormat", () => {
it.each(["date", "date-time", "email", "uri", "multiline"])(
it.each(["date", "date-time", "email", "uri", "multiline", "cron"])(
"passes through canonical format %s",
(f) => expect(normalizeStringFormat(f)).toBe(f),
)
Expand All @@ -428,6 +428,7 @@ describe("normalizeStringFormat", () => {
["longtext", "multiline"],
["datetime", "date-time"],
["url", "uri"],
["crontab", "cron"],
])("maps alias %s -> %s", (alias, canonical) =>
expect(normalizeStringFormat(alias)).toBe(canonical),
)
Expand Down
Loading