Kanzo UI
AI

Tasks

A small model fills a small artefact under a JSON Schema, oracles check it, and a bounded loop repairs it or asks the person — defineTask, runTask, propose, verify from @kanzo-tech/ai/task, and useTask for a screen's list of proposals.

A Task is what the platform asks a model for: an artefact whose shape is a schema, checked by oracles that are facts of the data or of the program, never the model's opinion. Tasks in the design is why; Ask is the first one built on it.

import { defineTask, propose, runTask, verify } from "@kanzo-tech/ai/task";

const questions = defineTask<string, { text: string }>({
  name: "questions",
  instructions: "Offer competency questions about the domain.",
  prompt: (domain) => domain,
  output: () => jsonSchema({ type: "object", properties: { text: { type: "string" } }, required: ["text"] }),
  verify: [(value) => (value.text.endsWith("?") ? undefined : { code: "not-a-question", message: "a question ends in ?" })],
});
defineTask(definition)The Task: its context (computed, never asked), instructions, prompt, output schema, verify oracles, the decisions it settles with decide, and how many attempts
runTask(task, input, { model, signal, onState })One artefact: asked, checked, the diagnostics sent back while attempts are left; a Proposal whose verdict is accepted, the diagnostics, or a Decision for the person
propose(task, input, { model, count })Several at once, in one call, keeping only those every oracle accepts — what a list offers before anything is asked
verify(task, value, input)The oracles' verdict on a value no model wrote: a person's edit

An artefact can be text in a language instead of JSON. A Task declares grammar(input, context), an EBNF, in place of output: the model is asked for text, the grammar travels as providerOptions.kanzo.grammar, and localModel holds every token to it, so the model cannot write a name the grammar does not list. The oracles check the text as they check any artefact — for a Fossil mapping, the checker. propose takes a schema only.

Held by packages/ai/src/task/task.test.ts, "asks for text with the grammar beside the prompt, and checks it as any artefact".

An oracle answers nothing, a Diagnostic ({ code, message, at? }, at a JSON Pointer into the artefact), or a { decision } — a question in the shape of MCP's elicitation, with options and never free text.

Held by packages/ai/src/task/task.test.ts, "sends what the oracles found back, with where, and takes the repaired answer"; "stops at a decision it has no safe default for, and asks with options"; "offers only what every oracle accepts, with no decision left open, at most as many as asked".

A list on a screen

useTask(task, input, { model, count }) is a screen's list of what a Task offers. It asks when the screen says — propose() — never on mount, because the input is usually still being filled and the model call is the expensive part. edit(id, value) checks the person's change with verify, with no model, and keeps it with its verdict: one the oracles refuse stays on the list, saying why. dismiss(id) takes one off. Taking one is the screen's: the proposal's target says where it lands, and the screen owns that document.

const list = useTask(questions, domain, { model });
// list.state: "idle" | "proposing" | "ready" | "failed"; list.error when it failed
<Button onClick={() => list.propose()}>Suggest questions</Button>

Held by packages/ai/src/use-task.test.tsx, "asks only when told, and keeps what the oracles accept"; "checks an edit with no model, and keeps it with its verdict; a dismissal takes it off".

The types are exported as TaskDefinition, Proposal, Verdict, Diagnostic, Decision, Finding, Oracle, Target, RunOptions, TaskList, TaskListState and UseTaskOptions.

On this page