formgen

package
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 9, 2026 License: AGPL-3.0 Imports: 14 Imported by: 0

Documentation

Overview

Package formgen generates a form from a description and from the process it belongs to (ADR-0260).

It is design-time authoring and nothing else. There is no instance, no token, no job and no event: an author describes the form they want — in prose, or by naming the process it starts — a model writes a form-js schema, and that schema arrives in the editor as an unsaved proposal the author reads before saving. Nothing here writes to a store. ADR-0032 said this about diagrams and it is the same sentence about forms: what a model produces is a draft that passes the same gate a hand-written one does.

**It asks the Worker an operator already configured.** An agent Worker (ADR-0255) is a Console record holding an endpoint, a wire format, a model name and a vault reference to an API key. Generation reaches exactly that record, through exactly the adapters the runtime agent uses (connector/agent) — so there is no second place to configure a model, no second credential to rotate, and no key in the browser. Which model answers a generation is the same question, with the same answer, as which model answers an ai task.

**This area owns no state**, which is why it holds no run loop where every other service under ADR-0147 holds one. It stores nothing and reads nothing directly: its three collaborators are the server's, and each observes the single writer itself (I3). What this service does hold is the one thing that must never go near that loop — an outbound call to a model endpoint, seconds to minutes long, on the goroutine of the request that asked for it.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func SchemaFrom

func SchemaFrom(answer, formID string, maxAnswer int64) (map[string]any, error)

SchemaFrom turns a model's answer into a form-js schema the editor can open, under the identity formID names. The returned map is plain JSON values, ready to marshal into a response; every error it returns is written to be shown to the author as it stands.

Types

type Capability

type Capability struct {
	Available bool     `json:"available"`
	Workers   []Worker `json:"workers"`
}

Capability is what the editor asks before it offers the affordance at all. A button that produces "no AI Worker is configured" is a button that should not have been there.

type Element

type Element struct {
	// ID is the BPMN element id — what a form binding, a token overlay and an
	// incident all name it by, so it is what the author sees elsewhere too.
	ID string
	// Kind is the BPMN local name (userTask, startEvent, sequenceFlow …). It is the
	// vocabulary, unmapped: a model that knows BPMN reads it directly.
	Kind string
	// Name and Documentation are the modeller's own.
	Name          string
	Documentation string
	// Condition is a sequence flow's `<conditionExpression>`. It rides here because a
	// gateway's outgoing conditions are where a process says what it decides on, and
	// therefore which of a form's answers actually matter.
	Condition string
	// FormID is the form the step already binds (zeebe:formDefinition). Empty for a
	// step that binds none — and non-empty is worth saying out loud, because it means
	// a generation is a replacement rather than a first draft.
	FormID string
}

Element is one step as the outline carries it: what it is, what it is called, and what the modeller wrote about it.

type Process

type Process struct {
	ID            string
	Name          string
	Documentation string
	Elements      []Element
	// Variables are the names the model already uses for its data, in document order
	// and deduplicated: input/output mapping targets, data objects, and result
	// variables. They matter because a form whose keys match them needs no mapping
	// afterwards, while one that invents `vacationDays` for a process that says
	// `urlaubstage` has made work for somebody.
	Variables []string
	// contains filtered or unexported fields
}

Process is a BPMN model's own account of itself, as much of it as a form generator has any use for. The zero value is "nothing could be read", which every caller treats as "the author's prose is the whole brief" rather than as an error.

func ReadProcess

func ReadProcess(src []byte) Process

ReadProcess walks BPMN XML for what a form generator can use. It never fails: a truncated or malformed document yields whatever was readable before the break, because the alternative — refusing to generate a form for a draft that does not parse — would withhold the feature from the author who most needs it.

func (Process) Describe

func (p Process) Describe(elementID string) string

Describe renders the outline as the paragraphs that go into a prompt. elementID names the step the form is for and may be empty, which is the start-form case: the form starts the process, so the process as a whole is what it is for.

It returns the empty string when there is nothing to say, so a caller can append it unconditionally and a model never reads a heading over an empty section.

func (Process) Element

func (p Process) Element(id string) (Element, bool)

Element finds a step by its BPMN id.

type Request

type Request struct {
	// Description is the author's own brief, in their own language.
	Description string `json:"description"`
	// Worker names the agent Worker to ask. Empty picks the only one in reach, and is
	// refused when there are several — choosing a model for somebody is choosing what
	// their form costs.
	Worker string `json:"worker,omitempty"`
	// Model overrides the language model that Worker is configured for, exactly as a
	// task may (ADR-0256): one Worker, one credential, a cheap model for a short form
	// and a strong one for a hard one.
	Model string `json:"model,omitempty"`
	// ProcessID names the process the form belongs to. Its draft is read if there is
	// one, otherwise its deployed version.
	ProcessID string `json:"processId,omitempty"`
	// ElementID names the step the form is for. Empty is the start-form case: the form
	// starts the process rather than completing a step in it.
	ElementID string `json:"elementId,omitempty"`
	// FormID is the id the editor is holding this form under, stamped into the result
	// so a generation cannot rename a form a user task binds (ADR-0222).
	FormID string `json:"formId,omitempty"`
	// Schema is the form as it stands, present when this is a refinement. The model is
	// shown it and asked to return the whole document.
	Schema json.RawMessage `json:"schema,omitempty"`
}

Request is what an author asks for. Everything but the description is optional, and a request with only a process id is a complete one: "the form that starts this" says enough.

type Response

type Response struct {
	Schema map[string]any `json:"schema"`
	// Worker and Model say who answered. An author comparing two attempts needs to
	// know which model wrote which, and it is the first thing to check when a
	// generation comes back poor.
	Worker string `json:"worker"`
	Model  string `json:"model,omitempty"`
	// ProcessID, ElementID and ProcessSource echo the context that was actually read,
	// so a result that ignored the process the author thought they had named says so.
	ProcessID     string `json:"processId,omitempty"`
	ElementID     string `json:"elementId,omitempty"`
	ProcessSource string `json:"processSource,omitempty"`
}

Response is the proposal. It is not saved anywhere: the author reads it in the editor and saves it themselves, or does not.

type Service

type Service struct {

	// Limits are the installation's resource budgets. New sets them to
	// [limits.Default]; the server overwrites them with its own once it has read the
	// environment, so every ceiling in this service is the one operators configured
	// (ADR-0291).
	Limits limits.Limits
	// contains filtered or unexported fields
}

Service generates forms. Build it with New.

func New

func New(workers func(*http.Request) ([]Worker, error),
	dial func(*http.Request, string) (agent.Model, error),
	source func(*http.Request, string) (Source, bool, error)) *Service

New builds the generation service. All three collaborators are the server's, and each reaches shared state through the run loop itself — this service holds none of it (see the package comment).

func (*Service) Generate

func (s *Service) Generate(r *http.Request, req Request) (Response, int, error)

Generate runs one generation and returns the proposal. status is the HTTP status the error should reach the author as; it is 0 when there is no error.

The model call happens here, on the caller's goroutine, under the request's own context — so an author who navigates away takes their generation with them, and a model endpoint that hangs costs one request rather than the engine.

func (*Service) HandleCapability

func (s *Service) HandleCapability(w http.ResponseWriter, r *http.Request)

HandleCapability answers what the editor needs before it offers to generate anything: whether there is an AI Worker to ask, and which ones this principal may name.

It exists so the affordance can be absent rather than broken. A button that only ever produces "no AI Worker is configured" teaches an author that the feature does not work, which is a worse outcome than not having seen it.

func (*Service) HandleGenerate

func (s *Service) HandleGenerate(w http.ResponseWriter, r *http.Request)

HandleGenerate writes one form and returns it unsaved.

Nothing is stored: the response is a proposal the author opens in the editor, reads, and saves themselves through the ordinary save path — with the ordinary id check, the ordinary scope check, and their own name on it. That is ADR-0032's stance about generated diagrams, applied to forms for the same reason: the compiler gate and the author's own eye are what make a generated artifact safe to deploy, and skipping either to save a click would be trading the whole argument for the click.

type Source

type Source struct {
	ProcessID string
	Name      string
	XML       string
	// Origin is "draft" or "deployment".
	Origin string
}

Source is a process's BPMN as design-time state holds it: a draft under the author's hands, or the version currently deployed. Which of the two it came from is carried because it is worth saying — a form generated against a draft was generated against a process that is not running yet.

type Worker

type Worker struct {
	Name string `json:"name"`
	// Model is the language model the Worker is configured for, empty when it is
	// whatever its protocol defaults to. It is shown to the author because it is the
	// one thing about an AI Worker that changes what comes back.
	Model string `json:"model,omitempty"`
	// Provider is the wire format ("messages", "chat-completions"), carried so the
	// picker can tell two Workers apart when their names do not.
	Provider string `json:"provider,omitempty"`
}

Worker is one agent Worker as this area needs to speak about it: the name a request names it by, and what it is configured to ask. No endpoint and nowhere to put a credential — the same property connector/agent's Task has, and for the same reason (ADR-0041/0069).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL