diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a15831c7..eafd137ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -694,7 +694,7 @@ jobs: grep --quiet '"isError":true' introspected.txt grep --quiet 'tool_server_not_configured' introspected.txt call_mcp /mcp '{"jsonrpc":"2.0","id":8,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"ci","version":"1"}}}' initialized.txt - grep --quiet 'Before writing a definition, read its format with get_guide' initialized.txt + grep --quiet 'Studio manages them; your agent is a separate client' initialized.txt call_mcp /mcp '{"jsonrpc":"2.0","id":9,"method":"tools/call","params":{"name":"get_guide","arguments":{"guide":"reasoning-function"}}}' guide.txt grep --quiet '# Reasoning function format' guide.txt grep --quiet 'On this server, ' guide.txt diff --git a/.oxfmtrc.json b/.oxfmtrc.json index 8db13a53b..23edc3dd2 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -12,8 +12,7 @@ "pnpm-lock.yaml", "CHANGELOG.md", "packages/api/src/testing/openapi/openapi-3.1-schema-2022-10-07.json", - "docs/assets/openapi.json", - "**/*.overview.md" + "docs/assets/openapi.json" ], "overrides": [ { diff --git a/capabilities/computation/README.md b/capabilities/computation/README.md index 3250bbae8..e9fe07d25 100644 --- a/capabilities/computation/README.md +++ b/capabilities/computation/README.md @@ -6,7 +6,7 @@ User documentation is [Computation function format](../../docs/reference/computa ## The document -`parseComputationDocument(source)` reads a document with the shared reader of [`@on.auto/definitions/document`](../../packages/definitions/README.md#reading-function-documents) and gives a `ComputationFunctionDefinitionDocument`: an optional `description`, an optional `about`, which `AboutSchema` of `@on.auto/definitions/document` bounds and the summary carries into the overview, the `language`, `typescript`, the compiled `input.schema` and `output.schema` when the document has them, or for `output.shape` the [output shape](../../packages/definitions/README.md#output-shapes) it names with its schema compiled as `output.schema`, the `program`, the module, and the line it starts on. A language other than `typescript` is refused with the engine's `oneLanguage`, `The playbook's one language is TypeScript; write the program as a TypeScript function`, at its line. The front matter's keys are `description`, `about`, `language`, `input.schema`, `output.schema` and `output.shape`, the last two never together; every other key, `model`, `config`, `tools`, `output.format` and `input.default` among them, is an issue at its line, and the message for empty front matter names `the language`. A schema is compiled with the reader's compiler and validates values nested at most 512 levels. +`parseComputationDocument(source)` reads a document with the shared reader of [`@on.auto/definitions/document`](../../packages/definitions/README.md#reading-function-documents) and gives a `ComputationFunctionDefinitionDocument`: an optional `description`, the `language`, `typescript`, the compiled `input.schema` and `output.schema` when the document has them, or for `output.shape` the [output shape](../../packages/definitions/README.md#output-shapes) it names with its schema compiled as `output.schema`, the `program`, the module, and the line it starts on. A language other than `typescript` is refused with the engine's `oneLanguage`, `The playbook's one language is TypeScript; write the program as a TypeScript function`, at its line. The front matter's keys are `description`, `language`, `input.schema`, `output.schema` and `output.shape`, the last two never together; every other key, `model`, `config`, `tools`, `output.format` and `input.default` among them, is an issue at its line, and the message for empty front matter names `the language`. A schema is compiled with the reader's compiler and validates values nested at most 512 levels. Parsing keeps the module as it is written. The module is checked when the document is saved, by the capability's `check` (`src/document/document-check.ts`), which asks the pool's check worker of [`@on.auto/definitions`](../../packages/definitions/README.md#the-check-at-save), a step of the save beside `prepare`, against `Input` and `Output`, the schemas as types, with the TypeScript compiler and the sandbox's own library, each problem refused with its line in the document and the compiler's words, in the words of a document that does not parse; a check that a ready checker does not finish within 2 seconds leaves the document unsaved, `invalid_input`, and a save that finds no checker ready is `unavailable`. A run never checks again. @@ -65,4 +65,4 @@ A checkpoint counts the engine's own steps, so a loop whose body is one native o ## Source -`src/index.ts` is the entry point and `src/testing/index.ts` the entry point of the test support. `src/document` holds the document: its type, the front matter's keys and parsing. `src/run` holds a run: its bounds, the input's checks, the call of the pool and the endings. `src/capability` holds the capability, whose guide is the public reference page, served to agents as `computation-function`. `src/overview` is the function's overview, told as what happens by the rules of [Told as what happens](../../packages/definitions/README.md#told-as-what-happens): one sentence, `computedSentence` of `@on.auto/definitions`, of what it is given and what it answers with, each field by its meaning, or the shape it answers, and under Details the tables of its input and its output and its program's language, through `inputDetails`, `outputDetails` and `programSection`. `reference-example.overview.md` is the overview of the example of the public reference page, `shaped-example.overview.md` that of the computation function of the reference of output shapes, and `month-window.overview.md` that of a function a workflow calls, as a read writes it with the workflow, which `computation-overview.test.ts` writes again and compares byte for byte. `src/testing` holds the example and what the tests share. +`src/index.ts` is the entry point and `src/testing/index.ts` the entry point of the test support. `src/document` holds the document: its type, the front matter's keys and parsing. `src/run` holds a run: its bounds, the input's checks, the call of the pool and the endings. `src/capability` holds the capability, whose guide is the public reference page, served to agents as `computation-function`. `src/testing` holds the example and what the tests share. diff --git a/capabilities/computation/src/capability/computation-function.test.ts b/capabilities/computation/src/capability/computation-function.test.ts index 24ca717f4..50316f0c7 100644 --- a/capabilities/computation/src/capability/computation-function.test.ts +++ b/capabilities/computation/src/capability/computation-function.test.ts @@ -166,7 +166,7 @@ describe('a computation function definition that is refused', () => { { pointer: '', detail: - 'Line 3, /model: model is not a key of the front matter; it takes description, about, language, input, output', + 'Line 3, /model: model is not a key of the front matter; it takes description, language, input, output', }, { pointer: '', detail: 'Line 5: The definition has no program: write it after the front matter' }, ], diff --git a/capabilities/computation/src/capability/computation-function.ts b/capabilities/computation/src/capability/computation-function.ts index 5b4ff7195..3aa10479b 100644 --- a/capabilities/computation/src/capability/computation-function.ts +++ b/capabilities/computation/src/capability/computation-function.ts @@ -14,7 +14,6 @@ import { Effect, Result } from 'effect'; import type { ComputationFunctionDefinitionDocument } from '../document/computation-document.ts'; import { documentCheck, invalidDefinition } from '../document/document-check.ts'; import { parseComputationDocument } from '../document/document-parsing.ts'; -import { computationOverview } from '../overview/computation-overview.ts'; import { computationRun } from '../run/computation-run.ts'; import { computationBounds } from '../run/run-bounds.ts'; @@ -32,10 +31,9 @@ function parse(source: string): Effect.Effect run(document, input, stripped), longestAnyRunMs: deadlineMs, }); diff --git a/capabilities/computation/src/document/computation-document.ts b/capabilities/computation/src/document/computation-document.ts index d23fb743c..e07c3859b 100644 --- a/capabilities/computation/src/document/computation-document.ts +++ b/capabilities/computation/src/document/computation-document.ts @@ -7,7 +7,6 @@ export interface OutputContract extends ValueContract { export interface ComputationFunctionDefinitionDocument { readonly description?: string; - readonly about?: string; readonly language: 'typescript'; readonly input: ValueContract; readonly output: OutputContract; diff --git a/capabilities/computation/src/document/document-parsing.test.ts b/capabilities/computation/src/document/document-parsing.test.ts index 3ae11fa97..d9b4086af 100644 --- a/capabilities/computation/src/document/document-parsing.test.ts +++ b/capabilities/computation/src/document/document-parsing.test.ts @@ -91,9 +91,9 @@ describe('the front matter of a computation function definition', () => { ), ), ).toEqual([ - 'Line 3, /model: model is not a key of the front matter; it takes description, about, language, input, output', - 'Line 4, /config: config is not a key of the front matter; it takes description, about, language, input, output', - 'Line 5, /tools: tools is not a key of the front matter; it takes description, about, language, input, output', + 'Line 3, /model: model is not a key of the front matter; it takes description, language, input, output', + 'Line 4, /config: config is not a key of the front matter; it takes description, language, input, output', + 'Line 5, /tools: tools is not a key of the front matter; it takes description, language, input, output', 'Line 6, /input/default: default is not a key of input; it takes schema', 'Line 7, /output/format: format is not a key of output; it takes schema, shape', ]); @@ -118,7 +118,7 @@ describe('the values in the front matter of a computation function definition', it('is checked with the program, whose issues it reports beside its own', () => { expect(issuesIn(programDocument(' ', 'language: typescript\nsize: 2'))).toEqual([ - 'Line 3, /size: size is not a key of the front matter; it takes description, about, language, input, output', + 'Line 3, /size: size is not a key of the front matter; it takes description, language, input, output', 'Line 5: The definition has no program: write it after the front matter', ]); expect(issuesIn('---\nlanguage: [typescript\n---\n')).toEqual([ @@ -141,23 +141,3 @@ describe('the program of a computation function definition', () => { ).toBe('export default function (input: Input): Output {\n return input.perod;\n}'); }); }); - -describe('the about of a computation function definition', () => { - it('is kept as its author wrote it', () => { - const frontMatter = 'language: typescript\nabout: |\n The finance team reads it.'; - - expect(parsed(programDocument(functionOf('return input;'), frontMatter)).about).toBe( - 'The finance team reads it.\n', - ); - }); - - it('takes no heading, nor does the description, each refused at its line', () => { - const frontMatter = - "language: typescript\ndescription: '# Adds one'\nabout: |\n The finance team reads it.\n ---"; - - expect(issuesIn(programDocument(functionOf('return input;'), frontMatter))).toEqual([ - 'Line 3, /description: description takes no heading, since an overview writes its own: the line that begins “# Adds one” makes one, with # before its words or as a line of = or - under the words above it', - 'Line 6, /about: about takes no heading, since an overview writes its own: this line makes one, with # before its words or as a line of = or - under the words above it', - ]); - }); -}); diff --git a/capabilities/computation/src/document/document-parsing.ts b/capabilities/computation/src/document/document-parsing.ts index 51cfab18d..3d70d2acc 100644 --- a/capabilities/computation/src/document/document-parsing.ts +++ b/capabilities/computation/src/document/document-parsing.ts @@ -86,7 +86,7 @@ function documentFrom( if (issues.length > 0 || Result.isFailure(decoded)) { return Result.fail([...issues, ...issuesOf(program)]); } - const { description, about, input, output } = decoded.success; + const { description, input, output } = decoded.success; const parts = { language: languageOf(decoded.success.language, lines), input: contractOf(input, 'input', lines), @@ -103,7 +103,6 @@ function documentFrom( ? Result.fail(found) : Result.map(Result.all(parts), (checked) => ({ ...(description === undefined ? {} : { description }), - ...(about === undefined ? {} : { about }), language: checked.language, input: checked.input, output: checked.output, diff --git a/capabilities/computation/src/document/front-matter.ts b/capabilities/computation/src/document/front-matter.ts index 53f6b36a2..c3988ad1c 100644 --- a/capabilities/computation/src/document/front-matter.ts +++ b/capabilities/computation/src/document/front-matter.ts @@ -1,9 +1,4 @@ -import { - AboutSchema, - DescriptionSchema, - type FrontMatterSection, - type FrontMatterShape, -} from '@on.auto/definitions/document'; +import { DescriptionSchema, type FrontMatterSection, type FrontMatterShape } from '@on.auto/definitions/document'; import { Schema } from 'effect'; const SectionSchema = Schema.Struct({ schema: Schema.optionalKey(Schema.JsonObject) }); @@ -15,7 +10,6 @@ const OutputSectionSchema = Schema.Struct({ const FrontMatterSchema = Schema.Struct({ description: Schema.optionalKey(DescriptionSchema), - about: Schema.optionalKey(AboutSchema), language: Schema.String, input: Schema.optionalKey(SectionSchema), output: Schema.optionalKey(OutputSectionSchema), diff --git a/capabilities/computation/src/overview/computation-overview.test.ts b/capabilities/computation/src/overview/computation-overview.test.ts deleted file mode 100644 index 2a706ae48..000000000 --- a/capabilities/computation/src/overview/computation-overview.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { readFileSync } from 'node:fs'; - -import { markdownOf } from '@on.auto/definitions'; -import { overviewAsRead } from '@on.auto/definitions/testing'; -import { describe, expect, it } from 'vitest'; - -import { computationWith } from '../testing/computation-runs.ts'; - -const page = readFileSync(new URL('../../../../docs/reference/computation-format.md', import.meta.url), 'utf8'); - -const golden = readFileSync(new URL('reference-example.overview.md', import.meta.url), 'utf8'); - -const [, example = ''] = /```markdown\n([\s\S]*?)```/u.exec(page) ?? []; - -const shapesPage = readFileSync(new URL('../../../../docs/reference/output-shapes.md', import.meta.url), 'utf8'); - -const [, shapedExample = ''] = [...shapesPage.matchAll(/```markdown\n([\s\S]*?)```/gu)][1] ?? []; - -const monthWindow = [ - '---', - `description: 'Turns a month such as 2026-04 or "April 2026" into its date range, the previous month''s date range and readable labels'`, - 'language: typescript', - 'input:', - ' schema:', - ' type: object', - ' required: [month]', - ' properties:', - " month: { type: string, minLength: 6, maxLength: 20, description: 'The month to report on, as 2026-04 or April 2026' }", - 'output:', - ' schema:', - ' type: object', - ' required: [month, label, start, end, prev_label, prev_start, prev_end]', - ' properties:', - ' month: { type: string }', - ' label: { type: string }', - ' start: { type: string }', - ' end: { type: string }', - ' prev_label: { type: string }', - ' prev_start: { type: string }', - ' prev_end: { type: string }', - '---', - 'export default function (input: Input): Output {', - " const [year = '', month = ''] = input.month.split('-');", - " const previous = month === '01' ? `${Number(year) - 1}-12` : `${year}-${String(Number(month) - 1).padStart(2, '0')}`;", - ' return {', - ' month: input.month,', - ' label: input.month,', - ' start: `${input.month}-01`,', - ' end: `${input.month}-28`,', - ' prev_label: previous,', - ' prev_start: `${previous}-01`,', - ' prev_end: `${previous}-28`,', - ' };', - '}', -].join('\n'); - -function overviewOf(source: string, name: string): string { - return markdownOf(computationWith().prepared(source).overview(name)); -} - -describe('the overview of the example of the reference of computation functions', () => { - it('is, byte for byte, the overview pinned beside this test', () => { - expect(overviewOf(example, 'campaign-pace')).toBe(golden); - }); -}); - -describe('the overview of the computation function of the reference of output shapes', () => { - it('is, byte for byte, the overview pinned beside this test, the shape it answers told in its sentence', () => { - expect(overviewOf(shapedExample, 'monthly-sign-ups')).toBe( - readFileSync(new URL('shaped-example.overview.md', import.meta.url), 'utf8'), - ); - }); -}); - -describe('the overview of a computation function a workflow calls', () => { - it('tells what it is given and answers by the meanings of its fields, and is, as read, the overview pinned beside this test', () => { - const overview = computationWith().prepared(monthWindow).overview('month-window'); - const caller = { - type: 'workflow', - name: 'monthly-website-report', - details: { - title: 'Monthly website report', - calls: [{ type: 'computation', name: 'month-window', steps: [{ number: 1, name: 'work-out-month' }] }], - }, - }; - - expect(overviewAsRead({ type: 'computation', name: 'month-window', overview }, [caller])).toBe( - readFileSync(new URL('month-window.overview.md', import.meta.url), 'utf8'), - ); - }); -}); - -describe('the overview of a computation function', () => { - it('says that a function without schemas is given any input and answers any value, and holds the rest under Details', () => { - const source = [ - '---', - 'language: typescript', - '---', - 'export default function (input: Input): Output {', - ' return input;', - '}', - ]; - - expect(overviewOf(source.join('\n'), 'echo')).toBe( - [ - '# echo', - 'This computation function has no description yet.', - 'It is given any input, and a program works out the answer, the same every time for the same input: any value.', - '## Details', - '### Input', - 'It takes any input, which its document does not describe.', - '### Output', - 'It produces any output, which its document does not describe.', - '### Program', - "TypeScript, in the definition's document.\n", - ].join('\n\n'), - ); - }); -}); - -describe('the overview of a computation function whose document has an about', () => { - it('writes the about as its author wrote it, after the description and before what it is given', () => { - const source = [ - '---', - 'description: Adds one', - 'about: |', - ' The finance team reads it to see *how many* campaigns there are.', - '', - ' A workflow runs it at the end of each period.', - 'language: typescript', - '---', - 'export default function (input: Input): Output {', - ' return input;', - '}', - ]; - - expect(overviewOf(source.join('\n'), 'count').split('\n\n').slice(0, 5)).toEqual([ - '# count', - 'Adds one.', - 'The finance team reads it to see *how many* campaigns there are.', - 'A workflow runs it at the end of each period.', - 'It is given any input, and a program works out the answer, the same every time for the same input: any value.', - ]); - }); -}); diff --git a/capabilities/computation/src/overview/computation-overview.ts b/capabilities/computation/src/overview/computation-overview.ts deleted file mode 100644 index 89f59b798..000000000 --- a/capabilities/computation/src/overview/computation-overview.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { computedSentence, inputDetails, outputDetails, programSection, type ToldParts } from '@on.auto/definitions'; - -import type { ComputationFunctionDefinitionDocument } from '../document/computation-document.ts'; - -export function computationOverview({ language, input, output }: ComputationFunctionDefinitionDocument): ToldParts { - return { - body: [computedSentence({ input: input.schema?.document, output: output.schema?.document, shape: output.shape })], - appendix: [ - inputDetails(input.schema?.document), - outputDetails(output.schema?.document, output.shape), - programSection(language), - ], - }; -} diff --git a/capabilities/computation/src/overview/month-window.overview.md b/capabilities/computation/src/overview/month-window.overview.md deleted file mode 100644 index acd9174a0..000000000 --- a/capabilities/computation/src/overview/month-window.overview.md +++ /dev/null @@ -1,37 +0,0 @@ -# month-window - -Turns a month such as 2026-04 or "April 2026" into its date range, the previous month's date range and readable labels. - -It is given the month to report on, as 2026-04 or April 2026, and a program works out the answer, the same every time for the same input: the month, the label, the start, the end, the prev label, the prev start and the prev end. - -## Used by - -- The workflow **Monthly website report** calls it in step 1, **Work out month**. - -## Details - -### Input - -| Field | Required | What it is | -| --- | --- | --- | -| **month** | yes | The month to report on, as 2026-04 or April 2026. Text, 6 to 20 characters. | - -### Output - -| Field | Required | What it is | -| --- | --- | --- | -| **month** | yes | Text. | -| **label** | yes | Text. | -| **start** | yes | Text. | -| **end** | yes | Text. | -| **prev_label** | yes | Text. | -| **prev_start** | yes | Text. | -| **prev_end** | yes | Text. | - -### Program - -TypeScript, in the definition's document. - -### Used by - -- `monthly-website-report`, a workflow diff --git a/capabilities/computation/src/overview/reference-example.overview.md b/capabilities/computation/src/overview/reference-example.overview.md deleted file mode 100644 index 25b59c19d..000000000 --- a/capabilities/computation/src/overview/reference-example.overview.md +++ /dev/null @@ -1,25 +0,0 @@ -# campaign-pace - -Spend, pace and projection per campaign, in cents, for a reporting period. - -It is given the rows and the period, and a program works out the answer, the same every time for the same input: the campaigns and the total spend cents. - -## Details - -### Input - -| Field | Required | What it is | -| --- | --- | --- | -| **rows** | yes | A list of at most 4,000 records with: **campaign** (text), **cost_cents** (a whole number, from 0 to 5,000,000), **budget_cents** (a whole number, from 0 to 1,000,000,000). | -| **period** | yes | A record with: **days_elapsed** (a whole number, from 1 to 366), **days_total** (a whole number, from 1 to 366). | - -### Output - -| Field | Required | What it is | -| --- | --- | --- | -| **campaigns** | yes | A list of records. | -| **total_spend_cents** | yes | A whole number. | - -### Program - -TypeScript, in the definition's document. diff --git a/capabilities/computation/src/overview/shaped-example.overview.md b/capabilities/computation/src/overview/shaped-example.overview.md deleted file mode 100644 index b569c38b7..000000000 --- a/capabilities/computation/src/overview/shaped-example.overview.md +++ /dev/null @@ -1,21 +0,0 @@ -# monthly-sign-ups - -Monthly sign-ups, with their monthly average marked. - -It is given the months, and a program works out the answer, the same every time for the same input: a series over time. - -## Details - -### Input - -| Field | Required | What it is | -| --- | --- | --- | -| **months** | yes | A list of 1 to 120 records with: **month** (text), **signups** (a whole number, at least 0). | - -### Output - -It answers a series over time, in **unit**: 1 to 20 lines, one for each **name**, of at most 2,000 **points** in all, each a **value** at its **at**, with a **low** and a **high** where given, and at most 8 **marks**, each a **value** with a **label**. Each **at** is a day, week, month, quarter or year, as **period** names, or a date and time where it names none, and **origin** is the last one observed. - -### Program - -TypeScript, in the definition's document. diff --git a/capabilities/coordination/README.md b/capabilities/coordination/README.md index 0a11278e9..e0433fc2e 100644 --- a/capabilities/coordination/README.md +++ b/capabilities/coordination/README.md @@ -25,7 +25,7 @@ A run a caller starts, one whose context names no `trigger`, whether `run_defini ## The shape of a workflow's output -`output.shape` at the top of a document names an [output shape](../../packages/definitions/README.md#output-shapes) for the run's output, a key of the runtime's own beside the DSL's `output.as` and `output.schema` (`src/document/workflow-output.ts`). The DSL's own check of the document reads it without the key, so a task's `output.shape` is still refused as a key the DSL does not know; a name that is no shape, or a shape beside `output.schema`, is refused at its line with the words of `declaredShape`. The summary publishes the shape's schema as the `outputSchema` and its name as `details.shape`, beside `calls`, and the overview says the shape in **What it produces**. The machine's functions refuse the run's output, after `output.as`, when it does not match the shape, `The output of the workflow does not match the shape : `, which ends the run as a `conflict` of the kind `unworkable` (see [the engine](../../packages/workflow-engine/README.md#the-output-of-a-run)); a workflow that names no shape ends as it did, whatever its `output.schema` says. +`output.shape` at the top of a document names an [output shape](../../packages/definitions/README.md#output-shapes) for the run's output, a key of the runtime's own beside the DSL's `output.as` and `output.schema` (`src/document/workflow-output.ts`). The DSL's own check of the document reads it without the key, so a task's `output.shape` is still refused as a key the DSL does not know; a name that is no shape, or a shape beside `output.schema`, is refused at its line with the words of `declaredShape`. The summary publishes the shape's schema as the `outputSchema` and its name as `details.shape`, beside `calls`. The machine's functions refuse the run's output, after `output.as`, when it does not match the shape, `The output of the workflow does not match the shape : `, which ends the run as a `conflict` of the kind `unworkable` (see [the engine](../../packages/workflow-engine/README.md#the-output-of-a-run)); a workflow that names no shape ends as it did, whatever its `output.schema` says. ## The expressions of a document @@ -39,7 +39,7 @@ Parsing keeps the document with the locator of its YAML and its compiled input s A call of a workflow, `call: run_definition`, executes the definition it names through the operations for the caller who started the run, under a run id derived from the workflow's run id, the task and the run of the task (`nestedRunId` of `@on.auto/definitions`), so a call started again is the same run. The machine names that run on the call's `waiting` entry through `childOf` (`src/calls/child-run.ts`), which derives it the same way and names nothing for arguments the call would refuse. The call starts it with a lineage, the record that started the call as the cause and the workflow's tree as the correlation, with the reaction depth of the run, and with a call depth one more than the run's and the call it answers, which the server passes in the playbook request of the in-process start, never in its input. A call of a run that finishes later, such as one of another workflow, answers the host that it waits for that run, by its run id; the host gives the call the ending of that run, mapped by `callResultOfEnding`. Arguments that name no definition are rejected as `invalid_arguments`, a `validation` error of the task. A rejection keeps its detail, its `issues`, its `kind` and its `because`, which the error of the task carries for a `catch` to read, and an output of more than 1 MiB fails the call. The detail of a refused input already says each issue in a sentence, so the call adds nothing to it; its issues keep the pointers the step saw, under `/input`. The functions the machine is given word the title of a rejected call (`refusal`): a workflow it ran `stopped` when the rejection came from one of that workflow's steps, `invalid_input`, `unavailable`, `conflict` or `unanswered`, and `refused its input` when its input schema refused the input the call gave it, a rejection of the kind `input_schema` (`InvalidInputKindSchema` of `@on.auto/operations`), and any other definition, or a workflow cancelled, not found or not allowed, says what its reason did in the words of `refusalInWords`. -The summary of a workflow keeps in its `details`, as `calls`, the definitions its `run_definition` tasks name with a `type` and a `name` written out, nested tasks among them, in the order of the document and each once, each with the `steps` that call it, the number each has in the workflow's diagram and the name of its task (`src/document/workflow-summary.ts`), and, as `title`, the document's `title` where it has one; it keeps no `details` when it names no definition and has no title. Its `description` is the document's `summary`, or else its `title`, and its `about` the document's `metadata.about`, which the overview writes after the description (`src/document/workflow-texts.ts`): an `about` that `AboutSchema` of `@on.auto/definitions/document` refuses, anything but text of 1 to 4,000 characters, is refused at its line, and what `writtenTextIssues` of the same module refuses in it or in the text that is the description, a heading, a line `## Details` or a block left open, is refused at the line and the column it is at in a literal block scalar, `|`, and in any other style at the line and the column its value opens at, naming the line by its first words. The reader and the summary each read the document's header, `document`, once, through `headerOf`. A call whose `type` or `name` is an expression names no definition until it runs. `definitionCallOf` is the one reading of a `run_definition` task, its `type` and its `name` where each is written out and the arguments it gives: the summary's calls are read with it, and so are the calls whose longest run bounds the wait of a call (`src/runs/call-limits.ts`) and the words and the label of the step in its diagram. `numberedTasksOf` of `src/steps/placed-tasks.ts` is the one numbering of the steps, which the diagram, its list and the summary's steps share. The summary is computed once per save and reaches the playbook's registry in the version's `details`, from which the read of each definition the workflow names says, under **Used by**, that the workflow, by its title or its name in words, calls it in each of those steps, in one sentence, and the read of the workflow says, under **What it calls**, each definition it names with its description (see [Used by](../../packages/definitions/README.md#used-by)). +The summary of a workflow keeps in its `details`, as `calls`, the definitions its `run_definition` tasks name with a `type` and a `name` written out, nested tasks among them, in the order of the document and each once, each with the `steps` that call it, the number of each, its place among the workflow's tasks in the order of the document as `allTaskEntries` of `@on.auto/workflow-engine` walks them, the tasks within a task after it and the steps of a `catch` after those of its `try`, and the name of its task (`src/document/workflow-summary.ts`); it keeps no `details` when it names no definition. Its `description` is the document's `summary`, or else its `title`. A call whose `type` or `name` is an expression names no definition until it runs. `definitionCallOf` is the one reading of a `run_definition` task, its `type` and its `name` where each is written out and the arguments it gives: the summary's calls are read with it, and so are the calls whose longest run bounds the wait of a call (`src/runs/call-limits.ts`). The summary is computed once per save and reaches the playbook's registry in the version's `details`, from which the read of each definition the workflow names answers, in `used_by`, that the workflow calls it in each of those steps (see [Used by](../../packages/definitions/README.md#used-by)). ## Triggers @@ -61,14 +61,10 @@ An `emit` task publishes an event to the playbook. Its `event.with` takes a `typ It then presents each step entry of the record as an event of its own, in the entry's order (`src/presenting/step-events.ts`): `step_started`, `step_waiting`, `step_finished` for `completed`, `step_failed` for `raised`, `timed_out` and `cancelled`, and `step_skipped`. Each has the record's `at`, its `data` the step's `name`, `reference` cut at 256 bytes, `run` and `times`, with `waits_for` on a wait, the child's `run_id` on the wait of a call, and on a failure its `outcome` and its error's `type` and `title`, cut at 256 bytes and 1 KiB; its summary names the step by its name in words. Its `id` is the record's message id, an underscore and its number among the record's recorded steps, from 1, `_` (`stepIdSeparator` of `@on.auto/operations`), so it is the same on every read, travels in a path as it is, and a reader resolves it by reading the record; its cause is the step its `caused_by` names, `_`, when that step is in the same record, and the record itself for `input` or for a step of an earlier record, which the input moved on. Records of earlier formats show no step events. -## The overview - -The capability's `overview` (`src/overview/workflow-overview.ts`; see [The overview of a definition](../../packages/definitions/README.md#the-overview-of-a-definition)) says what starts a workflow, each trigger `triggersOfDocument` reads, in words, an event trigger as `On an event,` and each of its filters after `each time` in the words `eventFilterPhrases` of `@on.auto/definitions` gives every event filter, and a call of `run_definition` last (`trigger-words.ts`), with a `cron` in words where its minute and hour are numbers, its month is `*` and its days are every day, a day of the week, a range or a list of them, or a day of the month, and as it is written otherwise (`cron-words.ts`); what it needs and what it produces, from the schemas of the summary `prepare` computed, which the overview is given, with its triggers, rather than computing it again; and what it does. What it calls the read writes after these, from the summary's `calls`, since it says each called definition with its description, which only the read knows; a step whose call names its definition when it runs says so in the list of what it does. `src/steps` draws what it does. `placed-tasks.ts` numbers every task in the order of the document, the tasks within a task after it and the steps of a `catch` after those of its `try` (`numberedTasksOf`), and makes each task a node labelled with its number and its words, which it is given, a `switch` a decision and a `raise` a terminal, but a `for`, a `do`, a `fork` and a `try`, which are each a group, nested in the group of the task around them; a `catch`'s steps sit in the group its `try` sits in. `task-edges.ts` joins the nodes as the document runs them: each trigger to the first step, each step to the next or to the step its `then` names, out of its list at `exit` and nowhere at `end`, around a step whose `if` may not hold, each case of a `switch` to its step and, when no case is a default, the `switch`'s own `then`, the last step of a `for` back to its first, every branch of a `fork` at once and each to the step after it, and every step within a `try` to its `catch` by a dashed edge. `task-words.ts` and `flow-words.ts` say each step in words for the numbered list beneath the diagram, numbered as the diagram is, and `task-diagram.ts` holds the one table of the words of each kind of task, its label from `task-labels.ts` and its sentence. `reference-example.overview.md`, `prediction-example.overview.md` and `shaped-example.overview.md` are the overviews of the examples of the workflow, prediction and output shape references as a read writes them beside the functions the references save, each with the description its reference gives it, which `workflow-overview.test.ts` writes again through `overviewAsRead` of `@on.auto/definitions/testing`, the read's own composition, and compares byte for byte, and the tests of `src/steps` draw a document of each task kind with `doneIn` of `src/testing/done-steps.ts`, which also reads the step numbers of a diagram and of its list. - ## Testing `src/workflows` holds the tests of how a workflow runs, each a document run through the memory driver of the engine (`src/testing/workflows.ts`, `interpret`), with the commands a run gave its ports and the settlement it ended with. `src/input-logs` holds sixteen recorded paths, whose input logs in `input-logs/` are the replay corpus: each replays through the machine to the events it recorded, and each runs today as it was recorded (`RECORD_INPUT_LOGS=1` records them again). The capability and `send_run_event` are tested on a playbook whose workflows run on a host over a private SQLite database in memory (`src/testing/workflow-playbook.ts`), its expressions checked at save by the compiler in a real worker within the 30 s of `checksAwaitedAtMostMs` of `@on.auto/definitions/testing` (`src/testing/expression-checks.ts`), so a check that a busy machine slows is not refused by the 2 s a save allows. ## Source -`src/document` parses a definition document: YAML, the DSL schema and graph, the policy, the schedule and its triggers, the issues and the summary. `src/capability` holds the capability, whose guide is the public reference page, served to agents as `workflow`, `src/overview` its overview and `src/steps` the diagram of its tasks and their words, `src/events` the event operation, `src/calls` what a call of a workflow does, `src/runs` the machine's options, the start of a run, its attributes and the limits of its calls, `src/presenting` the presenter of a run's log, `src/workflows` the tests of how a workflow runs, `src/input-logs` the recorded paths and their corpus, and `src/testing` what the tests share. +`src/document` parses a definition document: YAML, the DSL schema and graph, the policy, the schedule and its triggers, the issues and the summary. `src/capability` holds the capability, whose guide is the public reference page, served to agents as `workflow`, `src/events` the event operation, `src/calls` what a call of a workflow does, `src/runs` the machine's options, the start of a run, its attributes and the limits of its calls, `src/presenting` the presenter of a run's log, `src/workflows` the tests of how a workflow runs, `src/input-logs` the recorded paths and their corpus, and `src/testing` what the tests share. diff --git a/capabilities/coordination/src/capability/workflow.ts b/capabilities/coordination/src/capability/workflow.ts index dbc417f10..770a08717 100644 --- a/capabilities/coordination/src/capability/workflow.ts +++ b/capabilities/coordination/src/capability/workflow.ts @@ -13,7 +13,6 @@ import { Effect, type Schema } from 'effect'; import { checkedExpressions, type ExpressionCheck } from '../document/expression-check.ts'; import { runnableDocument, workflowReaderOf, type ReadWorkflow } from '../document/workflow-document.ts'; import { summaryOf } from '../document/workflow-summary.ts'; -import { workflowOverview } from '../overview/workflow-overview.ts'; import { runPresenter } from '../presenting/run-presenter.ts'; import { started, type WorkflowStarts } from '../runs/run-start.ts'; @@ -47,7 +46,6 @@ export function makeWorkflowAdapter(dependencies: WorkflowAdapterDependencies): readWorkflowDocument(source, dependencies.mostDurationMs), check: checkedExpressions(dependencies.check), summarize: ({ document }) => summaryOf(document), - overview: workflowOverview, run: (read, input, run, stripped) => Effect.flatMap(admittedInput(read, input, run), (admitted) => started(dependencies, runnableDocument(read.document, stripped), admitted, run), diff --git a/capabilities/coordination/src/document/workflow-document.test.ts b/capabilities/coordination/src/document/workflow-document.test.ts index a190f22de..f242daf5f 100644 --- a/capabilities/coordination/src/document/workflow-document.test.ts +++ b/capabilities/coordination/src/document/workflow-document.test.ts @@ -123,13 +123,7 @@ describe('a workflow document that nests too deeply', () => { }); describe('the summary of a workflow definition', () => { - it('carries the about of the metadata of the document, which its overview writes', () => { - expect( - summaryOf({ document: { summary: 'Greets', metadata: { about: 'It serves the front desk.' } }, do: [] }), - ).toEqual({ description: 'Greets', about: 'It serves the front desk.' }); - }); - - it('is the summary or title of the document, its inline input and output schemas, and its title in its details', () => { + it('is the summary or title of the document and its inline input and output schemas', () => { expect( summaryOf({ document: { summary: 'Greets', title: 'Greeting' }, @@ -141,12 +135,8 @@ describe('the summary of a workflow definition', () => { description: 'Greets', inputSchema: { type: 'object' }, outputSchema: { type: 'string' }, - details: { title: 'Greeting' }, - }); - expect(summaryOf({ document: { title: 'Greeting' }, do: [] })).toEqual({ - description: 'Greeting', - details: { title: 'Greeting' }, }); + expect(summaryOf({ document: { title: 'Greeting' }, do: [] })).toEqual({ description: 'Greeting' }); expect(summaryOf({ do: [] })).toEqual({}); }); @@ -159,65 +149,3 @@ describe('the summary of a workflow definition', () => { }); }); }); - -const greeting = 'do:\n - greet: { set: { a: 1 } }\n'; - -const notRun = 'The workflow document is not a workflow this runtime runs'; - -function withAbout(about: string): string { - return `${header} metadata:\n about: ${about}\n${greeting}`; -} - -describe('the about of a workflow document', () => { - it('is text of 1 to 4000 characters under the metadata of the document, refused at its line otherwise', async () => { - expect(await parsed(withAbout('5'))).toMatchObject( - rejected(notRun, ['Line 7, column 12: at /document/metadata/about: Expected string']), - ); - expect(await parsed(withAbout('x'.repeat(4001)))).toMatchObject( - rejected(notRun, [ - 'Line 7, column 12: at /document/metadata/about: Expected a value with a length of at most 4000', - ]), - ); - expect(await parsed(withAbout('x'.repeat(4000)))).toMatchObject({ _tag: 'Success' }); - }); -}); - -describe('the summary, the title and the about of a workflow document', () => { - it('take no heading, each refused at its line in a literal block and at the line its value opens in any other style', async () => { - const about = [' about: |', ' It serves the front desk.', '', ' ## When it runs'].join('\n'); - const source = `${header} summary: '# Greets a guest'\n metadata:\n${about}\n${greeting}`; - - expect(await parsed(source)).toMatchObject( - rejected(notRun, [ - 'Line 6, column 12: at /document/summary: summary takes no heading, since an overview writes its own: the line that begins “# Greets a guest” makes one, with # before its words or as a line of = or - under the words above it', - 'Line 11, column 7: at /document/metadata/about: about takes no heading, since an overview writes its own: this line makes one, with # before its words or as a line of = or - under the words above it', - ]), - ); - }); - - it('refuse a heading in the title where it is the description, and take one where the summary is', async () => { - const titled = `${header} title: '# Greeting'\n`; - - expect(await parsed(`${titled}${greeting}`)).toMatchObject( - rejected(notRun, [ - 'Line 6, column 10: at /document/title: title takes no heading, since an overview writes its own: the line that begins “# Greeting” makes one, with # before its words or as a line of = or - under the words above it', - ]), - ); - expect(await parsed(`${titled} summary: Greets a guest\n${greeting}`)).toMatchObject({ _tag: 'Success' }); - }); - - it('leave no block open and hold no line of the overview, at the line of the text in a literal block or of the value', async () => { - const about = [' about: |', ' It serves the front desk.', ' n2 - n2 -.->|"on error 409"| n3 -``` - -Its steps, numbered as in the diagram: - -1. **forecast** tries step 2; on an error with status 409, when `$error.kind == "no_active_version"`, it does step 3. -2. **ask** runs the prediction function **weekly-orders**. -3. **none** sets **forecast**. - -## What it calls - -It calls the prediction function **Weekly orders**: Predicts the orders of each region one and four weeks ahead. - -## Details - -### What it calls - -- `weekly-orders`, a prediction function diff --git a/capabilities/coordination/src/overview/reference-example.overview.md b/capabilities/coordination/src/overview/reference-example.overview.md deleted file mode 100644 index 24d872bae..000000000 --- a/capabilities/coordination/src/overview/reference-example.overview.md +++ /dev/null @@ -1,59 +0,0 @@ -# review-and-approve - -Reviews a campaign brief, then asks its owner to approve the review. - -## What starts it - -- By a call of `run_definition`, from a caller or from a step of another workflow - -## What it needs - -| Field | Required | What it is | -| --- | --- | --- | -| **brief** | yes | Text. | -| **owner** | yes | Text. | - -## What it produces - -It produces any output, which its document does not describe. - -## What it does - -```mermaid -flowchart LR - subgraph g1["1. Try"] - n2["2. Run review-campaign-brief"] - end - subgraph g2["4. Try"] - n4["5. Run approve-campaign-brief"] - end - n1(["By a call"]) - n3(["3. Raise “The review could not be run”"]) - n5["6. Set verdict and note"] - n1 --> n2 - n2 --> n4 - n2 -.->|"on error 503, after retries"| n3 - n4 -.->|"on an error"| n5 -``` - -Its steps, numbered as in the diagram: - -1. **review** tries step 2; on an error with status 503, it tries again under the retry policy **patient**, and once no retry is left, it does step 3. -2. **review-brief** runs the reasoning function **review-campaign-brief**. -3. **give-up** raises the error *The review could not be run*, with status 503. -4. **approval** tries step 5; on an error whose type is `https://on.auto/problems/unanswered`, it does step 6. -5. **ask-for-approval** runs the interaction function **approve-campaign-brief**. -6. **no-answer** sets **verdict** and **note**. - -## What it calls - -It calls the reasoning function **Review campaign brief**: Reviews a campaign brief against four criteria and recommends Ready or Revise. - -It calls the interaction function **Approve campaign brief**: Asks the campaign owner to approve a reviewed brief. - -## Details - -### What it calls - -- `review-campaign-brief`, a reasoning function -- `approve-campaign-brief`, an interaction function diff --git a/capabilities/coordination/src/overview/shaped-example.overview.md b/capabilities/coordination/src/overview/shaped-example.overview.md deleted file mode 100644 index 71e42ad57..000000000 --- a/capabilities/coordination/src/overview/shaped-example.overview.md +++ /dev/null @@ -1,41 +0,0 @@ -# campaign-pace-table - -Computes the pace of each campaign and answers it as a table. - -## What starts it - -- By a call of `run_definition`, from a caller or from a step of another workflow - -## What it needs - -| Field | Required | What it is | -| --- | --- | --- | -| **rows** | yes | A list. | -| **period** | yes | A record. | - -## What it produces - -It answers a table of 1 to 50 **columns**, each a **name** of its own with a **unit** where given, and at most 500 **rows**, each one cell for each column: text, a number, yes or no, or nothing. - -## What it does - -```mermaid -flowchart LR - n1(["By a call"]) - n2["1. Run campaign-pace"] - n1 --> n2 -``` - -Its steps, numbered as in the diagram: - -1. **pace** runs the computation function **campaign-pace**. - -## What it calls - -It calls the computation function **Campaign pace**: Spend, pace and projection per campaign, in cents, for a reporting period. - -## Details - -### What it calls - -- `campaign-pace`, a computation function diff --git a/capabilities/coordination/src/overview/trigger-words.test.ts b/capabilities/coordination/src/overview/trigger-words.test.ts deleted file mode 100644 index 48f531922..000000000 --- a/capabilities/coordination/src/overview/trigger-words.test.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { plainTextOf, type Phrases } from '@on.auto/definitions'; -import { describe, expect, it } from 'vitest'; - -import { summaryOf } from '../document/workflow-summary.ts'; -import { workflow } from '../testing/workflows.ts'; -import { cronInWords } from './cron-words.ts'; -import { startsOf } from './trigger-words.ts'; - -function startsIn(schedule: string) { - return startsOf(summaryOf(workflow(`schedule:\n${schedule}\ndo: []`)).triggers ?? []); -} - -const eventSchedule = [ - ' on:', - ' any:', - ' - with: { type: com.example.brief.submitted, data: \'${ $data.priority === "high" }\' }', - ' - with: { type: run_succeeded, subject: reasoning/summarize, depth: 2 }', - ' - with: { type: com.example.brief.closed, data: { closed: true } }', -].join('\n'); - -function plainOf(phrases: Phrases): string { - return phrases - .map((phrase) => { - if (typeof phrase === 'string') { - return phrase; - } - return 'name' in phrase ? `**${phrase.name}**` : `\`${plainTextOf([phrase])}\``; - }) - .join(''); -} - -describe('the words of an event trigger', () => { - it('say each of its filters in the words of an event filter, with what each tests, an event at a time', () => { - expect(startsIn(eventSchedule).map(({ label, words }) => [label, plainOf(words)])).toEqual([ - [ - 'On com.example.brief.submitted, run_succeeded, or com.example.brief.closed', - [ - 'On an event, each time an event `com.example.brief.submitted` is published to the playbook, when `$data.priority === "high"` holds for its data,', - 'each time a run of the reasoning function **summarize** succeeds, when its `depth` is `2`,', - 'or each time an event `com.example.brief.closed` is published to the playbook, when its data is exactly `{"closed":true}`', - ].join(' '), - ], - ['By a call', 'By a call of `run_definition`, from a caller or from a step of another workflow'], - ]); - }); -}); - -describe('the words of each way a workflow starts', () => { - it('say an event of a type alone, a cron schedule in words where it reads plainly, else as written, and an every', () => { - const [ping, plain, every] = startsIn( - " on: { one: { with: { type: com.example.ping } } }\n cron: '30 18 * * *'\n every: { hours: 1, minutes: 30 }", - ); - const [written] = startsIn(" cron: '*/5 * * * *'"); - - expect([ping, plain, every, written]).toEqual([ - { - label: 'On com.example.ping', - words: [ - 'On an event, ', - 'each time ', - 'an event ', - { code: 'com.example.ping' }, - ' is published to the playbook', - ], - }, - { label: 'At 18:30 UTC every day', words: ['On a schedule, at 18:30 UTC every day'] }, - { - label: 'Every 1 hour and 30 minutes', - words: ['Every 1 hour and 30 minutes, counted from when the trigger was saved'], - }, - { - label: 'On the schedule */5 * * * *', - words: ['On the cron schedule ', { code: '*/5 * * * *' }, ', read in UTC'], - }, - ]); - }); - - it('end with a call, the one way a workflow without a schedule starts', () => { - expect(startsOf([])).toEqual([ - { - label: 'By a call', - words: ['By a call of ', { code: 'run_definition' }, ', from a caller or from a step of another workflow'], - }, - ]); - }); -}); - -describe('a cron schedule in words', () => { - it('names the time in UTC and the days, a day of the week, a range, a list or a day of the month', () => { - expect( - ['0 9 * * 1-5', '0 7 * * 1', '0 7 * * 0', '0 7 * * 7', '0 7 * * 1,3,5', '0 0 1 * *', ' 5 23 * * * '].map((cron) => - cronInWords(cron), - ), - ).toEqual([ - 'at 09:00 UTC from Monday to Friday', - 'at 07:00 UTC every Monday', - 'at 07:00 UTC every Sunday', - 'at 07:00 UTC every Sunday', - 'at 07:00 UTC on Monday, Wednesday, and Friday', - 'at 00:00 UTC on day 1 of each month', - 'at 23:05 UTC every day', - ]); - }); - - it('reads no words of a schedule that does not read plainly', () => { - expect( - [ - '*/5 * * * *', - '0 */2 * * *', - '60 9 * * *', - '0 24 * * *', - '0 9 * 1 *', - '0 9 1 * 1', - '0 9 32 * *', - '0 9 * * 5-1', - '0 9 * * 1-8', - '0 9 * * 1,9', - '0 9 * * MON', - '0 9 *', - '0 9 * * * 2026', - ].map((cron) => cronInWords(cron)), - ).toEqual(Array.from({ length: 13 })); - }); -}); diff --git a/capabilities/coordination/src/overview/trigger-words.ts b/capabilities/coordination/src/overview/trigger-words.ts deleted file mode 100644 index 7968236b2..000000000 --- a/capabilities/coordination/src/overview/trigger-words.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { - durationInWords, - eventFilterPhrases, - listedPhrases, - type Phrases, - type Trigger, - type TriggerFilter, -} from '@on.auto/definitions'; -import { alternatives, capitalized } from '@on.auto/operations'; - -import { runDefinitionFunction } from '../document/workflow-functions.ts'; -import { cronInWords } from './cron-words.ts'; - -export interface Start { - readonly label: string; - readonly words: Phrases; -} - -const byACall: Start = { - label: 'By a call', - words: ['By a call of ', { code: runDefinitionFunction }, ', from a caller or from a step of another workflow'], -}; - -function filterPhrases({ type, attributes }: TriggerFilter): Phrases { - return ['each time ', ...eventFilterPhrases({ ...attributes, type })]; -} - -function eventStart(filters: readonly TriggerFilter[]): Start { - const each = filters.map((filter) => filterPhrases(filter)); - return { - label: `On ${alternatives(filters.map(({ type }) => type))}`, - words: ['On an event, ', ...listedPhrases(each, 'or')], - }; -} - -function cronStart(expression: string): Start { - const plain = cronInWords(expression); - return plain === undefined - ? { - label: `On the schedule ${expression}`, - words: ['On the cron schedule ', { code: expression }, ', read in UTC'], - } - : { label: capitalized(plain), words: [`On a schedule, ${plain}`] }; -} - -function startOf(trigger: Trigger): Start { - if (trigger.kind === 'event') { - return eventStart(trigger.filters); - } - if (trigger.kind === 'cron') { - return cronStart(trigger.expression); - } - const period = durationInWords(trigger.milliseconds); - return { label: `Every ${period}`, words: [`Every ${period}, counted from when the trigger was saved`] }; -} - -export function startsOf(triggers: readonly Trigger[]): readonly Start[] { - return [...triggers.map((trigger) => startOf(trigger)), byACall]; -} diff --git a/capabilities/coordination/src/overview/workflow-overview.test.ts b/capabilities/coordination/src/overview/workflow-overview.test.ts deleted file mode 100644 index a6344aef3..000000000 --- a/capabilities/coordination/src/overview/workflow-overview.test.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { readFileSync } from 'node:fs'; - -import { markdownOf } from '@on.auto/definitions'; -import { overviewAsRead, sectionIn, type HeldDefinition } from '@on.auto/definitions/testing'; -import { Effect } from 'effect'; -import { describe, expect, it } from 'vitest'; - -import { summaryOf } from '../document/workflow-summary.ts'; -import { recordingStarts, recordingWorkflows } from '../testing/recording-starts.ts'; -import { workflow } from '../testing/workflows.ts'; -import { workflowOverview } from './workflow-overview.ts'; - -const workflows = recordingWorkflows(recordingStarts().recording); - -const header = "document:\n dsl: '1.0.3'\n namespace: acme\n name: flow\n version: '1.0.0'\n"; - -function blocksOf(page: string, language: string): readonly string[] { - const text = readFileSync(new URL(`../../../../docs/reference/${page}`, import.meta.url), 'utf8'); - return [...text.matchAll(new RegExp(`\`\`\`${language}\\n([\\s\\S]*?)\`\`\``, 'gu'))].map( - ([, body = '']: readonly string[]) => body, - ); -} - -function descriptionIn(page: string, block: number): string { - return ( - /^description: (?.*)$/mu.exec(blocksOf(page, 'markdown')[block] ?? '')?.groups?.['description'] ?? '' - ); -} - -const heldByTheReferences: readonly HeldDefinition[] = [ - { type: 'reasoning', name: 'review-campaign-brief', description: descriptionIn('workflow-format.md', 0) }, - { type: 'interaction', name: 'approve-campaign-brief', description: descriptionIn('workflow-format.md', 1) }, - { type: 'prediction', name: 'weekly-orders', description: descriptionIn('prediction-format.md', 0) }, - { type: 'computation', name: 'campaign-pace', description: descriptionIn('computation-format.md', 0) }, -]; - -function goldenOf(name: string): string { - return readFileSync(new URL(name, import.meta.url), 'utf8'); -} - -function overviewOf(source: string, name = 'flow'): string { - const { summary, overview } = Effect.runSync(workflows.prepare(source)); - const details = summary.details === undefined ? {} : { details: summary.details }; - return overviewAsRead({ type: 'workflow', name, overview: overview(name), ...details }, heldByTheReferences); -} - -const [reviewAndApprove = '', schedule = ''] = blocksOf('workflow-format.md', 'yaml'); - -const [, nextWeekOrders = ''] = blocksOf('prediction-format.md', 'yaml'); - -const [, campaignPaceTable = ''] = blocksOf('output-shapes.md', 'yaml'); - -describe('the overview of the examples of the references of workflows, prediction functions and output shapes', () => { - it('is, byte for byte, the overview pinned beside this test for each, as read beside the functions the references save', () => { - expect(overviewOf(reviewAndApprove, 'review-and-approve')).toBe(goldenOf('reference-example.overview.md')); - expect(overviewOf(nextWeekOrders, 'next-week-orders')).toBe(goldenOf('prediction-example.overview.md')); - expect(overviewOf(campaignPaceTable, 'campaign-pace-table')).toBe(goldenOf('shaped-example.overview.md')); - }); -}); - -describe('the overview of a workflow whose output names a shape', () => { - it('says the shape in what it produces, in place of the fields of its schema', () => { - const overview = overviewOf(`${header}output: { shape: text }\ndo:\n - write: { set: { text: done } }\n`); - - expect(sectionIn(overview, 'What it produces')).toBe('It answers a **text**: Markdown of at most 65,536 bytes.'); - }); -}); - -describe('the overview of a workflow with a schedule', () => { - const overview = overviewOf(`${header}${schedule}do:\n - greet: { set: { greeting: hello } }\n`); - - it('says each trigger of the schedule of the reference in words, and that a call starts it too', () => { - expect(sectionIn(overview, 'What starts it')).toBe( - [ - '- On an event, each time an event `com.example.brief.submitted` is published to the playbook, when `$data.priority === "high"` holds for its data', - '- On a schedule, at 09:00 UTC from Monday to Friday', - '- Every 15 minutes, counted from when the trigger was saved', - '- By a call of `run_definition`, from a caller or from a step of another workflow', - ].join('\n'), - ); - }); - - it('enters its first step from each trigger, and says nothing of what it calls, since it calls nothing', () => { - expect(sectionIn(overview, 'What it does').split('\n').slice(2, 11)).toEqual([ - ' n1(["On com.example.brief.submitted"])', - ' n2(["At 09:00 UTC from Monday to Friday"])', - ' n3(["Every 15 minutes"])', - ' n4(["By a call"])', - ' n5["1. Set greeting"]', - ' n1 --> n5', - ' n2 --> n5', - ' n3 --> n5', - ' n4 --> n5', - ]); - expect(overview).not.toContain('## What it calls'); - }); -}); - -describe('the overview of a workflow that calls definitions', () => { - const source = [ - `${header}output:`, - ' schema:', - ' document: { type: object, properties: { review: { type: string } }, required: [review] }', - 'do:', - ' - first: { call: run_definition, with: { type: reasoning, name: review-brief } }', - ' - again: { call: run_definition, with: { type: reasoning, name: review-brief } }', - " - chosen: { call: run_definition, with: { type: '${ $data.type }', name: review-brief } }", - ' - pace: { call: run_definition, with: { type: computation, name: campaign-pace } }', - ].join('\n'); - - it('says, as read, each definition its steps run once, with its type, and leaves a definition named when a step runs to that step', () => { - const overview = overviewOf(source); - - expect(sectionIn(overview, 'What it produces')).toBe( - '| Field | Required | What it is |\n| --- | --- | --- |\n| **review** | yes | Text. |', - ); - expect(sectionIn(overview, 'What it calls')).toBe( - [ - 'It calls the reasoning function **Review brief**, which the playbook does not have yet.', - 'It calls the computation function **Campaign pace**: Spend, pace and projection per campaign, in cents, for a reporting period.', - ].join('\n\n'), - ); - expect(overview.split('\n\n## Details\n\n')[1]).toBe( - '### What it calls\n\n- `review-brief`, a reasoning function\n- `campaign-pace`, a computation function\n', - ); - expect(sectionIn(overview, 'What it does')).toContain( - '3. **chosen** runs the definition its call names when it runs.', - ); - }); - - it('composes nothing of what it calls itself, since the read says it', () => { - expect(markdownOf(Effect.runSync(workflows.prepare(source)).overview('flow'))).not.toContain('What it calls'); - }); -}); - -describe('the overview of a workflow without steps', () => { - it('says so in one line', () => { - const document = workflow('do: []'); - const parts = workflowOverview({ document, locate: () => ({ line: 1, column: 1 }) }, 'idle', { - summary: summaryOf(document), - }); - const written = markdownOf({ title: 'idle', opening: [], ...parts }); - - expect(written.endsWith('## What it does\n\nIt has no steps, so a run ends as it starts.\n')).toBe(true); - }); -}); - -describe('the overview of a workflow whose document has an about', () => { - it('writes the about as its author wrote it, after the summary and before what starts it', () => { - const about = [' about: |', ' It serves the front desk.', '', ' It runs on each arrival.']; - const source = `${header} summary: Greets a guest\n metadata:\n${about.join('\n')}\ndo:\n - greet: { set: { a: 1 } }\n`; - - expect(overviewOf(source).split('\n\n').slice(0, 5)).toEqual([ - '# flow', - 'Greets a guest', - 'It serves the front desk.', - 'It runs on each arrival.', - '## What starts it', - ]); - }); -}); diff --git a/capabilities/coordination/src/overview/workflow-overview.ts b/capabilities/coordination/src/overview/workflow-overview.ts deleted file mode 100644 index 6cca4aaa7..000000000 --- a/capabilities/coordination/src/overview/workflow-overview.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { - bulletList, - inputSection, - numberedList, - outputSection, - paragraph, - sectionOf, - type OverviewContext, - type OverviewParts, - type OverviewSection, -} from '@on.auto/definitions'; -import { field, type JsonObject } from '@on.auto/workflow-engine'; - -import type { ReadWorkflow } from '../document/workflow-document.ts'; -import { outputShapeOf } from '../document/workflow-output.ts'; -import { taskDiagramOf } from '../steps/task-diagram.ts'; -import { startsOf } from './trigger-words.ts'; - -function doneSection(document: JsonObject, entries: readonly string[]): OverviewSection { - const { diagram, steps } = taskDiagramOf(field(document, 'do'), entries); - return steps.length === 0 - ? sectionOf('What it does', paragraph('It has no steps, so a run ends as it starts.')) - : sectionOf('What it does', diagram, paragraph('Its steps, numbered as in the diagram:'), numberedList(steps)); -} - -export function workflowOverview( - { document }: ReadWorkflow, - _name: string, - { summary }: OverviewContext, -): OverviewParts { - const { inputSchema, outputSchema, triggers = [] } = summary; - const starts = startsOf(triggers); - return { - body: [], - sections: [ - sectionOf('What starts it', bulletList(starts.map(({ words }) => words))), - inputSection(inputSchema), - outputSection(outputSchema, outputShapeOf(document)), - doneSection( - document, - starts.map(({ label }) => label), - ), - ], - appendix: [], - }; -} diff --git a/capabilities/coordination/src/steps/flow-words.ts b/capabilities/coordination/src/steps/flow-words.ts deleted file mode 100644 index 13127ebde..000000000 --- a/capabilities/coordination/src/steps/flow-words.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { listedPhrases, named, type Phrases } from '@on.auto/definitions'; -import { listed } from '@on.auto/operations'; -import { entriesOf, field, textField, type JsonObject } from '@on.auto/workflow-engine'; - -import type { PlacedTask } from './placed-tasks.ts'; -import { casesOf, type SwitchCase } from './task-flow.ts'; -import { expressionText, objectAt } from './task-reading.ts'; - -export interface StepPlace { - readonly siblings: readonly PlacedTask[]; - readonly parent?: PlacedTask; -} - -function stepsWords(first: number, last: number): string { - if (last < first) { - return 'nothing'; - } - return first === last ? `step ${first}` : `steps ${first} to ${last}`; -} - -export function movedWords(next: string | undefined, { siblings, parent }: StepPlace): string | undefined { - if (next === 'end') { - return 'ends the run'; - } - if (next === 'exit') { - return parent === undefined ? 'ends the run' : `leaves step ${parent.step}`; - } - const target = siblings.find(({ name }) => name === next); - return target === undefined ? undefined : `goes to step ${target.step}`; -} - -export function forWords({ task, step, last }: PlacedTask): Phrases { - const loop = objectAt(task, 'for'); - const condition = field(task, 'while'); - return [ - `does ${stepsWords(step + 1, last)} for each `, - named(textField(loop, 'each') ?? 'item'), - ' of ', - { code: expressionText(field(loop, 'in') ?? null) }, - ...(condition === undefined ? [] : [', while ', { code: expressionText(condition) }, ' holds']), - ]; -} - -export function doWords({ step, last }: PlacedTask): Phrases { - return [`does ${stepsWords(step + 1, last)} in order`]; -} - -export function forkWords({ task, body }: PlacedTask): Phrases { - if (body.length === 0) { - return ['does nothing at once']; - } - const branches = listed(body.map(({ step }) => String(step))); - const handed = - field(objectAt(task, 'fork'), 'compete') === true - ? 'the output of the first to finish, cancelling the others' - : 'a list of their outputs'; - return [`does ${body.length === 1 ? 'step' : 'steps'} ${branches} at once, and hands on ${handed}`]; -} - -function caughtWords(caught: JsonObject): Phrases { - const tests = entriesOf(objectAt(caught, 'errors', 'with')).map(([key, value]): Phrases => - key === 'status' ? [`with status ${expressionText(value)}`] : [`whose ${key} is `, { code: expressionText(value) }], - ); - const when = field(caught, 'when'); - const exceptWhen = field(caught, 'exceptWhen'); - return [ - ...(tests.length === 0 ? ['any error'] : ['an error ', ...listedPhrases(tests, 'and')]), - ...(when === undefined ? [] : [', when ', { code: expressionText(when) }]), - ...(exceptWhen === undefined ? [] : [', except when ', { code: expressionText(exceptWhen) }]), - ]; -} - -function retryWords(caught: JsonObject): Phrases { - const retry = field(caught, 'retry'); - if (retry === undefined) { - return []; - } - const policy: Phrases = typeof retry === 'string' ? ['under the retry policy ', named(retry)] : ['as its catch says']; - return ['tries again ', ...policy, ', and once no retry is left, it ']; -} - -export function tryWords({ task, step, bodyLast, last, recovery }: PlacedTask): Phrases { - const caught = objectAt(task, 'catch'); - const recovered = recovery.length === 0 ? 'hands on its input' : `does ${stepsWords(bodyLast + 1, last)}`; - return [ - `tries ${stepsWords(step + 1, bodyLast)}; on `, - ...caughtWords(caught), - ', it ', - ...retryWords(caught), - recovered, - ]; -} - -function caseWords({ name, when, next }: SwitchCase, place: StepPlace): Phrases { - const chosen: Phrases = when === undefined ? [', by default,'] : [', when ', { code: when }, ',']; - return [named(name), ...chosen, ` ${movedWords(next, place) ?? 'goes on'}`]; -} - -export function switchWords({ task }: PlacedTask, place: StepPlace): Phrases { - const cases = casesOf(task).map((each) => caseWords(each, place)); - const unmatched: readonly Phrases[] = casesOf(task).every(({ when }) => when !== undefined) - ? [[`when no case holds, it ${movedWords(textField(task, 'then'), place) ?? 'goes on'}`]] - : []; - return ['chooses the first case that holds: ', ...listedPhrases([...cases, ...unmatched], 'and')]; -} diff --git a/capabilities/coordination/src/steps/grouped-tasks.test.ts b/capabilities/coordination/src/steps/grouped-tasks.test.ts deleted file mode 100644 index 1cc8bb3fb..000000000 --- a/capabilities/coordination/src/steps/grouped-tasks.test.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { doneIn, stepNumbersOf, type DoneSteps } from '../testing/done-steps.ts'; - -interface Example extends DoneSteps { - readonly tasks: readonly string[]; -} - -function drawnFrom({ tasks }: Example): DoneSteps { - return doneIn(tasks.join('\n')); -} - -function expectedOf({ diagram, steps }: Example): DoneSteps { - return { diagram, steps }; -} - -const choice: Example = { - tasks: [ - ' - route:', - ' switch:', - " - large: { when: '${ $data.n > 5 }', then: big }", - ' - fallback: { then: small }', - ' - big: { set: { size: large } }', - ' - small: { set: { size: small } }', - ], - diagram: [ - ' n1(["By a call"])', - ' n2{"1. Choose a case"}', - ' n3["2. Set size"]', - ' n4["3. Set size"]', - ' n1 --> n2', - ' n2 -->|"large"| n3', - ' n2 -->|"fallback"| n4', - ' n3 --> n4', - ], - steps: [ - '1. **route** chooses the first case that holds: **large**, when `$data.n > 5`, goes to step 2 and **fallback**, by default, goes to step 3.', - '2. **big** sets **size**.', - '3. **small** sets **size**.', - ], -}; - -const loop: Example = { - tasks: [ - ' - total:', - " for: { each: line, in: '${ $data.lines }' }", - " while: '${ ($data.sum ?? 0) < 1000 }'", - ' do:', - " - add: { set: { sum: '${ ($data.sum ?? 0) + $line.amount }' } }", - ' - report: { set: { reported: true } }', - ], - diagram: [ - ' subgraph g1["1. For each line of $data.lines"]', - ' n2["2. Set sum"]', - ' end', - ' n1(["By a call"])', - ' n3["3. Set reported"]', - ' n1 --> n2', - ' n2 --> n3', - ' n2 -->|"next item"| n2', - ], - steps: [ - '1. **total** does step 2 for each **line** of `$data.lines`, while `($data.sum ?? 0) < 1000` holds.', - '2. **add** sets **sum**.', - '3. **report** sets **reported**.', - ], -}; - -const branches: Example = { - tasks: [ - ' - reviews:', - ' fork:', - ' branches:', - ' - first: { call: run_definition, with: { type: reasoning, name: review-brief } }', - ' - second: { call: run_definition, with: { type: computation, name: campaign-pace } }', - ' - done: { set: { reviewed: true } }', - ], - diagram: [ - ' subgraph g1["1. At once"]', - ' n2["2. Run review-brief"]', - ' n3["3. Run campaign-pace"]', - ' end', - ' n1(["By a call"])', - ' n4["4. Set reviewed"]', - ' n1 --> n2', - ' n1 --> n3', - ' n2 --> n4', - ' n3 --> n4', - ], - steps: [ - '1. **reviews** does steps 2 and 3 at once, and hands on a list of their outputs.', - '2. **first** runs the reasoning function **review-brief**.', - '3. **second** runs the computation function **campaign-pace**.', - '4. **done** sets **reviewed**.', - ], -}; - -const race: Example = { - tasks: [ - ' - race:', - ' fork:', - ' compete: true', - ' branches:', - ' - quick: { wait: PT1M }', - ' - slow: { wait: { hours: 2 } }', - ], - diagram: [ - ' subgraph g1["1. At once, the first to finish wins"]', - ' n2["2. Wait 1 minute"]', - ' n3["3. Wait 2 hours"]', - ' end', - ' n1(["By a call"])', - ' n1 --> n2', - ' n1 --> n3', - ], - steps: [ - '1. **race** does steps 2 and 3 at once, and hands on the output of the first to finish, cancelling the others.', - '2. **quick** waits 1 minute.', - '3. **slow** waits 2 hours.', - ], -}; - -describe('the diagram of a switch', () => { - it('is a decision with a branch for each case, to the step each case names', () => { - const done = drawnFrom(choice); - - expect(done).toEqual(expectedOf(choice)); - expect(stepNumbersOf(done).drawn).toEqual(stepNumbersOf(done).listed); - }); -}); - -describe('the diagram of a for', () => { - it('is a group around its tasks, the last of which goes back for the next item', () => { - const done = drawnFrom(loop); - - expect(done).toEqual(expectedOf(loop)); - expect(stepNumbersOf(done).drawn).toEqual(stepNumbersOf(done).listed); - }); -}); - -describe('the diagram of a fork', () => { - it('is a group with a branch for each of its branches, which join at the step after it', () => { - const done = drawnFrom(branches); - - expect(done).toEqual(expectedOf(branches)); - expect(stepNumbersOf(done).drawn).toEqual(stepNumbersOf(done).listed); - }); - - it('notes in its group when the first branch to finish wins', () => { - const done = drawnFrom(race); - - expect(done).toEqual(expectedOf(race)); - expect(stepNumbersOf(done).drawn).toEqual(stepNumbersOf(done).listed); - }); -}); diff --git a/capabilities/coordination/src/steps/incomplete-tasks.test.ts b/capabilities/coordination/src/steps/incomplete-tasks.test.ts deleted file mode 100644 index c922e6543..000000000 --- a/capabilities/coordination/src/steps/incomplete-tasks.test.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { doneIn, stepNumbersOf } from '../testing/done-steps.ts'; - -const tasks = [ - ' - odd: { mystery: 1 }', - ' - loop: { for: {} }', - ' - empty: { fork: { branches: [] } }', - ' - bare: { fork: [] }', - ' - deaf: { listen: {} }', - ' - silent: { raise: {} }', - ' - unnamed: { raise: { error: unavailable } }', - ' - untitled: { raise: { error: { type: https://example.com/errors/x } } }', - ' - quiet: { raise: { error: { status: 500 } } }', - ' - blank: { emit: { event: { with: {} } } }', - " - computed: { set: '${ $data }' }", - ' - nothing: { set: [] }', - ' - fetch: { call: http, with: {} }', - ' - process: { run: {} }', - ' - unguarded: { try: [] }', - ' - none: { do: [] }', - ' - single: { fork: { branches: [{ only: { set: { o: 1 } } }] } }', - ' - bad: { switch: {} }', - ' - mixed: { switch: [1, { fallback: null }] }', - " - maybe: { switch: [{ perhaps: { when: '${ $data.maybe }' } }] }", - ' - idle: { do: [], then: idle }', -]; - -const groups = [ - ' subgraph g1["2. For each item of null"]', - ' end', - ' subgraph g2["3. At once"]', - ' end', - ' subgraph g3["4. At once"]', - ' end', - ' subgraph g4["15. Try"]', - ' end', - ' subgraph g5["16. In order"]', - ' end', - ' subgraph g6["17. At once"]', - ' n13["18. Set o"]', - ' end', - ' subgraph g7["22. In order"]', - ' end', -]; - -const nodes = [ - ' n1(["By a call"])', - ' n2["1. odd"]', - ' n3["5. Wait for an event sent to the run"]', - ' n4(["6. Raise an error"])', - ' n5(["7. Raise unavailable"])', - ' n6(["8. Raise an error"])', - ' n7(["9. Raise an error"])', - ' n8["10. Publish an event"]', - ' n9["11. Set its data"]', - ' n10["12. Set its data"]', - ' n11["13. Call http"]', - ' n12["14. Run a process"]', - ' n14{"19. Choose a case"}', - ' n15{"20. Choose a case"}', - ' n16{"21. Choose a case"}', -]; - -const edges = [ - ' n1 --> n2', - ' n2 --> n3', - ' n3 --> n4', - ' n8 --> n9', - ' n9 --> n10', - ' n10 --> n11', - ' n11 --> n12', - ' n12 --> n13', - ' n13 --> n14', - ' n14 -->|"no case holds"| n15', - ' n15 -->|"fallback"| n16', -]; - -const steps = [ - '1. **odd** is a step this runtime does not run.', - '2. **loop** does nothing for each **item** of `null`.', - '3. **empty** does nothing at once.', - '4. **bare** does nothing at once.', - '5. **deaf** waits for an event sent to the run.', - '6. **silent** raises an error.', - '7. **unnamed** raises the error **unavailable** its document names under `use.errors`.', - '8. **untitled** raises an error of the type `https://example.com/errors/x`.', - '9. **quiet** raises an error.', - '10. **blank** publishes an event of a type computed when it runs to the playbook.', - '11. **computed** sets its data from `$data`.', - '12. **nothing** sets its data.', - '13. **fetch** calls `http`.', - '14. **process** runs a process.', - '15. **unguarded** tries nothing; on any error, it hands on its input.', - '16. **none** does nothing in order.', - '17. **single** does step 18 at once, and hands on a list of their outputs.', - '18. **only** sets **o**.', - '19. **bad** chooses the first case that holds: when no case holds, it goes on.', - '20. **mixed** chooses the first case that holds: **fallback**, by default, goes on.', - '21. **maybe** chooses the first case that holds: **perhaps**, when `$data.maybe`, goes on and when no case holds, it goes on.', - '22. **idle** does nothing in order, and then goes to step 22.', -]; - -describe('the diagram of steps its document leaves incomplete', () => { - it('still draws each step, says what it can of it, and draws no edge around a step that comes back to itself', () => { - const done = doneIn(tasks.join('\n')); - const { drawn, listed } = stepNumbersOf(done); - - expect(done).toEqual({ diagram: [...groups, ...nodes, ...edges], steps }); - expect(drawn).toEqual(listed); - }); - - it('has no steps when its list holds none', () => { - expect(doneIn(' []')).toEqual({ diagram: [' n1(["By a call"])'], steps: [''] }); - }); -}); diff --git a/capabilities/coordination/src/steps/leaf-tasks.test.ts b/capabilities/coordination/src/steps/leaf-tasks.test.ts deleted file mode 100644 index 2a0be8132..000000000 --- a/capabilities/coordination/src/steps/leaf-tasks.test.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { doneIn, stepNumbersOf, type DoneSteps } from '../testing/done-steps.ts'; - -interface Example extends DoneSteps { - readonly tasks: readonly string[]; - readonly entries?: readonly string[]; -} - -function drawnFrom({ tasks, entries }: Example): DoneSteps { - return doneIn(tasks.join('\n'), entries); -} - -function expectedOf({ diagram, steps }: Example): DoneSteps { - return { diagram, steps }; -} - -const called: Example = { - tasks: [ - ' - review:', - ' call: run_definition', - " with: { type: reasoning, name: review-brief, input: { brief: '${ $data.brief }' } }", - ], - entries: ['On com.example.brief.submitted', 'By a call'], - diagram: [ - ' n1(["On com.example.brief.submitted"])', - ' n2(["By a call"])', - ' n3["1. Run review-brief"]', - ' n1 --> n3', - ' n2 --> n3', - ], - steps: ['1. **review** runs the reasoning function **review-brief**.'], -}; - -const assigned: Example = { - tasks: [" - tally: { set: { count: 1, total: '${ $data.total }' } }"], - diagram: [' n1(["By a call"])', ' n2["1. Set count and total"]', ' n1 --> n2'], - steps: ['1. **tally** sets **count** and **total**.'], -}; - -const raised: Example = { - tasks: [ - ' - stop:', - ' raise:', - ' error: { type: https://example.com/errors/stopped, status: 409, title: Stopped }', - ' - unreached: { set: { reached: true } }', - ], - diagram: [' n1(["By a call"])', ' n2(["1. Raise “Stopped”"])', ' n3["2. Set reached"]', ' n1 --> n2'], - steps: ['1. **stop** raises the error *Stopped*, with status 409.', '2. **unreached** sets **reached**.'], -}; - -const waited: Example = { - tasks: [ - ' - hear:', - ' listen:', - ' to:', - ' any:', - ' - with: { type: com.example.a }', - ` - with: { type: '\${ "com.example." + $data.kind }' }`, - ' timeout: { after: PT1H }', - ' - pause: { wait: { hours: 2, minutes: 30 } }', - ' - announce:', - ' emit:', - ' event:', - ' with: { type: com.example.brief.approved, source: https://example.com/campaigns }', - ], - diagram: [ - ' n1(["By a call"])', - ' n2["1. Wait for com.example.a or an event sent to the run"]', - ' n3["2. Wait 2 hours and 30 minutes"]', - ' n4["3. Publish com.example.brief.approved"]', - ' n1 --> n2', - ' n2 --> n3', - ' n3 --> n4', - ], - steps: [ - '1. **hear** waits for the first of the event **com.example.a** or an event sent to the run, for at most 1 hour.', - '2. **pause** waits 2 hours and 30 minutes.', - '3. **announce** publishes the event **com.example.brief.approved** to the playbook.', - ], -}; - -const heard: Example = { - tasks: [ - ' - one: { listen: { to: { one: { with: { type: com.example.a } } } } }', - ' - all: { listen: { to: { all: [{ with: { type: com.example.a } }, { with: { type: com.example.b } }] } } }', - ], - diagram: [ - ' n1(["By a call"])', - ' n2["1. Wait for com.example.a"]', - ' n3["2. Wait for com.example.a and com.example.b"]', - ' n1 --> n2', - ' n2 --> n3', - ], - steps: [ - '1. **one** waits for the event **com.example.a**.', - '2. **all** waits for each of the event **com.example.a** and the event **com.example.b**.', - ], -}; - -const ordered: Example = { - tasks: [ - ' - steps:', - ' do:', - ' - first: { set: { a: 1 } }', - ' - second: { set: { b: 2 } }', - " - build: { run: { script: { language: js, code: '1' } } }", - ], - diagram: [ - ' subgraph g1["1. In order"]', - ' n2["2. Set a"]', - ' n3["3. Set b"]', - ' end', - ' n1(["By a call"])', - ' n4["4. Run a script"]', - ' n1 --> n2', - ' n2 --> n3', - ' n3 --> n4', - ], - steps: [ - '1. **steps** does steps 2 to 3 in order.', - '2. **first** sets **a**.', - '3. **second** sets **b**.', - '4. **build** runs a script.', - ], -}; - -describe('the diagram of a call of run_definition', () => { - it('is a node named for the definition it runs, entered from each way the workflow starts', () => { - const done = drawnFrom(called); - - expect(done).toEqual(expectedOf(called)); - expect(stepNumbersOf(done).drawn).toEqual(stepNumbersOf(done).listed); - }); -}); - -describe('the diagram of a set', () => { - it('is a node that names what it sets', () => { - const done = drawnFrom(assigned); - - expect(done).toEqual(expectedOf(assigned)); - expect(stepNumbersOf(done).drawn).toEqual(stepNumbersOf(done).listed); - }); -}); - -describe('the diagram of a raise', () => { - it('is an end node, with no step after it', () => { - const done = drawnFrom(raised); - - expect(done).toEqual(expectedOf(raised)); - expect(stepNumbersOf(done).drawn).toEqual(stepNumbersOf(done).listed); - }); -}); - -describe('the diagram of a listen, a wait and an emit', () => { - it('are nodes that wait for their events, wait their time and publish their event', () => { - const done = drawnFrom(waited); - - expect(done).toEqual(expectedOf(waited)); - expect(stepNumbersOf(done).drawn).toEqual(stepNumbersOf(done).listed); - }); - - it('say which events a listen waits for, one or each of them', () => { - const done = drawnFrom(heard); - - expect(done).toEqual(expectedOf(heard)); - expect(stepNumbersOf(done).drawn).toEqual(stepNumbersOf(done).listed); - }); -}); - -describe('the diagram of a do and of a run', () => { - it('are a group of the tasks in order, and a node that runs its process', () => { - const done = drawnFrom(ordered); - - expect(done).toEqual(expectedOf(ordered)); - expect(stepNumbersOf(done).drawn).toEqual(stepNumbersOf(done).listed); - }); -}); diff --git a/capabilities/coordination/src/steps/placed-tasks.ts b/capabilities/coordination/src/steps/placed-tasks.ts deleted file mode 100644 index e41f13a9f..000000000 --- a/capabilities/coordination/src/steps/placed-tasks.ts +++ /dev/null @@ -1,123 +0,0 @@ -import type { DiagramGroup, DiagramNode, NodeShape } from '@on.auto/definitions'; -import { - field, - kindOf, - taskEntries, - type Json, - type JsonObject, - type TaskEntry, - type TaskKind, -} from '@on.auto/workflow-engine'; - -import { objectAt } from './task-reading.ts'; - -export type LabelOf = (kind: TaskKind | undefined, task: JsonObject, name: string) => string; - -export interface NumberedTask { - readonly name: string; - readonly task: JsonObject; - readonly kind: TaskKind | undefined; - readonly step: number; - readonly body: readonly NumberedTask[]; - readonly recovery: readonly NumberedTask[]; - readonly bodyLast: number; - readonly last: number; -} - -export interface PlacedTask extends NumberedTask { - readonly node?: number; - readonly group?: number; - readonly body: readonly PlacedTask[]; - readonly recovery: readonly PlacedTask[]; -} - -export interface Placement { - readonly tasks: readonly PlacedTask[]; - readonly nodes: readonly DiagramNode[]; - readonly groups: readonly DiagramGroup[]; -} - -interface GroupDraft { - readonly label: string; - readonly within: number | undefined; -} - -type GroupedKind = 'for' | 'do' | 'try' | 'fork'; - -const groupedKinds: ReadonlySet = new Set(['for', 'do', 'try', 'fork']); - -const shapeOfKind: Partial>> = { switch: 'decision', raise: 'terminal' }; - -function bodyOf(kind: TaskKind | undefined, task: JsonObject): Json | undefined { - if (kind === 'fork') { - return field(objectAt(task, 'fork'), 'branches'); - } - return field(task, kind === 'try' ? 'try' : 'do'); -} - -function recoveryOf(kind: TaskKind | undefined, task: JsonObject): Json | undefined { - return kind === 'try' ? field(objectAt(task, 'catch'), 'do') : undefined; -} - -function nodeOf(label: string, kind: TaskKind | undefined): DiagramNode { - const shape = kind === undefined ? undefined : shapeOfKind[kind]; - return shape === undefined ? { label } : { label, shape }; -} - -function groupOf({ label, within }: GroupDraft, nodes: readonly number[]): DiagramGroup { - return within === undefined ? { label, nodes } : { label, nodes, within }; -} - -export function numberedTasksOf(list: Json | undefined): readonly NumberedTask[] { - let steps = 0; - - function numberedList(tasks: Json | undefined): readonly NumberedTask[] { - return taskEntries(tasks, '').map((entry) => numberedTask(entry)); - } - - function numberedTask({ name, task }: TaskEntry): NumberedTask { - steps += 1; - const step = steps; - const kind = kindOf(task); - if (!groupedKinds.has(kind)) { - return { name, task, kind, step, body: [], recovery: [], bodyLast: step, last: step }; - } - const body = numberedList(bodyOf(kind, task)); - const bodyLast = steps; - const recovery = numberedList(recoveryOf(kind, task)); - return { name, task, kind, step, body, recovery, bodyLast, last: steps }; - } - - return numberedList(list); -} - -export function placedTasksOf(list: Json | undefined, entries: number, labelOf: LabelOf): Placement { - const nodes: DiagramNode[] = []; - const nodeGroups: (number | undefined)[] = []; - const drafts: GroupDraft[] = []; - - function placedTask(numbered: NumberedTask, within?: number): PlacedTask { - const { name, task, kind, step } = numbered; - const label = `${step}. ${labelOf(kind, task, name)}`; - if (!groupedKinds.has(kind)) { - const node = entries + nodes.length; - nodes.push(nodeOf(label, kind)); - nodeGroups.push(within); - return { ...numbered, node, body: [], recovery: [] }; - } - const group = drafts.length; - drafts.push({ label, within }); - const body = numbered.body.map((inner) => placedTask(inner, group)); - const recovery = numbered.recovery.map((inner) => placedTask(inner, within)); - return { ...numbered, group, body, recovery }; - } - - const tasks = numberedTasksOf(list).map((numbered) => placedTask(numbered)); - const groups = drafts.map((draft, group) => - groupOf( - draft, - nodeGroups.flatMap((within, index) => (within === group ? [entries + index] : [])), - ), - ); - return { tasks, nodes, groups }; -} diff --git a/capabilities/coordination/src/steps/task-diagram.ts b/capabilities/coordination/src/steps/task-diagram.ts deleted file mode 100644 index 1a7f38db5..000000000 --- a/capabilities/coordination/src/steps/task-diagram.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { named, type Diagram, type Phrases } from '@on.auto/definitions'; -import { field, textField, type Json, type JsonObject, type TaskKind } from '@on.auto/workflow-engine'; - -import { doWords, forkWords, forWords, movedWords, switchWords, tryWords, type StepPlace } from './flow-words.ts'; -import { placedTasksOf, type PlacedTask } from './placed-tasks.ts'; -import { edgesOf } from './task-edges.ts'; -import { conditionOf } from './task-flow.ts'; -import { - callLabel, - emitLabel, - forkLabel, - forLabel, - listenLabel, - raiseLabel, - runLabel, - setLabel, - waitLabel, -} from './task-labels.ts'; -import { durationText, objectAt } from './task-reading.ts'; -import { callWords, emitWords, listenWords, raiseWords, runWords, setWords, waitWords } from './task-words.ts'; - -export interface TaskDiagram { - readonly diagram: Diagram; - readonly steps: readonly Phrases[]; -} - -interface KindWords { - readonly label: (task: JsonObject) => string; - readonly words: (task: PlacedTask, place: StepPlace) => Phrases; -} - -const wordsOfKind: Readonly> = { - for: { label: forLabel, words: forWords }, - try: { label: () => 'Try', words: tryWords }, - call: { label: callLabel, words: ({ task }) => callWords(task) }, - fork: { label: forkLabel, words: forkWords }, - emit: { label: emitLabel, words: ({ task }) => emitWords(task) }, - listen: { label: listenLabel, words: ({ task }) => listenWords(task) }, - raise: { label: raiseLabel, words: ({ task }) => raiseWords(task) }, - run: { label: runLabel, words: ({ task }) => runWords(task) }, - set: { label: setLabel, words: ({ task }) => setWords(task) }, - switch: { label: () => 'Choose a case', words: switchWords }, - wait: { label: waitLabel, words: ({ task }) => waitWords(task) }, - do: { label: () => 'In order', words: doWords }, -}; - -const unmoved: ReadonlySet = new Set(['switch', 'raise']); - -function labelOf(kind: TaskKind | undefined, task: JsonObject, name: string): string { - return kind === undefined ? name : wordsOfKind[kind].label(task); -} - -function timeoutWords(task: JsonObject): Phrases { - const timeout = field(task, 'timeout'); - if (timeout === undefined) { - return []; - } - if (typeof timeout === 'string') { - return [', within the timeout ', named(timeout)]; - } - const duration = durationText(field(objectAt(task, 'timeout'), 'after')); - return [duration === undefined ? ', within a time computed when it runs' : `, for at most ${duration}`]; -} - -function suffixWords(placed: PlacedTask, place: StepPlace): Phrases { - const condition = conditionOf(placed.task); - const moved = unmoved.has(placed.kind) ? undefined : movedWords(textField(placed.task, 'then'), place); - return [ - ...(condition === undefined ? [] : [', only when ', { code: condition }, ' holds']), - ...timeoutWords(placed.task), - ...(moved === undefined ? [] : [`, and then ${moved}`]), - ]; -} - -function itemOf(placed: PlacedTask, place: StepPlace): Phrases { - const words = - placed.kind === undefined ? ['is a step this runtime does not run'] : wordsOfKind[placed.kind].words(placed, place); - return [named(placed.name), ' ', ...words, ...suffixWords(placed, place), '.']; -} - -function stepItems(tasks: readonly PlacedTask[], parent?: PlacedTask): readonly Phrases[] { - return tasks.flatMap((task) => [ - itemOf(task, parent === undefined ? { siblings: tasks } : { siblings: tasks, parent }), - ...stepItems(task.body, task), - ...stepItems(task.recovery, task), - ]); -} - -export function taskDiagramOf(list: Json | undefined, entries: readonly string[]): TaskDiagram { - const { tasks, nodes, groups } = placedTasksOf(list, entries.length, labelOf); - const starts = entries.map((label) => ({ label, shape: 'terminal' as const })); - const edges = edgesOf( - tasks, - entries.map((_, from) => ({ from })), - ); - return { diagram: { kind: 'diagram', nodes: [...starts, ...nodes], edges, groups }, steps: stepItems(tasks) }; -} diff --git a/capabilities/coordination/src/steps/task-edges.ts b/capabilities/coordination/src/steps/task-edges.ts deleted file mode 100644 index db6bfe1a5..000000000 --- a/capabilities/coordination/src/steps/task-edges.ts +++ /dev/null @@ -1,174 +0,0 @@ -import type { DiagramEdge } from '@on.auto/definitions'; -import { textField } from '@on.auto/workflow-engine'; - -import type { PlacedTask } from './placed-tasks.ts'; -import { casesOf, catchLabelOf, conditionOf } from './task-flow.ts'; - -export interface Exit { - readonly from: number; - readonly label?: string; - readonly dashed?: boolean; -} - -type Visited = ReadonlySet; - -type Onward = (exits: readonly Exit[], visited: Visited) => void; - -interface Place { - readonly tasks: readonly PlacedTask[]; - readonly onward: Onward; - readonly leave: Onward; - readonly draw: (edge: DiagramEdge) => void; -} - -interface Spot { - readonly place: Place; - readonly position: number; - readonly task: PlacedTask; -} - -const unvisited: Visited = new Set(); - -function runEnds(): void {} - -function labelled(exits: readonly Exit[], label: string): readonly Exit[] { - return exits.map((exit) => ({ ...exit, label: exit.label === undefined ? label : `${exit.label}, ${label}` })); -} - -function nodesWithin(tasks: readonly PlacedTask[]): readonly number[] { - return tasks.flatMap(({ node, body, recovery }) => [ - ...(node === undefined ? [] : [node]), - ...nodesWithin(body), - ...nodesWithin(recovery), - ]); -} - -function drawn({ from, label, dashed = false }: Exit, to: number): DiagramEdge { - return { from, to, ...(label === undefined ? {} : { label }), ...(dashed ? { dashed } : {}) }; -} - -function routed(next: string | undefined, spot: Spot, exits: readonly Exit[], visited: Visited): void { - const { place, position } = spot; - if (next === 'exit') { - place.leave(exits, visited); - } else if (next !== 'end') { - const target = place.tasks.findIndex(({ name }) => name === next); - arrive(place, target === -1 ? position + 1 : target, exits, visited); - } -} - -function completed(spot: Spot, exits: readonly Exit[], visited: Visited): void { - routed(textField(spot.task.task, 'then'), spot, exits, visited); -} - -function finishing(spot: Spot): Onward { - return (exits, visited) => { - completed(spot, exits, visited); - }; -} - -function bodyPlace(spot: Spot): Place { - const done = finishing(spot); - const looped: Onward = (exits, visited) => { - done(exits, visited); - if (!visited.has(spot.task)) { - arrive(bodyPlace(spot), 0, labelled(exits, 'next item'), new Set([...visited, spot.task])); - } - }; - const { draw } = spot.place; - return { tasks: spot.task.body, onward: spot.task.kind === 'for' ? looped : done, leave: done, draw }; -} - -function branchPlaces(spot: Spot): readonly Place[] { - const done = finishing(spot); - const { draw } = spot.place; - return spot.task.body.map((branch) => ({ tasks: [branch], onward: done, leave: done, draw })); -} - -function innerPlaces(spot: Spot): readonly Place[] { - return spot.task.kind === 'fork' ? branchPlaces(spot) : [bodyPlace(spot)]; -} - -function entered(spot: Spot, exits: readonly Exit[], visited: Visited): void { - const { node, kind, body } = spot.task; - if (node !== undefined) { - for (const exit of exits) { - spot.place.draw(drawn(exit, node)); - } - } else if (kind === 'fork' && body.length === 0) { - completed(spot, exits, visited); - } else { - for (const place of innerPlaces(spot)) { - arrive(place, 0, exits, visited); - } - } -} - -function arrive(place: Place, position: number, exits: readonly Exit[], visited: Visited): void { - const task = place.tasks[position]; - if (task === undefined) { - place.onward(exits, visited); - return; - } - if (visited.has(task)) { - return; - } - const condition = conditionOf(task.task); - const entering = condition === undefined ? exits : labelled(exits, `if ${condition}`); - entered({ place, position, task }, entering, new Set([...visited, task])); - if (condition !== undefined) { - arrive(place, position + 1, labelled(exits, 'otherwise'), visited); - } -} - -function wiredSwitch(spot: Spot, from: number): void { - const cases = casesOf(spot.task.task); - for (const { name, next } of cases) { - routed(next, spot, [{ from, label: name }], unvisited); - } - if (cases.every(({ when }) => when !== undefined)) { - completed(spot, [{ from, label: 'no case holds' }], unvisited); - } -} - -function wiredTry(spot: Spot): void { - const label = catchLabelOf(spot.task.task); - const failing = nodesWithin(spot.task.body).map((from) => ({ from, label, dashed: true })); - const { draw } = spot.place; - const recovery: Place = { tasks: spot.task.recovery, onward: finishing(spot), leave: finishing(spot), draw }; - arrive(recovery, 0, failing, unvisited); - wired(recovery); -} - -function wiredTask(spot: Spot): void { - const { node, kind } = spot.task; - if (node === undefined) { - for (const place of innerPlaces(spot)) { - wired(place); - } - if (kind === 'try') { - wiredTry(spot); - } - } else if (kind === 'switch') { - wiredSwitch(spot, node); - } else if (kind !== 'raise') { - completed(spot, [{ from: node }], unvisited); - } -} - -function wired(place: Place): void { - for (const [position, task] of place.tasks.entries()) { - wiredTask({ place, position, task }); - } -} - -export function edgesOf(tasks: readonly PlacedTask[], entries: readonly Exit[]): readonly DiagramEdge[] { - const edges = new Map(); - const draw = (edge: DiagramEdge) => { - edges.set(JSON.stringify(edge), edge); - }; - const top: Place = { tasks, onward: runEnds, leave: runEnds, draw }; - arrive(top, 0, entries, unvisited); - wired(top); - return [...edges.values()]; -} diff --git a/capabilities/coordination/src/steps/task-flow.test.ts b/capabilities/coordination/src/steps/task-flow.test.ts deleted file mode 100644 index 6cc14ad63..000000000 --- a/capabilities/coordination/src/steps/task-flow.test.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { doneIn, stepNumbersOf, type DoneSteps } from '../testing/done-steps.ts'; - -interface Example extends DoneSteps { - readonly tasks: readonly string[]; -} - -function drawnFrom({ tasks }: Example): DoneSteps { - return doneIn(tasks.join('\n')); -} - -function expectedOf({ diagram, steps }: Example): DoneSteps { - return { diagram, steps }; -} - -const jumps: Example = { - tasks: [ - " - start: { set: { n: 1 }, if: '${ $data.go }' }", - ' - loop:', - ' do:', - ' - check: { set: { n: 2 }, then: exit }', - ' - never: { set: { n: 3 } }', - ' - again: { set: { n: 4 }, then: start }', - ], - diagram: [ - ' subgraph g1["2. In order"]', - ' n3["3. Set n"]', - ' n4["4. Set n"]', - ' end', - ' n1(["By a call"])', - ' n2["1. Set n"]', - ' n5["5. Set n"]', - ' n1 -->|"if $data.go"| n2', - ' n1 -->|"otherwise"| n3', - ' n2 --> n3', - ' n3 --> n5', - ' n4 --> n5', - ' n5 -->|"if $data.go"| n2', - ' n5 -->|"otherwise"| n3', - ], - steps: [ - '1. **start** sets **n**, only when `$data.go` holds.', - '2. **loop** does steps 3 to 4 in order.', - '3. **check** sets **n**, and then leaves step 2.', - '4. **never** sets **n**.', - '5. **again** sets **n**, and then goes to step 1.', - ], -}; - -const endings: Example = { - tasks: [ - ' - route:', - ' switch:', - " - stop: { when: '${ $data.stop }', then: end }", - " - leave: { when: '${ $data.leave }', then: exit }", - " - onward: { when: '${ $data.onward }', then: continue }", - ' then: last', - ' - skipped: { set: { skipped: true }, then: end }', - ' - last: { set: { last: true }, then: exit }', - ], - diagram: [ - ' n1(["By a call"])', - ' n2{"1. Choose a case"}', - ' n3["2. Set skipped"]', - ' n4["3. Set last"]', - ' n1 --> n2', - ' n2 -->|"onward"| n3', - ' n2 -->|"no case holds"| n4', - ], - steps: [ - '1. **route** chooses the first case that holds: **stop**, when `$data.stop`, ends the run, **leave**, when `$data.leave`, ends the run, **onward**, when `$data.onward`, goes on, and when no case holds, it goes to step 3.', - '2. **skipped** sets **skipped**, and then ends the run.', - '3. **last** sets **last**, and then ends the run.', - ], -}; - -const conditions: Example = { - tasks: [ - ' - steps:', - ' do:', - ' - route:', - ' switch:', - " - out: { when: '${ $data.out }', then: exit }", - ' - other: {}', - " - guarded: { set: { g: 1 }, if: '${ $data.g }' }", - ' - after: { set: { a: 1 } }', - ' - named: { wait: PT1M, timeout: quick }', - " - computed: { wait: '${ $data.wait }', timeout: { after: '${ $data.most }' } }", - ' - held: { wait: PT1S, timeout: {} }', - ], - diagram: [ - ' subgraph g1["1. In order"]', - ' n2{"2. Choose a case"}', - ' n3["3. Set g"]', - ' end', - ' n1(["By a call"])', - ' n4["4. Set a"]', - ' n5["5. Wait 1 minute"]', - ' n6["6. Wait"]', - ' n7["7. Wait 1 second"]', - ' n1 --> n2', - ' n2 -->|"out"| n4', - ' n2 -->|"other, if $data.g"| n3', - ' n2 -->|"other, otherwise"| n4', - ' n3 --> n4', - ' n4 --> n5', - ' n5 --> n6', - ' n6 --> n7', - ], - steps: [ - '1. **steps** does steps 2 to 3 in order.', - '2. **route** chooses the first case that holds: **out**, when `$data.out`, leaves step 1 and **other**, by default, goes on.', - '3. **guarded** sets **g**, only when `$data.g` holds.', - '4. **after** sets **a**.', - '5. **named** waits 1 minute, within the timeout **quick**.', - '6. **computed** waits for a time computed when it runs, within a time computed when it runs.', - '7. **held** waits 1 second, within a time computed when it runs.', - ], -}; - -describe('the flow between the steps of a diagram', () => { - it('follows then to a named step, out of a list with exit, and around a step whose if does not hold', () => { - const done = drawnFrom(jumps); - - expect(done).toEqual(expectedOf(jumps)); - expect(stepNumbersOf(done).drawn).toEqual(stepNumbersOf(done).listed); - }); - - it('ends the run at end and at an exit of the workflow, and follows a switch of its own when no case holds', () => { - const done = drawnFrom(endings); - - expect(done).toEqual(expectedOf(endings)); - expect(stepNumbersOf(done).drawn).toEqual(stepNumbersOf(done).listed); - }); - - it('labels a case that reaches a step whose if may not hold, and says the timeout of a step', () => { - const done = drawnFrom(conditions); - - expect(done).toEqual(expectedOf(conditions)); - expect(stepNumbersOf(done).drawn).toEqual(stepNumbersOf(done).listed); - }); -}); diff --git a/capabilities/coordination/src/steps/task-flow.ts b/capabilities/coordination/src/steps/task-flow.ts deleted file mode 100644 index 28758e630..000000000 --- a/capabilities/coordination/src/steps/task-flow.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { entriesOf, field, isList, isObject, textField, type Json, type JsonObject } from '@on.auto/workflow-engine'; - -import { expressionText, objectAt } from './task-reading.ts'; - -export interface SwitchCase { - readonly name: string; - readonly when?: string; - readonly next?: string; -} - -function caseOf(name: string, value: Json): SwitchCase { - const written = isObject(value) ? value : {}; - const when = field(written, 'when'); - const next = textField(written, 'then'); - return { - name, - ...(when === undefined ? {} : { when: expressionText(when) }), - ...(next === undefined ? {} : { next }), - }; -} - -export function casesOf(task: JsonObject): readonly SwitchCase[] { - const cases = field(task, 'switch'); - return isList(cases) - ? cases.flatMap((item) => (isObject(item) ? entriesOf(item).map(([name, value]) => caseOf(name, value)) : [])) - : []; -} - -export function conditionOf(task: JsonObject): string | undefined { - const condition = field(task, 'if'); - return condition === undefined ? undefined : expressionText(condition); -} - -export function catchLabelOf(task: JsonObject): string { - const status = field(objectAt(task, 'catch', 'errors', 'with'), 'status'); - const retried = field(objectAt(task, 'catch'), 'retry') === undefined ? '' : ', after retries'; - return typeof status === 'number' ? `on error ${status}${retried}` : `on an error${retried}`; -} diff --git a/capabilities/coordination/src/steps/task-labels.ts b/capabilities/coordination/src/steps/task-labels.ts deleted file mode 100644 index ed81aa7ba..000000000 --- a/capabilities/coordination/src/steps/task-labels.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { alternatives, listed, quoted } from '@on.auto/operations'; -import { field, textField, type JsonObject } from '@on.auto/workflow-engine'; - -import { definitionCallOf, literalText } from '../document/workflow-functions.ts'; -import { durationText, expressionText, listenedOf, namedKeys, objectAt, runKindOf } from './task-reading.ts'; - -export function callLabel(task: JsonObject): string { - const call = definitionCallOf(task); - if (call === undefined) { - return `Call ${String(textField(task, 'call'))}`; - } - return call.name === undefined ? 'Run a definition' : `Run ${call.name}`; -} - -export function setLabel(task: JsonObject): string { - const keys = namedKeys(field(task, 'set')); - return keys === undefined || keys.length === 0 ? 'Set its data' : `Set ${listed(keys)}`; -} - -export function listenLabel(task: JsonObject): string { - const { mode, types } = listenedOf(task); - const named = types.map((type) => type ?? 'an event sent to the run'); - return `Wait for ${mode === 'all' ? listed(named) : alternatives(named)}`; -} - -export function waitLabel(task: JsonObject): string { - const duration = durationText(field(task, 'wait')); - return duration === undefined ? 'Wait' : `Wait ${duration}`; -} - -export function emitLabel(task: JsonObject): string { - const type = literalText(field(objectAt(task, 'emit', 'event', 'with'), 'type')); - return type === undefined ? 'Publish an event' : `Publish ${type}`; -} - -export function raiseLabel(task: JsonObject): string { - const error = field(objectAt(task, 'raise'), 'error'); - if (typeof error === 'string') { - return `Raise ${error}`; - } - const title = textField(objectAt(task, 'raise', 'error'), 'title'); - return title === undefined ? 'Raise an error' : `Raise ${quoted(title)}`; -} - -export function runLabel(task: JsonObject): string { - const kind = runKindOf(task); - return kind === undefined ? 'Run a process' : `Run a ${kind}`; -} - -export function forLabel(task: JsonObject): string { - const loop = objectAt(task, 'for'); - return `For each ${textField(loop, 'each') ?? 'item'} of ${expressionText(field(loop, 'in') ?? null)}`; -} - -export function forkLabel(task: JsonObject): string { - return field(objectAt(task, 'fork'), 'compete') === true ? 'At once, the first to finish wins' : 'At once'; -} diff --git a/capabilities/coordination/src/steps/task-reading.ts b/capabilities/coordination/src/steps/task-reading.ts deleted file mode 100644 index 200953e16..000000000 --- a/capabilities/coordination/src/steps/task-reading.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { durationInWords } from '@on.auto/definitions'; -import { - enclosedBody, - field, - isList, - isObject, - objectField, - readDuration, - type Json, - type JsonObject, -} from '@on.auto/workflow-engine'; - -import { literalText } from '../document/workflow-functions.ts'; - -type ListenMode = 'one' | 'any' | 'all'; - -export interface Listened { - readonly mode: ListenMode; - readonly types: readonly (string | undefined)[]; -} - -const listenModes: readonly ListenMode[] = ['one', 'any', 'all']; - -const runKinds: readonly string[] = ['container', 'script', 'shell', 'workflow']; - -export function objectAt(value: JsonObject, ...keys: readonly string[]): JsonObject { - return keys.reduce((current, key) => objectField(current, key) ?? {}, value); -} - -export function expressionText(value: Json): string { - return typeof value === 'string' ? (enclosedBody(value) ?? value).trim() : JSON.stringify(value); -} - -export function durationText(value: Json | undefined): string | undefined { - const reading = value === undefined ? undefined : readDuration(value); - return reading !== undefined && 'milliseconds' in reading ? durationInWords(reading.milliseconds) : undefined; -} - -function filterType(filter: Json): string | undefined { - return literalText(field(objectAt({ filter }, 'filter', 'with'), 'type')); -} - -export function listenedOf(task: JsonObject): Listened { - const to = objectAt(task, 'listen', 'to'); - const mode = listenModes.find((each) => field(to, each) !== undefined) ?? 'one'; - const filters = field(to, mode); - return { mode, types: isList(filters) ? filters.map((filter) => filterType(filter)) : [filterType(filters ?? {})] }; -} - -export function namedKeys(value: Json | undefined): readonly string[] | undefined { - return isObject(value) ? Object.keys(value) : undefined; -} - -export function runKindOf(task: JsonObject): string | undefined { - const run = objectAt(task, 'run'); - return runKinds.find((kind) => field(run, kind) !== undefined); -} diff --git a/capabilities/coordination/src/steps/task-words.ts b/capabilities/coordination/src/steps/task-words.ts deleted file mode 100644 index ec0ee1cce..000000000 --- a/capabilities/coordination/src/steps/task-words.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { definitionResourceLabel, listedPhrases, named, type Phrases } from '@on.auto/definitions'; -import { field, textField, type JsonObject } from '@on.auto/workflow-engine'; - -import { definitionCallOf, literalText, namesItsDefinition } from '../document/workflow-functions.ts'; -import { durationText, expressionText, listenedOf, namedKeys, objectAt, runKindOf } from './task-reading.ts'; - -export function callWords(task: JsonObject): Phrases { - const call = definitionCallOf(task); - if (call === undefined) { - return ['calls ', { code: String(textField(task, 'call')) }]; - } - return namesItsDefinition(call) - ? [`runs the ${definitionResourceLabel(call.type)} `, named(call.name)] - : ['runs the definition its call names when it runs']; -} - -export function setWords(task: JsonObject): Phrases { - const value = field(task, 'set'); - const keys = namedKeys(value) ?? []; - if (keys.length > 0) { - return [ - 'sets ', - ...listedPhrases( - keys.map((key) => [named(key)]), - 'and', - ), - ]; - } - return typeof value === 'string' ? ['sets its data from ', { code: expressionText(value) }] : ['sets its data']; -} - -export function listenWords(task: JsonObject): Phrases { - const { mode, types } = listenedOf(task); - const events = types.map((type): Phrases => - type === undefined ? ['an event sent to the run'] : ['the event ', named(type)], - ); - if (mode === 'any') { - return ['waits for the first of ', ...listedPhrases(events, 'or')]; - } - return [mode === 'all' ? 'waits for each of ' : 'waits for ', ...listedPhrases(events, 'and')]; -} - -export function waitWords(task: JsonObject): Phrases { - const duration = durationText(field(task, 'wait')); - return [duration === undefined ? 'waits for a time computed when it runs' : `waits ${duration}`]; -} - -export function emitWords(task: JsonObject): Phrases { - const type = literalText(field(objectAt(task, 'emit', 'event', 'with'), 'type')); - return type === undefined - ? ['publishes an event of a type computed when it runs to the playbook'] - : ['publishes the event ', named(type), ' to the playbook']; -} - -function statusWords(error: JsonObject): Phrases { - const status = field(error, 'status'); - return typeof status === 'number' ? [`, with status ${status}`] : []; -} - -export function raiseWords(task: JsonObject): Phrases { - const reference = field(objectAt(task, 'raise'), 'error'); - if (typeof reference === 'string') { - return ['raises the error ', named(reference), ' its document names under ', { code: 'use.errors' }]; - } - const error = objectAt(task, 'raise', 'error'); - const title = textField(error, 'title'); - const type = textField(error, 'type'); - if (title !== undefined) { - return ['raises the error ', { emphasis: title }, ...statusWords(error)]; - } - return type === undefined - ? ['raises an error'] - : ['raises an error of the type ', { code: type }, ...statusWords(error)]; -} - -export function runWords(task: JsonObject): Phrases { - const kind = runKindOf(task); - return [kind === undefined ? 'runs a process' : `runs a ${kind}`]; -} diff --git a/capabilities/coordination/src/steps/tried-tasks.test.ts b/capabilities/coordination/src/steps/tried-tasks.test.ts deleted file mode 100644 index 3fb62af62..000000000 --- a/capabilities/coordination/src/steps/tried-tasks.test.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { doneIn, stepNumbersOf, type DoneSteps } from '../testing/done-steps.ts'; - -interface Example extends DoneSteps { - readonly tasks: readonly string[]; -} - -function drawnFrom({ tasks }: Example): DoneSteps { - return doneIn(tasks.join('\n')); -} - -function expectedOf({ diagram, steps }: Example): DoneSteps { - return { diagram, steps }; -} - -const recovered: Example = { - tasks: [ - ' - attempt:', - ' try:', - ' - review: { call: run_definition, with: { type: reasoning, name: review-brief } }', - ' - score: { call: run_definition, with: { type: computation, name: campaign-pace } }', - ' catch:', - ' errors: { with: { status: 503 } }', - ' retry: { delay: { seconds: 5 }, limit: { attempt: { count: 3 } } }', - ' do:', - ' - fallback: { set: { review: null } }', - ], - diagram: [ - ' subgraph g1["1. Try"]', - ' n2["2. Run review-brief"]', - ' n3["3. Run campaign-pace"]', - ' end', - ' n1(["By a call"])', - ' n4["4. Set review"]', - ' n1 --> n2', - ' n2 --> n3', - ' n2 -.->|"on error 503, after retries"| n4', - ' n3 -.->|"on error 503, after retries"| n4', - ], - steps: [ - '1. **attempt** tries steps 2 to 3; on an error with status 503, it tries again as its catch says, and once no retry is left, it does step 4.', - '2. **review** runs the reasoning function **review-brief**.', - '3. **score** runs the computation function **campaign-pace**.', - '4. **fallback** sets **review**.', - ], -}; - -const guarded: Example = { - tasks: [ - ' - guarded:', - ' try:', - " - risky: { call: run_definition, with: { type: reasoning, name: '${ $data.name }' } }", - ' catch:', - ' errors: { with: { type: https://on.auto/problems/unanswered, status: 410 } }', - ` when: '\${ $error.kind == "expired" }'`, - ` exceptWhen: '\${ $error.because == "notified" }'`, - ' retry: patient', - ' - after: { set: { done: true } }', - ], - diagram: [ - ' subgraph g1["1. Try"]', - ' n2["2. Run a definition"]', - ' end', - ' n1(["By a call"])', - ' n3["3. Set done"]', - ' n1 --> n2', - ' n2 --> n3', - ' n2 -.->|"on error 410, after retries"| n3', - ], - steps: [ - '1. **guarded** tries step 2; on an error whose type is `https://on.auto/problems/unanswered` and with status 410, when `$error.kind == "expired"`, except when `$error.because == "notified"`, it tries again under the retry policy **patient**, and once no retry is left, it hands on its input.', - '2. **risky** runs the definition its call names when it runs.', - '3. **after** sets **done**.', - ], -}; - -const nestedInALoop: Example = { - tasks: [ - ' - each:', - " for: { in: '${ $data.briefs }' }", - ' do:', - ' - attempt:', - ' try:', - " - review: { call: run_definition, with: { type: reasoning, name: review-brief, input: '${ $item }' } }", - ' catch:', - ' do:', - ' - skip: { set: { skipped: true } }', - ], - diagram: [ - ' subgraph g1["1. For each item of $data.briefs"]', - ' n3["4. Set skipped"]', - ' subgraph g2["2. Try"]', - ' n2["3. Run review-brief"]', - ' end', - ' end', - ' n1(["By a call"])', - ' n1 --> n2', - ' n2 -->|"next item"| n2', - ' n2 -.->|"on an error"| n3', - ' n3 -->|"next item"| n2', - ], - steps: [ - '1. **each** does steps 2 to 4 for each **item** of `$data.briefs`.', - '2. **attempt** tries step 3; on any error, it does step 4.', - '3. **review** runs the reasoning function **review-brief**.', - '4. **skip** sets **skipped**.', - ], -}; - -const aroundAGroup: Example = { - tasks: [ - ' - attempt:', - ' try:', - ' - both:', - ' do:', - ' - one: { set: { a: 1 } }', - ' - two: { set: { b: 2 } }', - ' catch:', - ' do:', - ' - recover: { set: { c: 3 } }', - ], - diagram: [ - ' subgraph g1["1. Try"]', - ' subgraph g2["2. In order"]', - ' n2["3. Set a"]', - ' n3["4. Set b"]', - ' end', - ' end', - ' n1(["By a call"])', - ' n4["5. Set c"]', - ' n1 --> n2', - ' n2 --> n3', - ' n2 -.->|"on an error"| n4', - ' n3 -.->|"on an error"| n4', - ], - steps: [ - '1. **attempt** tries steps 2 to 4; on any error, it does step 5.', - '2. **both** does steps 3 to 4 in order.', - '3. **one** sets **a**.', - '4. **two** sets **b**.', - '5. **recover** sets **c**.', - ], -}; - -describe('the diagram of a try', () => { - it('is a group, with its catch a dashed edge from each of its steps to the steps that recover', () => { - const done = drawnFrom(recovered); - - expect(done).toEqual(expectedOf(recovered)); - expect(stepNumbersOf(done).drawn).toEqual(stepNumbersOf(done).listed); - }); - - it('goes on with a dashed edge when its catch has no steps, and names the errors it catches', () => { - const done = drawnFrom(guarded); - - expect(done).toEqual(expectedOf(guarded)); - expect(stepNumbersOf(done).drawn).toEqual(stepNumbersOf(done).listed); - }); -}); - -describe('the diagram of a task within another', () => { - it('nests its group within the group of the task around it, and the catch of a try in that group too', () => { - const done = drawnFrom(nestedInALoop); - - expect(done).toEqual(expectedOf(nestedInALoop)); - expect(stepNumbersOf(done).drawn).toEqual(stepNumbersOf(done).listed); - }); - - it('draws the catch of a try from each step of a group within it', () => { - const done = drawnFrom(aroundAGroup); - - expect(done).toEqual(expectedOf(aroundAGroup)); - expect(stepNumbersOf(done).drawn).toEqual(stepNumbersOf(done).listed); - }); -}); diff --git a/capabilities/coordination/src/testing/done-steps.ts b/capabilities/coordination/src/testing/done-steps.ts deleted file mode 100644 index 277d00a15..000000000 --- a/capabilities/coordination/src/testing/done-steps.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { markdownOf, numberedList, sectionOf } from '@on.auto/definitions'; -import { field } from '@on.auto/workflow-engine'; - -import { taskDiagramOf } from '../steps/task-diagram.ts'; -import { workflow } from './workflows.ts'; - -export interface DoneSteps { - readonly diagram: readonly string[]; - readonly steps: readonly string[]; -} - -export interface StepNumbers { - readonly drawn: readonly number[]; - readonly listed: readonly number[]; -} - -const labelledStep = /^\s*(?:n\d+\W+|subgraph g\d+\[")(?\d+)\. /u; - -export function doneIn(tasks: string, entries: readonly string[] = ['By a call']): DoneSteps { - const { diagram, steps } = taskDiagramOf(field(workflow(`do:\n${tasks}`), 'do'), entries); - const parts = markdownOf({ - title: 'flow', - opening: [], - body: [], - sections: [sectionOf('What it does', diagram, numberedList(steps))], - appendix: [], - }).split('\n\n'); - return { diagram: String(parts[3]).split('\n').slice(2, -1), steps: String(parts[4]).trimEnd().split('\n') }; -} - -function stepOf(line: string): readonly number[] { - const step = labelledStep.exec(line)?.groups?.['step']; - return step === undefined ? [] : [Number(step)]; -} - -export function stepNumbersOf({ diagram, steps }: DoneSteps): StepNumbers { - return { - drawn: diagram.flatMap((line) => stepOf(line)).toSorted((left, right) => left - right), - listed: steps.map((item) => Number(item.slice(0, item.indexOf('.')))), - }; -} diff --git a/capabilities/interaction/README.md b/capabilities/interaction/README.md index ee2505b9d..26932f06f 100644 --- a/capabilities/interaction/README.md +++ b/capabilities/interaction/README.md @@ -72,7 +72,7 @@ output: --- ``` -`parseInteractionDocument(source)` reads it with the shared document reader of `@on.auto/definitions/document` (`src/document`). The front matter takes `description`, `about`, the author's own Markdown, which `AboutSchema` of `@on.auto/definitions/document` bounds and the summary carries into the overview, `to`, a Liquid template that names the party, `from`, one that names the answerer, `expires`, an ISO 8601 duration from one minute to thirty days read by `readDuration` of `@on.auto/workflow-engine/dsl`, `deliver` and `replies`, `input` and `output`, each with a `schema`, and `reply`, the reply rule. Every other key, `model` and `tools` among them, is refused with its line, within the nested blocks too. Without `output.schema` the function is a notification: it takes no answer, and its run succeeds, with the output `{}`, once its delivery lands, at once without `deliver`. The body is the message, a Liquid template. A document is one of two shapes, `{ shape: 'call' }` or `{ shape: 'request' }` (`interaction-document.ts`): with `call`, `output.schema` is required and `to`, `expires`, `deliver`, `replies`, `from`, `reply` and a body are refused with their line (`call-parts.ts`); without it, `to` and `expires` are required (`request-parts.ts`). `to`, `from` and the message read `input`, `today` and `now` alone, through an instance of the engine of `@on.auto/definitions/template` with no registrations of its own, and a template that does not compile, reads another name, or a property a closed input schema does not have, is refused with its line. +`parseInteractionDocument(source)` reads it with the shared document reader of `@on.auto/definitions/document` (`src/document`). The front matter takes `description`, `to`, a Liquid template that names the party, `from`, one that names the answerer, `expires`, an ISO 8601 duration from one minute to thirty days read by `readDuration` of `@on.auto/workflow-engine/dsl`, `deliver` and `replies`, `input` and `output`, each with a `schema`, and `reply`, the reply rule. Every other key, `model` and `tools` among them, is refused with its line, within the nested blocks too. Without `output.schema` the function is a notification: it takes no answer, and its run succeeds, with the output `{}`, once its delivery lands, at once without `deliver`. The body is the message, a Liquid template. A document is one of two shapes, `{ shape: 'call' }` or `{ shape: 'request' }` (`interaction-document.ts`): with `call`, `output.schema` is required and `to`, `expires`, `deliver`, `replies`, `from`, `reply` and a body are refused with their line (`call-parts.ts`); without it, `to` and `expires` are required (`request-parts.ts`). `to`, `from` and the message read `input`, `today` and `now` alone, through an instance of the engine of `@on.auto/definitions/template` with no registrations of its own, and a template that does not compile, reads another name, or a property a closed input schema does not have, is refused with its line. `src/tool-blocks` holds the blocks, written once as effect schemas on one `ToolUseSchema`, `{ server, tool, with }`: `DeliverBlockSchema`, which adds `sent`, `CallBlockSchema`, which adds `read`, and `RepliesSchema`; `compiledToolBlock(written, lines, inputSchema)` checks a delivery and its reading at save, and `compiledCallBlock` a call: the server and tool names by their shapes, `isServerName` and `isToolName` of `@on.auto/tool-servers`; every template of `call.with`, `deliver.with`, `replies.conversation`, `replies.with` and `tell.with` against the names its place reads, with no `${`; arguments of a call that read no `input` within 16 KiB as JSON; each pointer of `call.read`, `deliver.sent` and `replies.read` a JSON Pointer that starts with a slash, refused in one sentence; `read.order`; `wait` from `PT5S` to `PT1H`. `replies` needs `deliver`, its `sent` and `output.schema`. Arguments are typed as written (`renderedArguments`): a number, boolean, null, list or object is sent as written with the templates in its strings rendered, a string that is one `{{ expression }}` alone is sent as the value it reads, and any other string is rendered as text, so a structure inside a longer text is refused at save; `momentVariables(input, at)` gives a call and a request the same `input`, `today` and `now`. `routeOf(blocks)` answers what an attempt, a read or a telling works with, `{ kind: 'inbox' }` or `{ kind: 'tool', delivery, replies? }`, and `throughWords` says it, the same sentence the capability's own `deliveryStarted` says (`src/facts/fact-words.ts`). @@ -151,6 +151,4 @@ Attempts follow `attemptSchedule` (`src/schedule/attempt-schedule.ts`): five, th ## Source -`src/document` reads the definition, `src/tool-blocks` the call, the delivery and the reading, `src/call` runs a call and reads its endings, `src/run` renders and defers a request, `src/requests` holds the projection of open requests and the two operations, `src/facts` the facts the capability declares, their words and the rules it records them by, `src/delivery` the attempt as a recorded call, `src/schedule` the due work and its schedule, `src/conversations` the projection of conversations and the reading, `src/replies` the reply rule, the taking and the tellings, `src/overview` the overview, `src/capability` the capability, its guide, the public reference page served to agents as `interaction-function`, and its words, and `src/testing` what the tests share. - -`src/overview` is the function's overview, told as what happens by the rules of [Told as what happens](../../packages/definitions/README.md#told-as-what-happens), which `overview` of `src/capability/definition-reading.ts` composes by the document's shape. Of a function that asks a system, `callOverview` says in one sentence, `callSentence` of `@on.auto/definitions`, what it is given, the server and the tool it asks and where in the tool's answer `read` points, and under Details the tables of its input and its output and the call as its document writes it, `callSection`. Of one that asks a person, `requestOverview` draws the reply, `replyDiagram`, where its rule reads a first word; says what it is given and whom it sends the message: the party `to` means, `partyOf` of `request-party.ts`, the meaning of the value `to` reads where `to` is one output, `to` as written where it is one line of text, and the party named under Details otherwise; quotes the message, read from the text its parsed template keeps with the line it starts on, through `quotedTemplate` of `@on.auto/definitions/template`, each value in brackets; says in one paragraph how long it waits and how far apart it reads the replies, `replyWaitMsOf` of `src/conversations/cadence.ts`, how its reply rule takes a reply, and when the request ends unanswered, or that it is left in the inbox, or that a notification waits for nothing; and says what it answers with. Under Details are the tables of its input and its answer and **Where**: `to` and `from` as written, the tools and servers of the delivery and of the reading, and `wait` and `expires` as written. `reference-inbox.overview.md` and `reference-chat.overview.md` are the overviews of the two examples of the public reference page, `reference-call.overview.md` that of the function that asks a system on the reference page of computation functions, and `approve-report.overview.md` that of a function that asks a person and reads the replies to its message, as a read writes it with the workflow that calls it, which `interaction-overview.test.ts` writes again and compares byte for byte. +`src/document` reads the definition, `src/tool-blocks` the call, the delivery and the reading, `src/call` runs a call and reads its endings, `src/run` renders and defers a request, `src/requests` holds the projection of open requests and the two operations, `src/facts` the facts the capability declares, their words and the rules it records them by, `src/delivery` the attempt as a recorded call, `src/schedule` the due work and its schedule, `src/conversations` the projection of conversations and the reading, `src/replies` the reply rule, the taking and the tellings, `src/capability` the capability, its guide, the public reference page served to agents as `interaction-function`, and its words, and `src/testing` what the tests share. diff --git a/capabilities/interaction/src/capability/definition-reading.ts b/capabilities/interaction/src/capability/definition-reading.ts index 4af032dff..ddc90a5e9 100644 --- a/capabilities/interaction/src/capability/definition-reading.ts +++ b/capabilities/interaction/src/capability/definition-reading.ts @@ -1,4 +1,4 @@ -import type { DefinitionSummary, ToldParts } from '@on.auto/definitions'; +import type { DefinitionSummary } from '@on.auto/definitions'; import { issueText } from '@on.auto/definitions/document'; import { renderClock } from '@on.auto/definitions/template'; import { InvalidInput } from '@on.auto/operations'; @@ -6,8 +6,6 @@ import { Effect, Result, type Schema } from 'effect'; import { parseInteractionDocument } from '../document/document-parsing.ts'; import type { InteractionFunctionDefinitionDocument } from '../document/interaction-document.ts'; -import { callOverview } from '../overview/call-overview.ts'; -import { requestOverview } from '../overview/request-overview.ts'; export function parse(source: string): Effect.Effect { return Effect.flatMap(renderClock, (clock) => @@ -35,16 +33,11 @@ function detailsOf(document: InteractionFunctionDefinitionDocument): Schema.Json } export function summarize(document: InteractionFunctionDefinitionDocument): DefinitionSummary { - const { description, about, input, output } = document; + const { description, input, output } = document; return { ...(description === undefined ? {} : { description }), - ...(about === undefined ? {} : { about }), ...(input.schema === undefined ? {} : { inputSchema: input.schema.document }), ...(output.schema === undefined ? {} : { outputSchema: output.schema.document }), details: detailsOf(document), }; } - -export function overview(document: InteractionFunctionDefinitionDocument): ToldParts { - return document.shape === 'call' ? callOverview(document) : requestOverview(document); -} diff --git a/capabilities/interaction/src/capability/interaction-function.ts b/capabilities/interaction/src/capability/interaction-function.ts index 0214de010..c9d3983bd 100644 --- a/capabilities/interaction/src/capability/interaction-function.ts +++ b/capabilities/interaction/src/capability/interaction-function.ts @@ -10,7 +10,7 @@ import type { InteractionFunctionDefinitionDocument } from '../document/interact import { interactionFacts } from '../facts/declared-facts.ts'; import { asksLater, requestRun } from '../run/interaction-run.ts'; import type { InteractionPorts } from '../run/request-reach.ts'; -import { overview, parse, summarize } from './definition-reading.ts'; +import { parse, summarize } from './definition-reading.ts'; import { interactionType } from './interaction-type.ts'; import { describeAnswer, interactionRunWords } from './interaction-words.ts'; import { cancelledRequest } from './request-cancels.ts'; @@ -40,7 +40,6 @@ export function makeInteractionFunctionAdapter(ports: InteractionPorts): Capabil mediaType: 'text/markdown', parse, summarize, - overview, run: (document, input, context) => document.shape === 'call' ? called(document, input, context) : asked(document, input, context), whenCancelled: 'finish', diff --git a/capabilities/interaction/src/document/call-parts.ts b/capabilities/interaction/src/document/call-parts.ts index b7ee2ede1..6315a8748 100644 --- a/capabilities/interaction/src/document/call-parts.ts +++ b/capabilities/interaction/src/document/call-parts.ts @@ -66,7 +66,6 @@ export function callFrom( : Result.map(Result.all({ input, output: answered }), (contracts: Pick) => ({ shape: 'call', ...(written.description === undefined ? {} : { description: written.description }), - ...(written.about === undefined ? {} : { about: written.about }), call, ...contracts, })); diff --git a/capabilities/interaction/src/document/document-parsing.test.ts b/capabilities/interaction/src/document/document-parsing.test.ts index e128e08a7..57a61ef36 100644 --- a/capabilities/interaction/src/document/document-parsing.test.ts +++ b/capabilities/interaction/src/document/document-parsing.test.ts @@ -3,7 +3,7 @@ import { standingRenderClock } from '@on.auto/definitions/testing'; import { Result } from 'effect'; import { describe, expect, it } from 'vitest'; -import { approvalDocument, callDocument, notificationDocument } from '../testing/index.ts'; +import { approvalDocument, notificationDocument } from '../testing/index.ts'; import { requestDocumentOf } from '../testing/route-documents.ts'; import { parseInteractionDocument } from './document-parsing.ts'; @@ -18,7 +18,7 @@ function documentWith(frontMatter: readonly string[], body = 'Approve {{ input.c const essentials = ["to: '{{ input.owner }}'", 'expires: P2D']; -const keys = 'description, about, call, to, from, expires, deliver, replies, input, output, reply'; +const keys = 'description, call, to, from, expires, deliver, replies, input, output, reply'; describe('an interaction function definition', () => { it('reads its party, expiry, schemas and message, and waits in the inbox without deliver', () => { @@ -154,26 +154,3 @@ describe('the refusals of the form of a definition', () => { ]); }); }); - -describe('the about of an interaction function definition', () => { - const about = ['about: |', ' The campaign owner approves each brief before it goes to a client.']; - - it('is kept as its author wrote it, for a function that asks a person and for one that asks a system', () => { - expect([ - requestDocumentOf(documentWith([...essentials, ...about])).about, - Result.getOrThrow(parseInteractionDocument(callDocument({ front: about }), standingRenderClock)).about, - ]).toEqual([ - 'The campaign owner approves each brief before it goes to a client.\n', - 'The campaign owner approves each brief before it goes to a client.\n', - ]); - }); - - it('takes no heading, nor does the description, each refused at its line', () => { - const frontMatter = ["description: '# Approve a brief'", ...essentials, ...about, ' ## When it runs']; - - expect(problemsOf(documentWith(frontMatter))).toEqual([ - 'Line 2, /description: description takes no heading, since an overview writes its own: the line that begins “# Approve a brief” makes one, with # before its words or as a line of = or - under the words above it', - 'Line 7, /about: about takes no heading, since an overview writes its own: this line makes one, with # before its words or as a line of = or - under the words above it', - ]); - }); -}); diff --git a/capabilities/interaction/src/document/front-matter.ts b/capabilities/interaction/src/document/front-matter.ts index 38f43fcd1..09fb9b8d3 100644 --- a/capabilities/interaction/src/document/front-matter.ts +++ b/capabilities/interaction/src/document/front-matter.ts @@ -1,9 +1,4 @@ -import { - AboutSchema, - DescriptionSchema, - type FrontMatterSection, - type FrontMatterShape, -} from '@on.auto/definitions/document'; +import { DescriptionSchema, type FrontMatterSection, type FrontMatterShape } from '@on.auto/definitions/document'; import { Schema } from 'effect'; import { WrittenRuleSchema } from '../replies/reply-rule.ts'; @@ -21,7 +16,6 @@ const SectionSchema = Schema.Struct({ schema: Schema.optionalKey(Schema.JsonObje const FrontMatterSchema = Schema.Struct({ description: Schema.optionalKey(DescriptionSchema), - about: Schema.optionalKey(AboutSchema), call: Schema.optionalKey(CallBlockSchema), to: Schema.optionalKey(Schema.String), from: Schema.optionalKey(Schema.String), diff --git a/capabilities/interaction/src/document/interaction-document.ts b/capabilities/interaction/src/document/interaction-document.ts index 06e39ace6..837b8c60b 100644 --- a/capabilities/interaction/src/document/interaction-document.ts +++ b/capabilities/interaction/src/document/interaction-document.ts @@ -8,7 +8,6 @@ import type { CallBlock } from '../tool-blocks/tool-block-schemas.ts'; interface WrittenFunction { readonly description?: string; - readonly about?: string; readonly input: ValueContract; } diff --git a/capabilities/interaction/src/document/request-parts.ts b/capabilities/interaction/src/document/request-parts.ts index 0c4456e05..8a9310cd9 100644 --- a/capabilities/interaction/src/document/request-parts.ts +++ b/capabilities/interaction/src/document/request-parts.ts @@ -64,7 +64,6 @@ function documentOfParts(written: InteractionFrontMatter, expires: string, done: return { shape: 'request', ...(written.description === undefined ? {} : { description: written.description }), - ...(written.about === undefined ? {} : { about: written.about }), ...(route === null ? {} : { route }), to, ...(from === null ? {} : { from }), diff --git a/capabilities/interaction/src/overview/approve-report.overview.md b/capabilities/interaction/src/overview/approve-report.overview.md deleted file mode 100644 index 5e8b5dffb..000000000 --- a/capabilities/interaction/src/overview/approve-report.overview.md +++ /dev/null @@ -1,62 +0,0 @@ -# approve-report - -Sends the draft report to the approver in the team chat and waits for a reply of approve or reject in the thread. - -```mermaid -flowchart LR - n1["The approver, the month and the draft report"] - n2["A message to the approver through gas"] - n3["Waits up to 7 days for a reply"] - n4{"The first word of the reply"} - n5(["approve"]) - n6(["reject"]) - n7(["unanswered"]) - n1 --> n2 - n2 --> n3 - n3 --> n4 - n4 -->|"approve, approved, yes, ok, okay, lgtm or ship"| n5 - n4 -->|"reject, rejected, no or nope"| n6 - n4 -.->|"any other word, told how to answer"| n3 - n3 -.->|"no reply"| n7 -``` - -It is given the approver, the month and the draft report. It sends the approver this message through **gas**, with the run's values in place of the words in brackets: - -> :wave: Draft website report for *\[the month\]* is ready for your okay before it goes to #random. -> -> \[the draft report\] -> -> Reply *in this thread* with *approve* to log it in the tracker sheet and post it, or *reject* (optionally with a note). - -It waits up to 7 days for a reply to that message, reading the replies at least 15 seconds apart. A reply whose first word is approve, approved, yes, ok, okay, lgtm or ship gives the verdict approve; one whose first word is reject, rejected, no or nope gives reject; whatever follows the first word is the note. A reply that begins with any other word is not taken, and the person is told how to answer. If nobody answers within 7 days, the request ends unanswered. - -It answers with the verdict, one of approve and reject, and the note, when there is one. - -## Used by - -- The workflow **Monthly website report** calls it in step 6, **Send draft**. - -## Details - -### Input - -| Field | Required | What it is | -| --- | --- | --- | -| **approver** | yes | Chat user id of the approver. Text. | -| **label** | yes | The month, such as April 2026. Text. | -| **draft** | yes | The draft report. Text, at most 6,000 characters. | - -### Answer - -| Field | Required | What it is | -| --- | --- | --- | -| **verdict** | yes | One of: approve, reject. | -| **note** | no | Text, at most 2,000 characters. | - -### Where - -To `{{ input.approver }}`. Sent through the tool `execute` of the server **gas**; the replies read with the tool `execute` of the server **gas**, at least `PT15S` apart; expires after `P7D`. - -### Used by - -- `monthly-website-report`, a workflow diff --git a/capabilities/interaction/src/overview/call-overview.ts b/capabilities/interaction/src/overview/call-overview.ts deleted file mode 100644 index a3594462e..000000000 --- a/capabilities/interaction/src/overview/call-overview.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { callSection, callSentence, inputDetails, outputDetails, type ToldParts } from '@on.auto/definitions'; - -import type { CallDocument } from '../document/interaction-document.ts'; - -export function callOverview({ call, input, output }: CallDocument): ToldParts { - const { server, tool, read } = call; - const given = input.schema?.document; - const answered = output.schema.document; - return { - body: [callSentence({ input: given, server, tool, read, output: answered })], - appendix: [inputDetails(given), outputDetails(answered), callSection({ server, tool, read, arguments: call.with })], - }; -} diff --git a/capabilities/interaction/src/overview/interaction-overview.test.ts b/capabilities/interaction/src/overview/interaction-overview.test.ts deleted file mode 100644 index 78cc9e47c..000000000 --- a/capabilities/interaction/src/overview/interaction-overview.test.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { readFileSync } from 'node:fs'; - -import { markdownOf, type Overview } from '@on.auto/definitions'; -import { onTheStandingClock, overviewAsRead } from '@on.auto/definitions/testing'; -import { Effect } from 'effect'; -import { describe, expect, it } from 'vitest'; - -import { callDocument, interactionHarness } from '../testing/index.ts'; - -const { capability } = interactionHarness(); - -function examplesOf(page: string): readonly string[] { - const text = readFileSync(new URL(`../../../../docs/reference/${page}`, import.meta.url), 'utf8'); - return [...text.matchAll(/```markdown\n([\s\S]*?)```/gu)].map(([, body = '']: readonly string[]) => body); -} - -function goldenOf(file: string): string { - return readFileSync(new URL(file, import.meta.url), 'utf8'); -} - -function composedOf(source: string, name: string): Overview { - return Effect.runSync(onTheStandingClock(capability.prepare(source))).overview(name); -} - -function overviewOf(source: string, name = 'read-thread'): string { - return markdownOf(composedOf(source, name)); -} - -const [inboxExample = '', chatExample = ''] = examplesOf('interaction-format.md'); - -const callExample = examplesOf('computation-format.md').find((example) => example.includes('\ncall:\n')) ?? ''; - -const approveReport = [ - '---', - 'description: Sends the draft report to the approver in the team chat and waits for a reply of approve or reject in the thread', - "to: '{{ input.approver }}'", - 'expires: P7D', - 'deliver:', - ' server: gas', - ' tool: execute', - ' with:', - ' function: postForApproval', - " parameters: ['{{ to }}', '{{ message }}']", - ' sent:', - ' conversation: /channel', - ' id: /ts', - 'replies:', - ' tool: execute', - ' with:', - ' function: repliesSince', - ` parameters: ['{{ sent.conversation }}', '{{ sent.id }}', '{{ since | default: "0" }}']`, - ' read:', - ' list: /replies', - ' order: oldest_first', - ' each: { id: /ts, sender: /user, text: /text }', - ' wait: PT15S', - ' tell:', - ' with:', - ' function: replyInThread', - " parameters: ['{{ sent.conversation }}', '{{ sent.id }}', '{{ message }}']", - 'reply:', - ' verdict: { from: word, words: { approve: [approved, yes, ok, okay, lgtm, ship], reject: [rejected, no, nope] } }', - ' note: rest', - 'input:', - ' schema:', - ' type: object', - ' required: [approver, label, draft]', - ' properties:', - " approver: { type: string, description: 'Chat user id of the approver' }", - " label: { type: string, description: 'The month, such as April 2026' }", - " draft: { type: string, maxLength: 6000, description: 'The draft report' }", - 'output:', - ' schema:', - ' type: object', - ' required: [verdict]', - ' properties:', - ' verdict: { type: string, enum: [approve, reject] }', - ' note: { type: string, maxLength: 2000 }', - '---', - ':wave: Draft website report for *{{ input.label }}* is ready for your okay before it goes to #random.', - '', - '{{ input.draft }}', - '', - 'Reply *in this thread* with *approve* to log it in the tracker sheet and post it, or *reject* (optionally with a note).', -].join('\n'); - -describe('the overviews of the examples of the reference of interaction functions', () => { - it('are, byte for byte, the overviews pinned beside this test, of a request to the inbox and through a chat', () => { - expect(overviewOf(inboxExample, 'approve-brief')).toBe(goldenOf('reference-inbox.overview.md')); - expect(overviewOf(chatExample, 'approve-draft')).toBe(goldenOf('reference-chat.overview.md')); - }); - - it('is, byte for byte for the function that asks a system, the overview pinned beside this test', () => { - expect(overviewOf(callExample, 'read-campaign-costs')).toBe(goldenOf('reference-call.overview.md')); - }); -}); - -describe('the overview of an interaction function that asks a person and reads the replies to its message', () => { - it('draws the reply, quotes the message with its values in brackets and tells what happens, as read the overview pinned beside this test', () => { - const caller = { - type: 'workflow', - name: 'monthly-website-report', - details: { - title: 'Monthly website report', - calls: [{ type: 'interaction', name: 'approve-report', steps: [{ number: 6, name: 'send-draft' }] }], - }, - }; - const overview = composedOf(approveReport, 'approve-report'); - - expect(overviewAsRead({ type: 'interaction', name: 'approve-report', overview }, [caller])).toBe( - goldenOf('approve-report.overview.md'), - ); - }); -}); - -describe('the overview of an interaction function that asks a system', () => { - it('names the server and the tool it asks, the input it is given and where in the answer it reads, in one paragraph', () => { - const source = callDocument({ read: '/data/items~1all~0x' }); - - expect(composedOf(source, 'read-thread').body).toHaveLength(1); - expect(overviewOf(source).split('\n\n')[2]).toBe( - 'It is given the channel and the thread and asks **chat** with its tool thread, once per run. It answers with what it answered under data, then items/all~x: a list of records.', - ); - }); - - it('answers with the whole answer without read, and writes the arguments of a call that reads no input as written', () => { - const overview = overviewOf(callDocument({ read: null, input: [], with: [' limit: 100'] })); - - expect(overview).toContain( - 'It is given any input and asks **chat** with its tool thread, once per run. It answers with the whole of what the tool answered:', - ); - expect(overview).toContain( - '### Call\n\nIt calls the tool `thread` of the server **chat**, its whole answer read, with these arguments, as written:\n\n- `limit`: `100`\n', - ); - }); -}); - -describe('the overview of an interaction function whose document has an about', () => { - it('writes the about as its author wrote it, after the description and before what it is given', () => { - const about = [ - 'about: |', - ' The campaign team reads what a thread said before it plans the next brief.', - '', - ' A workflow asks it once a thread is closed.', - ]; - - expect( - overviewOf(callDocument({ front: about })) - .split('\n\n') - .slice(0, 4), - ).toEqual([ - '# read-thread', - 'Read the replies of a thread in the team chat, oldest first.', - 'The campaign team reads what a thread said before it plans the next brief.', - 'A workflow asks it once a thread is closed.', - ]); - }); -}); diff --git a/capabilities/interaction/src/overview/reference-call.overview.md b/capabilities/interaction/src/overview/reference-call.overview.md deleted file mode 100644 index 9a37eb8f2..000000000 --- a/capabilities/interaction/src/overview/reference-call.overview.md +++ /dev/null @@ -1,23 +0,0 @@ -# read-campaign-costs - -Reads the cost rows of every campaign for a month. - -It is given the month and asks **ads** with its tool campaign-costs, once per run. It answers with what it answered under rows: a list of records. - -## Details - -### Input - -| Field | Required | What it is | -| --- | --- | --- | -| **month** | yes | Text. | - -### Output - -A list of records with: **campaign** (text), **cost_cents** (a whole number), **budget_cents** (a whole number). - -### Call - -It calls the tool `campaign-costs` of the server **ads**, its answer read at `/rows`, with these arguments, as written: - -- `month`: `{{ input.month }}` diff --git a/capabilities/interaction/src/overview/reference-chat.overview.md b/capabilities/interaction/src/overview/reference-chat.overview.md deleted file mode 100644 index e13111308..000000000 --- a/capabilities/interaction/src/overview/reference-chat.overview.md +++ /dev/null @@ -1,53 +0,0 @@ -# approve-draft - -Send the draft to its owner in the chat and take their approval. - -```mermaid -flowchart LR - n1["The owner and the draft"] - n2["A message to the owner through chat"] - n3["Waits up to 2 days for a reply"] - n4{"The first word of the reply"} - n5(["approve"]) - n6(["revise"]) - n7(["unanswered"]) - n1 --> n2 - n2 --> n3 - n3 --> n4 - n4 -->|"approve"| n5 - n4 -->|"revise"| n6 - n4 -.->|"any other word, told how to answer"| n3 - n3 -.->|"no reply"| n7 -``` - -It is given the owner and the draft. It sends the owner this message through **chat**, with the run's values in place of the words in brackets: - -> Here is the draft: -> -> \[the draft\] -> -> Reply **approve**, or **revise** with a note of what to change. - -It waits up to 2 days for a reply to that message, reading the replies at least 1 minute apart. A reply whose first word is approve gives the verdict approve; one whose first word is revise gives revise; whatever follows the first word is the note. A reply that begins with any other word is not taken, and the person is told how to answer. If nobody answers within 2 days, the request ends unanswered. - -It answers with the verdict, one of approve and revise, and the note, when there is one. - -## Details - -### Input - -| Field | Required | What it is | -| --- | --- | --- | -| **owner** | yes | Text. | -| **draft** | yes | Text, at most 4,000 characters. | - -### Answer - -| Field | Required | What it is | -| --- | --- | --- | -| **verdict** | yes | One of: approve, revise. | -| **note** | no | Text, at most 2,000 characters. | - -### Where - -To `{{ input.owner }}`. Sent through the tool `post_message` of the server **chat**; the replies read with the tool `thread_replies` of the server **chat**, at least `PT1M` apart; expires after `P2D`. diff --git a/capabilities/interaction/src/overview/reference-inbox.overview.md b/capabilities/interaction/src/overview/reference-inbox.overview.md deleted file mode 100644 index 1bf334c4e..000000000 --- a/capabilities/interaction/src/overview/reference-inbox.overview.md +++ /dev/null @@ -1,35 +0,0 @@ -# approve-brief - -Asks the campaign owner to approve a reviewed brief. - -It is given the owner and the review. It sends the owner this message, with the run's values in place of the words in brackets: - -> The review of your campaign brief is ready: -> -> \[the review\] -> -> Approve the brief as it stands, or ask for a revision. - -It leaves the message in the playbook's inbox, where whoever may write to the playbook answers it. If nobody answers within 2 days, the request ends unanswered. - -It answers with the verdict, one of approve and revise, and the note, when there is one. - -## Details - -### Input - -| Field | Required | What it is | -| --- | --- | --- | -| **owner** | yes | Text. | -| **review** | yes | Text. | - -### Answer - -| Field | Required | What it is | -| --- | --- | --- | -| **verdict** | yes | One of: approve, revise. | -| **note** | no | Text, at most 2,000 characters. | - -### Where - -To `{{ input.owner }}`. Left in the playbook's inbox; expires after `P2D`. diff --git a/capabilities/interaction/src/overview/request-overview.test.ts b/capabilities/interaction/src/overview/request-overview.test.ts deleted file mode 100644 index c6e99424d..000000000 --- a/capabilities/interaction/src/overview/request-overview.test.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { markdownOf } from '@on.auto/definitions'; -import { onTheStandingClock } from '@on.auto/definitions/testing'; -import { Effect } from 'effect'; -import { describe, expect, it } from 'vitest'; - -import { - approvalDocument, - chatDelivery, - interactionHarness, - notificationDocument, - threadDocument, - threadReplies, -} from '../testing/index.ts'; - -const { capability } = interactionHarness(); - -function overviewOf(source: string): string { - return markdownOf(Effect.runSync(onTheStandingClock(capability.prepare(source))).overview('approve-brief')); -} - -function bodyOf(source: string): readonly string[] { - const [story = ''] = overviewOf(source).split('\n\n## Details\n\n'); - return story.split('\n\n').slice(2); -} - -function leadOf(source: string): string { - return bodyOf(source).find((block) => block.startsWith('It is given')) ?? ''; -} - -function leadTo(to: string): string { - return leadOf(approvalDocument().replace("to: '{{ input.owner }}'", to)); -} - -function detailsIn(overview: string, heading: string): string { - return overview.split(`\n\n### ${heading}\n\n`)[1]?.split('\n\n### ')[0] ?? ''; -} - -function waitOf(expires: string): string { - return bodyOf(approvalDocument([], expires)).at(-2) ?? ''; -} - -const choices = [ - 'output:', - ' schema:', - ' type: object', - ' required: [decision]', - ' properties:', - ' decision: { type: string, enum: [approve, revise, skip] }', - ' note: { type: string }', - ' said: { type: string }', - 'reply:', - ' decision: { from: word, words: { approve: [yes], skip: [later, pass] } }', - ' note: rest', - ' said: text', -]; - -const counted = [ - 'output:', - ' schema:', - ' type: object', - ' required: [count]', - ' properties:', - ' count: { type: integer }', -]; - -describe('how long a request waits, in the overview', () => { - it('says the expiry in days, hours, minutes and seconds, to the nearest second, for a request left in the inbox', () => { - expect(['P2D', 'PT1M', 'PT4H', 'P1DT12H30M', 'PT90.5S', 'P1W'].map((expires) => waitOf(expires))).toEqual( - ['2 days', '1 minute', '4 hours', '1 day, 12 hours, and 30 minutes', '1 minute and 31 seconds', '7 days'].map( - (words) => - `It leaves the message in the playbook's inbox, where whoever may write to the playbook answers it. If nobody answers within ${words}, the request ends unanswered.`, - ), - ); - }); - - it('says of a notification that it waits for nothing, takes no answer and draws nothing, with its expiry under Details', () => { - const inbox = overviewOf(notificationDocument()); - const sent = overviewOf(notificationDocument(chatDelivery)); - - expect([bodyOf(notificationDocument()).at(-1), bodyOf(notificationDocument(chatDelivery)).at(-1)]).toEqual([ - "It leaves the message in the playbook's inbox, where whoever may write to the playbook reads it, and waits for nothing.", - 'It sends the message and waits for nothing.', - ]); - expect([detailsIn(inbox, 'Answer'), detailsIn(sent, 'Where')]).toEqual([ - 'None: it is a notification, which takes no answer and succeeds once its request is delivered.', - 'To `{{ input.owner }}`. Sent through the tool `post_message` of the server **chat**; expires after `PT1H`.\n', - ]); - expect([inbox, sent].map((overview) => [overview.includes('mermaid'), overview.includes('It answers')])).toEqual([ - [false, false], - [false, false], - ]); - }); - - it('says that a request sent through a tool whose replies it does not read waits for the answer of whoever may write', () => { - expect([ - bodyOf(approvalDocument(chatDelivery)).at(-2), - bodyOf(approvalDocument(chatDelivery, 'P2D', counted)), - ]).toEqual([ - 'It waits up to 2 days for the answer, which whoever may write to the playbook gives. If nobody answers within 2 days, the request ends unanswered.', - [ - "It is given the campaign and the owner. It sends the owner this message through **chat**, with the run's values in place of the words in brackets:", - '> Please review the brief for \\[the campaign\\].', - 'It waits up to 2 days for the answer, which whoever may write to the playbook gives. If nobody answers within 2 days, the request ends unanswered.', - 'It answers with the count.', - ], - ]); - }); -}); - -describe('what a request is given and whom it asks, in the overview', () => { - it('says the fields it is given, then the party the value of its to means, before the message it quotes', () => { - expect(bodyOf(approvalDocument()).slice(0, 2)).toEqual([ - "It is given the campaign and the owner. It sends the owner this message, with the run's values in place of the words in brackets:", - '> Please review the brief for \\[the campaign\\].', - ]); - }); - - it('names a party written as text on one line as it is written, and any other party by the party named under Details', () => { - expect([ - leadTo("to: '#approvals'"), - leadTo("to: '{% if input.urgent %}on-call{% else %}{{ input.owner }}{% endif %}'"), - leadTo('to: "{{ input.owner }}\\n{{ input.campaign }}"'), - leadTo("to: '{{ input.owner }} {{ input.campaign }}'"), - ]).toEqual([ - "It is given the campaign and the owner. It sends #approvals this message, with the run's values in place of the words in brackets:", - "It is given the campaign and the owner. It sends the party named under Details this message, with the run's values in place of the words in brackets:", - "It is given the campaign and the owner. It sends the party named under Details this message, with the run's values in place of the words in brackets:", - "It is given the campaign and the owner. It sends the party named under Details this message, with the run's values in place of the words in brackets:", - ]); - }); - - it('writes the party and the answerer as their document writes them under Details', () => { - expect(detailsIn(overviewOf(threadDocument()), 'Where')).toBe( - 'To `#approvals-{{ input.team }}`, answered by `{{ input.owner }}`. Sent through the tool `post_message` of the server **chat**; the replies read with the tool `thread_replies` of the server **chat**, at least `PT1M` apart; expires after `P2D`.\n', - ); - }); -}); - -describe('the replies of a request, in the overview', () => { - it('say how a reply gives each part of the answer, what it reads first and that a reply of another word is not taken', () => { - const source = threadDocument({ rule: [] }).replace(/output:\n[\s\S]*maxLength: 2000 \}/u, choices.join('\n')); - - expect(bodyOf(source).slice(-2)).toEqual([ - 'It waits up to 2 days for a reply to that message, reading the replies at least 1 minute apart. A reply whose first word is approve or yes gives the decision approve; one whose first word is revise gives revise; one whose first word is skip, later or pass gives skip; whatever follows the first word is the note; the whole reply is the said. A reply that begins with any other word is not taken, and the person is told how to answer. If nobody answers within 2 days, the request ends unanswered.', - 'It answers with the decision, one of approve, revise and skip, the note, when there is one, and the said, when there is one.', - ]); - }); - - it('say that a reply of another word is not taken, without telling the person where the function does not', () => { - const source = threadDocument({ replies: threadReplies.slice(0, -5) }); - - expect(bodyOf(source).at(-2)).toMatch(/ A reply that begins with any other word is not taken\. If nobody/u); - expect(overviewOf(source)).toContain(' n4 -.->|"any other word"| n3\n'); - }); - - it('draw no reply and refuse no word of a rule that reads no first word', () => { - const overview = overviewOf(threadDocument({ rule: ['reply:', ' note: text'] })); - - expect(overview).not.toContain('mermaid'); - expect(overview).not.toContain('not taken'); - expect(overview).toContain('reading the replies at least 1 minute apart. The whole reply is the note. If nobody'); - }); -}); diff --git a/capabilities/interaction/src/overview/request-overview.ts b/capabilities/interaction/src/overview/request-overview.ts deleted file mode 100644 index 061309ba9..000000000 --- a/capabilities/interaction/src/overview/request-overview.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { - answerSection, - answerSentence, - answerWaitSentence, - inboxSentence, - inputDetails, - messageLead, - notTakenSentence, - notificationSentence, - paragraph, - replyDiagram, - replyRuleSentence, - replyWaitSentence, - unansweredSentence, - whereSection, - type OverviewBlock, - type Phrases, - type ToldParts, -} from '@on.auto/definitions'; -import { quotedTemplate } from '@on.auto/definitions/template'; -import type { Schema } from 'effect'; - -import { replyWaitMsOf } from '../conversations/cadence.ts'; -import type { RequestDocument } from '../document/interaction-document.ts'; -import type { ReplyRule } from '../replies/reply-rule.ts'; -import type { WrittenRoute } from '../tool-blocks/compiled-tool-block.ts'; -import type { Replies } from '../tool-blocks/tool-block-schemas.ts'; -import { partyOf } from './request-party.ts'; - -type Words = Readonly>; - -interface ReadReplies { - readonly route: WrittenRoute; - readonly replies: Replies; - readonly reply: ReplyRule; - readonly answer: Schema.JsonObject; -} - -function verdictsOf(reply: ReplyRule): Words | undefined { - for (const part of Object.values(reply)) { - if (part.from === 'word') { - return part.words; - } - } - return undefined; -} - -function readRepliesOf({ route, reply, output }: RequestDocument): ReadReplies | undefined { - const answer = output.schema?.document; - return route?.replies === undefined || reply === undefined || answer === undefined - ? undefined - : { route, replies: route.replies, reply, answer }; -} - -function readingSentences({ replies, reply, answer }: ReadReplies, expiresMs: number): readonly string[] { - return [ - replyWaitSentence(expiresMs, replyWaitMsOf(replies)), - replyRuleSentence(reply, answer), - ...(verdictsOf(reply) === undefined ? [] : [notTakenSentence(replies.tell !== undefined)]), - unansweredSentence(expiresMs), - ]; -} - -function waitingSentences(document: RequestDocument): readonly string[] { - const { route, output, expiresMs } = document; - const read = readRepliesOf(document); - if (read !== undefined) { - return readingSentences(read, expiresMs); - } - if (output.schema === undefined) { - return [route === undefined ? inboxSentence('nothing') : notificationSentence]; - } - return [route === undefined ? inboxSentence('answer') : answerWaitSentence(expiresMs), unansweredSentence(expiresMs)]; -} - -function diagramOf(document: RequestDocument, party: Phrases): readonly OverviewBlock[] { - const read = readRepliesOf(document); - const verdicts = read === undefined ? undefined : verdictsOf(read.reply); - if (read === undefined || verdicts === undefined) { - return []; - } - return [ - replyDiagram({ - input: document.input.schema?.document, - party, - server: read.route.deliver.server, - expiresMs: document.expiresMs, - verdicts, - told: read.replies.tell !== undefined, - }), - ]; -} - -function whereOf({ to, from, route, expires }: RequestDocument): ReturnType { - const replies = route?.replies; - return whereSection({ - to: to.text, - from: from?.text, - route: - route === undefined - ? undefined - : { - server: route.deliver.server, - tool: route.deliver.tool, - replies: replies === undefined ? undefined : { tool: replies.tool, wait: replies.wait }, - }, - expires, - }); -} - -export function requestOverview(document: RequestDocument): ToldParts { - const input = document.input.schema?.document; - const answer = document.output.schema?.document; - const party = partyOf(document.to, input); - return { - body: [ - ...diagramOf(document, party), - messageLead({ input, party, server: document.route?.deliver.server }), - ...quotedTemplate(document.message, input), - paragraph(waitingSentences(document).join(' ')), - ...(answer === undefined ? [] : [answerSentence(answer)]), - ], - appendix: [inputDetails(input), answerSection(answer), whereOf(document)], - }; -} diff --git a/capabilities/interaction/src/overview/request-party.ts b/capabilities/interaction/src/overview/request-party.ts deleted file mode 100644 index c2fabd05b..000000000 --- a/capabilities/interaction/src/overview/request-party.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { partyNamedUnderDetails, valueMeaning, type Phrases } from '@on.auto/definitions'; -import type { ParsedTemplate } from '@on.auto/definitions/template'; -import type { Schema } from 'effect'; - -const oneOutput = /^\{\{[^{}]*\}\}$/u; - -const liquid = /\{[{%]/u; - -export function partyOf({ text, variables }: ParsedTemplate, inputSchema: Schema.JsonObject | undefined): Phrases { - const written = text.trim(); - const [only, ...others] = variables; - if (oneOutput.test(written) && only !== undefined && others.length === 0) { - return [valueMeaning(only.path, inputSchema)]; - } - return liquid.test(written) || written.includes('\n') ? [partyNamedUnderDetails] : [written]; -} diff --git a/capabilities/interaction/src/testing/index.ts b/capabilities/interaction/src/testing/index.ts index c7646700c..9b3ef40a1 100644 --- a/capabilities/interaction/src/testing/index.ts +++ b/capabilities/interaction/src/testing/index.ts @@ -14,7 +14,6 @@ export { notificationDocument, replyRuleLines, threadDocument, - threadReplies, type ThreadOptions, } from './documents.ts'; export { callDocument, repliesOutput, type CallOptions } from './call-documents.ts'; diff --git a/capabilities/prediction/README.md b/capabilities/prediction/README.md index 1d1b15022..95bea8a03 100644 --- a/capabilities/prediction/README.md +++ b/capabilities/prediction/README.md @@ -8,11 +8,11 @@ User documentation is [Prediction function format](../../docs/reference/predicti ## The document -`parsePredictionDocument(source)` reads a document with the shared reader of [`@on.auto/definitions/document`](../../packages/definitions/README.md#reading-function-documents) and gives a `PredictionFunctionDefinitionDocument`: the optional `description` and `about`, the latter bounded by `AboutSchema` of `@on.auto/definitions/document`, `predicts`, `data`, `evaluation`, `source` and `rules`, the brief compiled as a Liquid template, the digest of the data part, `dataSha256`, the `data` expressions of the filters with their pointers and lines, and the derived schemas. The front matter's keys are `description`, `about`, `predicts`, `data`, `evaluation`, `source` and `rules`; every other key, `input` and `output` among them, is an issue at its line. Each part is checked in its own module (`src/document/predicts-part.ts`, `evaluation-part.ts`, `source-part.ts`, `src/data/data-part.ts`), and every problem is reported at once, each at the line of the value it names: a repeated horizon, a history that is not a whole number from 0 to 500 or that is past the window, a holdout or a window outside the bounds the horizons and the season set, `seasonal-naive` without a season, a `given` name that is not a feature, a feature or the target named `series` or `period`, a feature named as the target, a `time` or `series` pointing where none of the named event types holds a value, `count` with `of` or another aggregation without it, `to` beside `through`, a rule past its bounds. The brief (`src/brief`) reads `dataset` and `function` alone: parsing compiles it and refuses a brief that does not compile and a name outside them at their lines; the compile is cheap and deterministic, held by the template engine's bounds on characters and names and by no clock, 0.11 ms for the example's brief and 4.7 ms for one of 65,535 characters at the median, so every `prepare` of a run pays it. Rendering is the check's, below, since its 200 ms bound is a clock's. +`parsePredictionDocument(source)` reads a document with the shared reader of [`@on.auto/definitions/document`](../../packages/definitions/README.md#reading-function-documents) and gives a `PredictionFunctionDefinitionDocument`: the optional `description`, `predicts`, `data`, `evaluation`, `source` and `rules`, the brief compiled as a Liquid template, the digest of the data part, `dataSha256`, the `data` expressions of the filters with their pointers and lines, and the derived schemas. The front matter's keys are `description`, `predicts`, `data`, `evaluation`, `source` and `rules`; every other key, `input` and `output` among them, is an issue at its line. Each part is checked in its own module (`src/document/predicts-part.ts`, `evaluation-part.ts`, `source-part.ts`, `src/data/data-part.ts`), and every problem is reported at once, each at the line of the value it names: a repeated horizon, a history that is not a whole number from 0 to 500 or that is past the window, a holdout or a window outside the bounds the horizons and the season set, `seasonal-naive` without a season, a `given` name that is not a feature, a feature or the target named `series` or `period`, a feature named as the target, a `time` or `series` pointing where none of the named event types holds a value, `count` with `of` or another aggregation without it, `to` beside `through`, a rule past its bounds. The brief (`src/brief`) reads `dataset` and `function` alone: parsing compiles it and refuses a brief that does not compile and a name outside them at their lines; the compile is cheap and deterministic, held by the template engine's bounds on characters and names and by no clock, 0.11 ms for the example's brief and 4.7 ms for one of 65,535 characters at the median, so every `prepare` of a run pays it. Rendering is the check's, below, since its 200 ms bound is a clock's. The digest of the data part, `dataSha256`, is the SHA-256 of the canonical JSON of what the rows of its dataset depend on: the data part without its `window` and its `given`, with the grain beside it (`src/data/data-digest.ts`), since a row is folded from the filters, the pointers and the aggregations at the grain, and neither the window a build reads nor the features a question supplies changes one. Two functions whose rows are folded alike share one dataset, and an edit that widens the window or names other given features rebuilds nothing. -`summarize` answers the description, the about, which the overview writes after it, the input schema, the series shape as the output schema, and the `details` the registry keeps with the definition, through `publishedOutputOf` of `@on.auto/definitions`, as a reasoning function that declares `output.shape: series` publishes them: the details hold `shape: series`, then `data_sha256` and `data`, `data` being what the digest covers, so the host's projector folds a dataset without this package's parser, and `calls`, the interaction functions it calls, its trainer `source.through` with the role `trainer` and then the function of `evaluation.activate.approved` with the role `approver`, each when the document names it, so the overview of each says the prediction function uses it. The input schema is `oneOf` a question, `{ series, horizon?, given, marks? }` with `series` text of 1 to 256 bytes, whose `maxLength` of 256 a run's own byte count completes, `horizon` one of the horizons and none with `accumulates`, `given` required when the document lists it and holding those names alone, and `marks` the series shape's own `seriesMarksSchema`, at most 8, and a build, `{ build: {} }` (`src/document/derived-schemas.ts`). A build's outcome, `{ version, winner, candidates }`, is no series, so the output schema says the answer of a question, the run an agent reads a series from, and the format guide says the build's. +`summarize` answers the description, the input schema, the series shape as the output schema, and the `details` the registry keeps with the definition, through `publishedOutputOf` of `@on.auto/definitions`, as a reasoning function that declares `output.shape: series` publishes them: the details hold `shape: series`, then `data_sha256` and `data`, `data` being what the digest covers, so the host's projector folds a dataset without this package's parser, and `calls`, the interaction functions it calls, its trainer `source.through` with the role `trainer` and then the function of `evaluation.activate.approved` with the role `approver`, each when the document names it, so the read of each says in `used_by` that the prediction function uses it. The input schema is `oneOf` a question, `{ series, horizon?, given, marks? }` with `series` text of 1 to 256 bytes, whose `maxLength` of 256 a run's own byte count completes, `horizon` one of the horizons and none with `accumulates`, `given` required when the document lists it and holding those names alone, and `marks` the series shape's own `seriesMarksSchema`, at most 8, and a build, `{ build: {} }` (`src/document/derived-schemas.ts`). A build's outcome, `{ version, winner, candidates }`, is no series, so the output schema says the answer of a question, the run an agent reads a series from, and the format guide says the build's. The capability's `check` (`src/checks/prediction-check.ts`), which runs at a save and never at a run, renders the brief over the largest dataset the bounds admit (`src/brief/brief-dataset.ts`) and refuses, at its line, a brief that writes a list or a mapping where text goes, reads an item the largest dataset does not have, renders past 64 KiB or takes longer than the template engine's 200 ms to render. It reads the other definitions of the playbook through the `SavedDefinitions` port of `@on.auto/definitions`, which answers the active version of a definition with its schemas: an approver named under `evaluation.activate.approved` is an active interaction function whose input schema takes `{ function, version, card }` and whose output schema is an object whose one required property `decision` is `approve` or `reject`; a function named under `source.through` is an active interaction function whose input schema takes `{ build: {} }` and whose output schema is an object whose one required property `candidates` is a list of text (`src/checks/called-functions.ts`). A function that names an uploaded dataset is checked against what the port's `content(sha256)` answers (`src/checks/uploaded-datasets.ts`): the playbook keeps content under the digest, as `text/csv`, whose header and first row (`src/upload/kept-head.ts`) are of the grain `predicts.period` names and name `predicts.target` and then the features in `data.features`' order; each fault is refused at its line, so the standing never names a digest that was never uploaded. The filters' `data` expressions are then checked by the pool's check worker against `$data`, as a recall function's are. @@ -137,4 +137,4 @@ The tests of the scoring keep the 1.5 s of a walk in `src/scoring/scoring-runs.t ## Source -`src/index.ts` is the entry point and `src/testing/index.ts` the entry point of the test support. `src/document` holds the document: its type, the front matter, the parts and parsing, and the derived schemas. `src/data` holds the data part, its filters, pointers, measures and digest, and `src/brief` the brief's variables, template and largest dataset. `src/periods` holds the calendar: civil days, grains, enclosing periods and the moments an event's time is read as. `src/dataset` holds the folder, its tables and statements, the fold and the port; `src/files` the files a build reads and their streaming parse; `src/upload` the upload's operation, its CSV reader and grammar, and the reading of a kept dataset's head. `src/capability` holds the capability, its ports, its bounds, its words and the figures they print, and its guide, the public reference page, served to agents as `prediction-function`; `src/overview` its overview (see [The overview of a definition](../../packages/definitions/README.md#the-overview-of-a-definition)): what it predicts, with its answer in the words `seriesPathBlocks` of `@on.auto/definitions` gives a series it knows, in the period nouns of that module, what it learns from, each filter of a projected dataset after `each time` in the words `eventFilterPhrases` of `@on.auto/definitions` gives every event filter, who trains it, with the brief read from the text its parsed template keeps with the line it starts on, by `readableTemplate` of `@on.auto/definitions/template`, how a version goes live and its rules, of which `reference-example.overview.md` and `reference-uploaded.overview.md` are the overviews of the example of the public reference page and of that example over its uploaded dataset, which `prediction-overview.test.ts` writes again and compares byte for byte; `src/checks` its checks at save, of the functions it calls and of an uploaded dataset, and of a build's answer; and `src/standing` the standing `get_definition` shows. `src/run` holds a run, its two kinds and its lengths; `src/build` a build's start, its refusals, its files, its brief and its request; `src/trainer` the trainer's routes, a candidate's checks and its staging; `src/facts` the facts of a build and of the models; `src/builds` the builds projection, its due work, a build's state and the document it was built from; `src/build-work` the steps of that work, from the request's child to a run's end and the watch of a walk's run; `src/scoring` the scoring grid, the metrics, the baselines, the bootstrap, the verdicts, the ending and the card; `src/models` the models stream's decider, the claims of builds, its state, its projection and the activation operation; `src/shadow` the shadow's schedule, predictions, truths, grades and the activation that follows; and `src/question` a question and the version it asks. `src/model-processes` holds the model processes, their ports and the scoring permits; `src/model-jobs` their module and jobs, the sessions, the feeds, the opset, the walk and the window; and `src/model-cache` the file caches, their folder's checks and the parsed rows. `src/dataset-testing` and `src/build-testing` hold the suites and the harness the tests share, and `candidates/` the candidate models and their writer. +`src/index.ts` is the entry point and `src/testing/index.ts` the entry point of the test support. `src/document` holds the document: its type, the front matter, the parts and parsing, and the derived schemas. `src/data` holds the data part, its filters, pointers, measures and digest, and `src/brief` the brief's variables, template and largest dataset. `src/periods` holds the calendar: civil days, grains, enclosing periods and the moments an event's time is read as. `src/dataset` holds the folder, its tables and statements, the fold and the port; `src/files` the files a build reads and their streaming parse; `src/upload` the upload's operation, its CSV reader and grammar, and the reading of a kept dataset's head. `src/capability` holds the capability, its ports, its bounds, its words and the figures they print, and its guide, the public reference page, served to agents as `prediction-function`; `src/checks` its checks at save, of the functions it calls and of an uploaded dataset, and of a build's answer; and `src/standing` the standing `get_definition` shows. `src/run` holds a run, its two kinds and its lengths; `src/build` a build's start, its refusals, its files, its brief and its request; `src/trainer` the trainer's routes, a candidate's checks and its staging; `src/facts` the facts of a build and of the models; `src/builds` the builds projection, its due work, a build's state and the document it was built from; `src/build-work` the steps of that work, from the request's child to a run's end and the watch of a walk's run; `src/scoring` the scoring grid, the metrics, the baselines, the bootstrap, the verdicts, the ending and the card; `src/models` the models stream's decider, the claims of builds, its state, its projection and the activation operation; `src/shadow` the shadow's schedule, predictions, truths, grades and the activation that follows; and `src/question` a question and the version it asks. `src/model-processes` holds the model processes, their ports and the scoring permits; `src/model-jobs` their module and jobs, the sessions, the feeds, the opset, the walk and the window; and `src/model-cache` the file caches, their folder's checks and the parsed rows. `src/dataset-testing` and `src/build-testing` hold the suites and the harness the tests share, and `candidates/` the candidate models and their writer. diff --git a/capabilities/prediction/measure/order-history.ts b/capabilities/prediction/measure/order-history.ts index a00f667e2..a87991573 100644 --- a/capabilities/prediction/measure/order-history.ts +++ b/capabilities/prediction/measure/order-history.ts @@ -1,4 +1,3 @@ -import { overviewTitled } from '@on.auto/definitions/testing'; import type { ProjectedFolders } from '@on.auto/workflow-host/testing'; import { Result } from 'effect'; @@ -94,7 +93,7 @@ export function savedIn(folders: ProjectedFolders, { name, source, document }: M [ { type: 'definition_created', - data: { content: { source, overview: overviewTitled(name), details: datasetDetailsOf(document) } }, + data: { content: { source, details: datasetDetailsOf(document) } }, }, ], { diff --git a/capabilities/prediction/src/capability/prediction-definitions.ts b/capabilities/prediction/src/capability/prediction-definitions.ts index 561c84160..36da2a291 100644 --- a/capabilities/prediction/src/capability/prediction-definitions.ts +++ b/capabilities/prediction/src/capability/prediction-definitions.ts @@ -53,11 +53,10 @@ function callsOf({ source, evaluation: { activate } }: PredictionFunctionDefinit } export function summarize(document: PredictionFunctionDefinitionDocument): DefinitionSummary { - const { description, about, schemas } = document; + const { description, schemas } = document; const calls = callsOf(document); return { ...(description === undefined ? {} : { description }), - ...(about === undefined ? {} : { about }), inputSchema: schemas.input, ...publishedOutputOf({ shape: seriesShape, diff --git a/capabilities/prediction/src/capability/prediction-function.ts b/capabilities/prediction/src/capability/prediction-function.ts index 51af944b7..58a15b6d3 100644 --- a/capabilities/prediction/src/capability/prediction-function.ts +++ b/capabilities/prediction/src/capability/prediction-function.ts @@ -7,7 +7,6 @@ import { import { predictionCheck } from '../checks/prediction-check.ts'; import { predictionFacts } from '../facts/prediction-facts.ts'; -import { predictionOverview } from '../overview/prediction-overview.ts'; import { predictionRunsOn } from '../run/prediction-runs.ts'; import { predictionStanding } from '../standing/prediction-standing.ts'; import { predictionBounds, predictionDefinitionType } from './prediction-bounds.ts'; @@ -31,7 +30,6 @@ export function makePredictionFunctionAdapter({ parse, check: predictionCheck(pool), summarize, - overview: predictionOverview, ...predictionRunsOn(ports), facts: predictionFacts, mostActive: mostFunctions, diff --git a/capabilities/prediction/src/dataset-testing/projected-datasets.ts b/capabilities/prediction/src/dataset-testing/projected-datasets.ts index bdf1d7205..1547624a8 100644 --- a/capabilities/prediction/src/dataset-testing/projected-datasets.ts +++ b/capabilities/prediction/src/dataset-testing/projected-datasets.ts @@ -1,4 +1,3 @@ -import { overviewTitled } from '@on.auto/definitions/testing'; import { filterSandboxOf } from '@on.auto/workflow-engine/dsl'; import { someFolder, type DatabaseSettings } from '@on.auto/workflow-host'; import { projectedFolders, type ProjectedFolders } from '@on.auto/workflow-host/testing'; @@ -52,11 +51,7 @@ function definitionsOf(folders: ProjectedFolders): Pick { 'Line 3: The definition has no brief: write it after the front matter', ]); expect(problemsOf(weeklyOrdersWith(['source:', 'input:\n schema: {}\nsource:']))).toEqual([ - 'Line 24, /input: input is not a key of the front matter; it takes description, about, predicts, data, evaluation, source, rules', + 'Line 24, /input: input is not a key of the front matter; it takes description, predicts, data, evaluation, source, rules', ]); expect(problemsOf(weeklyOrdersWith([' period: week', ' period: fortnight']))).toEqual([periodRefused]); }); }); - -const descriptionLine = 'description: Predicts the orders of each region one and four weeks ahead'; - -describe('the about of a prediction function document', () => { - it('is kept as its author wrote it', () => { - const about = `${descriptionLine}\nabout: |\n The sales team plans the stock of each region with it.`; - - expect(documentOf(weeklyOrdersWith([descriptionLine, about])).about).toBe( - 'The sales team plans the stock of each region with it.\n', - ); - }); - - it('takes no heading, nor does the description, each refused at its line', () => { - const written = "description: '# Predicts the orders'\nabout: |\n The sales team plans with it.\n ==="; - - expect(problemsOf(weeklyOrdersWith([descriptionLine, written]))).toEqual([ - 'Line 2, /description: description takes no heading, since an overview writes its own: the line that begins “# Predicts the orders” makes one, with # before its words or as a line of = or - under the words above it', - 'Line 5, /about: about takes no heading, since an overview writes its own: this line makes one, with # before its words or as a line of = or - under the words above it', - ]); - }); -}); diff --git a/capabilities/prediction/src/document/document-parsing.ts b/capabilities/prediction/src/document/document-parsing.ts index eaad0f75c..8d524a639 100644 --- a/capabilities/prediction/src/document/document-parsing.ts +++ b/capabilities/prediction/src/document/document-parsing.ts @@ -23,7 +23,6 @@ function assembled( const { predicts, data, evaluation, source, rules, brief } = done; return { ...(front.description === undefined ? {} : { description: front.description }), - ...(front.about === undefined ? {} : { about: front.about }), predicts, data: data.data, dataSha256: dataDigestOf(data.data, predicts.period), diff --git a/capabilities/prediction/src/document/front-matter.ts b/capabilities/prediction/src/document/front-matter.ts index 1bcd8aea1..7ae6bc2b8 100644 --- a/capabilities/prediction/src/document/front-matter.ts +++ b/capabilities/prediction/src/document/front-matter.ts @@ -1,9 +1,4 @@ -import { - AboutSchema, - DescriptionSchema, - type FrontMatterSection, - type FrontMatterShape, -} from '@on.auto/definitions/document'; +import { DescriptionSchema, type FrontMatterSection, type FrontMatterShape } from '@on.auto/definitions/document'; import { Schema } from 'effect'; import { enclosings } from '../periods/enclosing.ts'; @@ -55,7 +50,6 @@ const SourceSchema = Schema.Struct({ const FrontMatterSchema = Schema.Struct({ description: Schema.optionalKey(DescriptionSchema), - about: Schema.optionalKey(AboutSchema), predicts: PredictsSchema, data: DataSchema, evaluation: EvaluationSchema, diff --git a/capabilities/prediction/src/document/prediction-document.ts b/capabilities/prediction/src/document/prediction-document.ts index eed4627f5..abd9fbd4c 100644 --- a/capabilities/prediction/src/document/prediction-document.ts +++ b/capabilities/prediction/src/document/prediction-document.ts @@ -84,7 +84,6 @@ export interface DerivedSchemas { export interface PredictionFunctionDefinitionDocument { readonly description?: string; - readonly about?: string; readonly predicts: Predicts; readonly data: DataPart; readonly dataSha256: string; diff --git a/capabilities/prediction/src/overview/data-words.ts b/capabilities/prediction/src/overview/data-words.ts deleted file mode 100644 index 181a0abc9..000000000 --- a/capabilities/prediction/src/overview/data-words.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { - bulletList, - eventFilterPhrases, - named, - paragraph, - sectionOf, - type OverviewBlock, - type OverviewSection, - type Phrases, -} from '@on.auto/definitions'; - -import { - isProjected, - type Aggregation, - type Feature, - type Measure, - type PredictionFunctionDefinitionDocument, - type ProjectedData, -} from '../document/prediction-document.ts'; -import { askedPeriods, periodsOf } from './predicts-words.ts'; - -const measureWords: Readonly, string>> = { - sum: 'the sum of ', - last: 'the last value of ', - mean: 'the mean of ', -}; - -function measurePhrases({ of, as }: Measure): Phrases { - return of === undefined || as === 'count' ? ['the number of its events'] : [measureWords[as], { code: of }]; -} - -function givenPhrases(name: string, given: readonly string[], asked: string): Phrases { - return given.includes(name) ? [`, which a question gives for ${asked}`] : []; -} - -function featurePhrases({ name, ...measure }: Feature, given: readonly string[], asked: string): Phrases { - return [named(name), ', ', ...measurePhrases(measure), ...givenPhrases(name, given, asked)]; -} - -function timePhrases(time: string, period: string): Phrases { - return time === 'event' - ? [`the ${period}, from the time of each event`] - : [`the ${period}, from `, { code: time }, " in each event's data"]; -} - -function projectedBlocks( - { events, time, series, target, features, given }: ProjectedData, - { predicts }: PredictionFunctionDefinitionDocument, -): readonly OverviewBlock[] { - const asked = askedPeriods(predicts); - return [ - paragraph('It learns from the rows the playbook folds from the first event of its history on:'), - bulletList(events.map((filter) => ['each time ', ...eventFilterPhrases(filter)])), - paragraph(`Each row is one series and one ${predicts.period}, and holds:`), - bulletList([ - timePhrases(time, predicts.period), - ['the series, from ', { code: series }], - [named(predicts.target), ', ', ...measurePhrases(target)], - ...features.map((feature) => featurePhrases(feature, given, asked)), - ]), - ]; -} - -function uploadedBlocks( - dataset: string, - features: readonly string[], - { predicts, data }: PredictionFunctionDefinitionDocument, -): readonly OverviewBlock[] { - const asked = askedPeriods(predicts); - const opening = paragraph( - 'It learns from the dataset uploaded to the playbook as ', - { code: dataset }, - `, one row a series and a ${predicts.period}, with `, - named(predicts.target), - features.length === 0 ? ' alone.' : ' and then these columns:', - ); - return features.length === 0 - ? [opening] - : [opening, bulletList(features.map((name) => [named(name), ...givenPhrases(name, data.given, asked)]))]; -} - -export function learnedSection(document: PredictionFunctionDefinitionDocument): OverviewSection { - const { data, predicts } = document; - const blocks = isProjected(data) - ? projectedBlocks(data, document) - : uploadedBlocks(data.dataset, data.features, document); - return sectionOf( - 'What it learns from', - ...blocks, - paragraph(`A build reads the last ${periodsOf(predicts.period, data.window)}.`), - ); -} diff --git a/capabilities/prediction/src/overview/evaluation-words.ts b/capabilities/prediction/src/overview/evaluation-words.ts deleted file mode 100644 index 4902741ca..000000000 --- a/capabilities/prediction/src/overview/evaluation-words.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { - listedPhrases, - named, - paragraph, - sectionOf, - type OverviewBlock, - type OverviewSection, -} from '@on.auto/definitions'; - -import { - isProjected, - type Baseline, - type Evaluation, - type PredictionFunctionDefinitionDocument, -} from '../document/prediction-document.ts'; -import { periodsOf } from './predicts-words.ts'; - -const metricWords: Readonly> = { - mase: 'its mean absolute scaled error', - winkler: 'the Winkler score of its interval', - quantile: 'the quantile loss of its percentiles', -}; - -const baselineWords: Readonly> = { - persistence: 'the last value carried forward', - 'seasonal-naive': 'the value one season earlier', -}; - -const share = new Intl.NumberFormat('en', { style: 'percent', maximumFractionDigits: 2 }); - -function shadowBlocks({ data, predicts, evaluation }: PredictionFunctionDefinitionDocument): readonly OverviewBlock[] { - const { shadowPeriods } = evaluation; - return shadowPeriods === 0 || !isProjected(data) - ? [] - : [ - paragraph( - `A version after the first runs beside the active one for ${periodsOf(predicts.accumulates ?? predicts.period, shadowPeriods)}, and takes its place only when it predicts no worse.`, - ), - ]; -} - -function activationBlock({ activate }: Evaluation): OverviewBlock { - return activate === 'automatic' - ? paragraph('A version that passes becomes the active one on its own.') - : paragraph( - 'A version that passes becomes the active one once it is approved through the interaction function ', - named(activate.approved), - '.', - ); -} - -export function goingLiveSection(document: PredictionFunctionDefinitionDocument): OverviewSection { - const { evaluation, predicts } = document; - const { holdout, metric, coverageFloor, baselines, confidence } = evaluation; - const beaten = baselines.map((baseline) => [named(baseline), ` (${baselineWords[baseline]})`]); - return sectionOf( - 'How a version goes live', - paragraph( - `The playbook holds back the last ${periodsOf(predicts.period, holdout)} of the window from the trainer, and scores each candidate on them by ${metricWords[metric]}.`, - ), - paragraph( - `A candidate passes when its interval holds at least ${share.format(coverageFloor)} of the truths and it beats `, - ...listedPhrases(beaten, 'and'), - ` with ${share.format(confidence)} confidence; the best that passes becomes the next version.`, - ), - ...shadowBlocks(document), - activationBlock(evaluation), - ); -} diff --git a/capabilities/prediction/src/overview/prediction-overview.test.ts b/capabilities/prediction/src/overview/prediction-overview.test.ts deleted file mode 100644 index ed9fdbb23..000000000 --- a/capabilities/prediction/src/overview/prediction-overview.test.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { readFileSync } from 'node:fs'; - -import { markdownOf } from '@on.auto/definitions'; -import { sectionIn } from '@on.auto/definitions/testing'; -import { describe, expect, it } from 'vitest'; - -import { dailyTotalsWith, dailyVisitsWith, uploadedDailyVisits } from '../testing/daily-documents.ts'; -import { predictionRuns } from '../testing/prediction-runs.ts'; -import { weeklyOrdersLines, weeklyOrdersWith } from '../testing/weekly-orders.ts'; - -const page = readFileSync(new URL('../../../../docs/reference/prediction-format.md', import.meta.url), 'utf8'); - -const [example = '', , dataPart = ''] = [...page.matchAll(/```(?:markdown|text|yaml)\n([\s\S]*?)```/gu)].map( - ([, body = '']: readonly string[]) => body, -); - -const sha256 = '3f2ac1d0e4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d'; - -function goldenOf(name: string): string { - return readFileSync(new URL(name, import.meta.url), 'utf8'); -} - -function overviewOf(source: string): string { - return markdownOf(predictionRuns().prepared(source).overview('weekly-orders')); -} - -function withDataPart(data: string): string { - const from = weeklyOrdersLines.indexOf('data:'); - const to = weeklyOrdersLines.indexOf('evaluation:'); - return [...weeklyOrdersLines.slice(0, from), data.trimEnd(), ...weeklyOrdersLines.slice(to)].join('\n'); -} - -describe('the overview of the examples of the reference of prediction functions', () => { - it('is, byte for byte, the overview pinned beside this test, for the example and over its uploaded dataset', () => { - expect(overviewOf(example)).toBe(goldenOf('reference-example.overview.md')); - expect(overviewOf(withDataPart(dataPart))).toBe(goldenOf('reference-uploaded.overview.md')); - }); -}); - -describe('the overview of a prediction function of a total to the end of a period', () => { - const overview = overviewOf( - dailyTotalsWith( - [ - ' target: { of: /amount, as: sum }', - ' target: { of: /amount, as: sum }\n features:\n - { name: promo, of: /promo, as: last }\n given: [promo]', - ], - [' activate: automatic', ' shadow_periods: 2\n activate: automatic'], - ), - ); - - it('says the total it predicts, its season, and the given value a question names for the periods it sums', () => { - expect(sectionIn(overview, 'What it predicts')).toBe( - [ - 'It predicts the total of **sales**, counted in sales, for each series from the day after the last one closed to the end of the week that day falls in.', - 'A season is 7 days long.', - 'A question names a series, with the value of **promo** planned for the days it sums.', - 'It answers the last 14 observed days and the total from the next day to the end of its week, with a likely value, a low and a high.', - 'The likely value is the 50th percentile of its model, the low the 10th and the high the 90th.', - ].join('\n\n'), - ); - }); - - it('dates each event by its own time, sums its target, and runs a new version in a shadow of whole periods summed', () => { - expect(sectionIn(overview, 'What it learns from')).toContain( - '- the day, from the time of each event\n- the series, from `/store`\n- **sales**, the sum of `/amount`\n- **promo**, the last value of `/promo`, which a question gives for the days it sums', - ); - expect(sectionIn(overview, 'How a version goes live')).toContain( - 'A version after the first runs beside the active one for 2 weeks, and takes its place only when it predicts no worse.', - ); - }); -}); - -const sentThrough = dailyVisitsWith( - [' horizons: [1, 2]', ' horizons: [1]\n history: 1'], - [ - ' - type: visit.made', - " - { type: visit.made, source: shop, data: '${ $data.paid }' }\n - { type: visit.counted, data: { kind: walk-in } }", - ], - [ - ' activate: automatic', - ' metric: quantile\n baselines: [persistence]\n coverage_floor: 0.75\n confidence: 0.95\n shadow_periods: 1\n activate: { approved: approve-version }', - ], - [' expires: P1D', ' expires: PT12H\n through: send-build'], - [ - 'Fit a model of the daily {{ function.target }} of each of {{ dataset.series }} stores.', - '{% comment %}Kept for the authors of the function.{% endcomment %}', - ], -); - -describe('the overview of a prediction function sent to its trainer and approved through interaction functions', () => { - const overview = overviewOf(sentThrough); - - it('predicts one period ahead, answers the history it names, and names the attributes and the data test of each filter', () => { - expect(sectionIn(overview, 'What it predicts').split('\n\n').slice(0, 3)).toEqual([ - 'It predicts **visits**, counted in visits, for each series a day at a time, 1 day ahead.', - 'A question names a series, and one horizon when it asks for that one alone.', - 'It answers the last observed day and 1 day ahead, with a likely value, a low and a high.', - ]); - expect(sectionIn(overview, 'What it learns from')).toContain( - [ - '- each time an event `visit.made` is published to the playbook, from the source `shop` and when `$data.paid` holds for its data', - '- each time an event `visit.counted` is published to the playbook, when its data is exactly `{"kind":"walk-in"}`', - ].join('\n'), - ); - }); - - it('names the function a build goes through, its wait, and that its brief holds no text a trainer reads', () => { - expect(sectionIn(overview, 'Who trains it')).toBe( - [ - 'Each build asks for candidate models through the interaction function **send-build**.', - 'A build waits at most 12 hours for its candidates, and the playbook then scores up to 8 of them, each within 10 minutes.', - 'Its brief holds no text a trainer reads, so its trainer reads the rows and the rules alone.', - ].join('\n\n'), - ); - }); - - it('says how candidates are scored and that a version goes live once it is approved, and states no rules', () => { - expect(sectionIn(overview, 'How a version goes live')).toBe( - [ - 'The playbook holds back the last 8 days of the window from the trainer, and scores each candidate on them by the quantile loss of its percentiles.', - 'A candidate passes when its interval holds at least 75% of the truths and it beats **persistence** (the last value carried forward) with 95% confidence; the best that passes becomes the next version.', - 'A version after the first runs beside the active one for 1 day, and takes its place only when it predicts no worse.', - 'A version that passes becomes the active one once it is approved through the interaction function **approve-version**.\n', - ].join('\n\n'), - ); - expect(overview).not.toContain('## Rules'); - }); -}); - -describe('the overview of a prediction function over an uploaded dataset of its target alone', () => { - it('names the dataset and its target, and runs no shadow, since the dataset gains no rows', () => { - const overview = overviewOf( - uploadedDailyVisits(sha256).replace( - ' activate: automatic', - ' shadow_periods: 3\n metric: winkler\n activate: automatic', - ), - ); - - expect(sectionIn(overview, 'What it learns from')).toBe( - [ - `It learns from the dataset uploaded to the playbook as \`${sha256}\`, one row a series and a day, with **visits** alone.`, - 'A build reads the last 24 days.', - ].join('\n\n'), - ); - expect(sectionIn(overview, 'How a version goes live')).not.toContain('beside the active one'); - expect(sectionIn(overview, 'How a version goes live')).toContain('by the Winkler score of its interval.'); - }); -}); - -describe('the overview of a prediction function whose document has an about', () => { - it('writes the about as its author wrote it, after the description and before what it predicts', () => { - const description = 'description: Predicts the orders of each region one and four weeks ahead'; - const about = [ - 'about: |', - ' The sales team plans the stock of each region with it.', - '', - ' A person approves each version before it answers.', - ]; - - expect( - overviewOf(weeklyOrdersWith([description, [description, ...about].join('\n')])) - .split('\n\n') - .slice(0, 5), - ).toEqual([ - '# weekly-orders', - 'Predicts the orders of each region one and four weeks ahead', - 'The sales team plans the stock of each region with it.', - 'A person approves each version before it answers.', - '## What it predicts', - ]); - }); -}); diff --git a/capabilities/prediction/src/overview/prediction-overview.ts b/capabilities/prediction/src/overview/prediction-overview.ts deleted file mode 100644 index 42b288cf3..000000000 --- a/capabilities/prediction/src/overview/prediction-overview.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { bulletList, paragraph, sectionOf, type OverviewParts, type OverviewSection } from '@on.auto/definitions'; - -import type { PredictionFunctionDefinitionDocument } from '../document/prediction-document.ts'; -import { learnedSection } from './data-words.ts'; -import { goingLiveSection } from './evaluation-words.ts'; -import { predictedSection } from './predicts-words.ts'; -import { trainerSection } from './trainer-words.ts'; - -function rulesSections(rules: readonly string[]): readonly OverviewSection[] { - return rules.length === 0 - ? [] - : [ - sectionOf( - 'Rules', - paragraph('Its trainer follows these rules, and the card of each version states them:'), - bulletList(rules.map((rule) => [rule])), - ), - ]; -} - -export function predictionOverview(document: PredictionFunctionDefinitionDocument): OverviewParts { - return { - body: [], - sections: [ - predictedSection(document.predicts, document.data), - learnedSection(document), - trainerSection(document.source, document.brief), - goingLiveSection(document), - ...rulesSections(document.rules), - ], - appendix: [], - }; -} diff --git a/capabilities/prediction/src/overview/predicts-words.ts b/capabilities/prediction/src/overview/predicts-words.ts deleted file mode 100644 index 9f9414b58..000000000 --- a/capabilities/prediction/src/overview/predicts-words.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { - listedPhrases, - named, - paragraph, - periodNouns, - sectionOf, - seriesPathBlocks, - type OverviewSection, - type Phrases, -} from '@on.auto/definitions'; -import { listed } from '@on.auto/operations'; - -import { figure } from '../capability/figures.ts'; -import type { DataPart, Predicts } from '../document/prediction-document.ts'; -import type { Grain } from '../periods/grains.ts'; - -export function periodsOf(period: Grain, count: number): string { - const { one, other } = periodNouns[period]; - return `${figure(count)} ${count === 1 ? one : other}`; -} - -export function askedPeriods({ period, accumulates }: Predicts): string { - const { other } = periodNouns[period]; - return accumulates === undefined ? `the ${other} ahead` : `the ${other} it sums`; -} - -function predictedWords({ target, period, horizons, accumulates, unit }: Predicts): Phrases { - if (accumulates !== undefined) { - return [ - 'It predicts the total of ', - named(target), - `, counted in ${unit}, for each series from the ${period} after the last one closed to the end of the ${accumulates} that ${period} falls in.`, - ]; - } - const { one, other } = periodNouns[period]; - const ahead = horizons.length === 1 && horizons[0] === 1 ? one : other; - return [ - 'It predicts ', - named(target), - `, counted in ${unit}, for each series a ${period} at a time, ${listed(horizons.map((horizon) => figure(horizon)))} ${ahead} ahead.`, - ]; -} - -function seasonWords({ period, season }: Predicts): readonly Phrases[] { - return season === undefined ? [] : [[`A season is ${periodsOf(period, season)} long.`]]; -} - -function questionWords(predicts: Predicts, { given }: DataPart): Phrases { - const asked = - predicts.accumulates === undefined - ? 'A question names a series, and one horizon when it asks for that one alone' - : 'A question names a series'; - const planned = - given.length === 0 - ? [] - : [ - ', with the value of ', - ...listedPhrases( - given.map((name) => [named(name)]), - 'and', - ), - ` planned for ${askedPeriods(predicts)}`, - ]; - return [asked, ...planned, '.']; -} - -function answerBlocks({ period, history, horizons, accumulates }: Predicts) { - return seriesPathBlocks({ - period, - observed: history, - ahead: accumulates === undefined ? { periods: horizons } : { totalTo: accumulates }, - }); -} - -export function predictedSection(predicts: Predicts, data: DataPart): OverviewSection { - return sectionOf( - 'What it predicts', - paragraph(...predictedWords(predicts)), - ...seasonWords(predicts).map((words) => paragraph(...words)), - paragraph(...questionWords(predicts, data)), - ...answerBlocks(predicts), - paragraph('The likely value is the 50th percentile of its model, the low the 10th and the high the 90th.'), - ); -} diff --git a/capabilities/prediction/src/overview/reference-example.overview.md b/capabilities/prediction/src/overview/reference-example.overview.md deleted file mode 100644 index 56a2cdea8..000000000 --- a/capabilities/prediction/src/overview/reference-example.overview.md +++ /dev/null @@ -1,62 +0,0 @@ -# weekly-orders - -Predicts the orders of each region one and four weeks ahead - -## What it predicts - -It predicts **orders**, counted in orders, for each series a week at a time, 1 and 4 weeks ahead. - -A season is 52 weeks long. - -A question names a series, and one horizon when it asks for that one alone, with the value of **promotion** planned for the weeks ahead. - -It answers the last 8 observed weeks and 1 and 4 weeks ahead, each with a likely value, a low and a high. - -The likely value is the 50th percentile of its model, the low the 10th and the high the 90th. - -## What it learns from - -It learns from the rows the playbook folds from the first event of its history on: - -- each time an event `order.placed` is published to the playbook - -Each row is one series and one week, and holds: - -- the week, from `/placed_at` in each event's data -- the series, from `/region` -- **orders**, the number of its events -- **basket**, the mean of `/total` -- **promotion**, the last value of `/promotion`, which a question gives for the weeks ahead - -A build reads the last 156 weeks. - -## Who trains it - -Each build asks for candidate models in the playbook's inbox, as a request to **trainer**. - -A build waits at most 7 days for its candidates, and the playbook then scores up to 8 of them, each within 10 minutes. - -Its trainer reads the brief first, with the values of each build in place of the names in bold: - -Fit a model of the weekly **target** of each of **series** regions, in **unit**, -over **window** weeks from **first** to **last**. -Predict **horizons** weeks ahead. The features are -each feature of dataset.features as **name** (**as**), separated by commas. - -*For each rule of function.rules:* - -- **rule** - -## How a version goes live - -The playbook holds back the last 12 weeks of the window from the trainer, and scores each candidate on them by its mean absolute scaled error. - -A candidate passes when its interval holds at least 70% of the truths and it beats **persistence** (the last value carried forward) and **seasonal-naive** (the value one season earlier) with 90% confidence; the best that passes becomes the next version. - -A version that passes becomes the active one on its own. - -## Rules - -Its trainer follows these rules, and the card of each version states them: - -- Train on the rows given and on nothing else diff --git a/capabilities/prediction/src/overview/reference-uploaded.overview.md b/capabilities/prediction/src/overview/reference-uploaded.overview.md deleted file mode 100644 index 2c73e8ed1..000000000 --- a/capabilities/prediction/src/overview/reference-uploaded.overview.md +++ /dev/null @@ -1,55 +0,0 @@ -# weekly-orders - -Predicts the orders of each region one and four weeks ahead - -## What it predicts - -It predicts **orders**, counted in orders, for each series a week at a time, 1 and 4 weeks ahead. - -A season is 52 weeks long. - -A question names a series, and one horizon when it asks for that one alone, with the value of **promotion** planned for the weeks ahead. - -It answers the last 8 observed weeks and 1 and 4 weeks ahead, each with a likely value, a low and a high. - -The likely value is the 50th percentile of its model, the low the 10th and the high the 90th. - -## What it learns from - -It learns from the dataset uploaded to the playbook as `3f2ac1d0e4b5a69788796a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a7b6c5d`, one row a series and a week, with **orders** and then these columns: - -- **basket** -- **promotion**, which a question gives for the weeks ahead - -A build reads the last 156 weeks. - -## Who trains it - -Each build asks for candidate models in the playbook's inbox, as a request to **trainer**. - -A build waits at most 7 days for its candidates, and the playbook then scores up to 8 of them, each within 10 minutes. - -Its trainer reads the brief first, with the values of each build in place of the names in bold: - -Fit a model of the weekly **target** of each of **series** regions, in **unit**, -over **window** weeks from **first** to **last**. -Predict **horizons** weeks ahead. The features are -each feature of dataset.features as **name** (**as**), separated by commas. - -*For each rule of function.rules:* - -- **rule** - -## How a version goes live - -The playbook holds back the last 12 weeks of the window from the trainer, and scores each candidate on them by its mean absolute scaled error. - -A candidate passes when its interval holds at least 70% of the truths and it beats **persistence** (the last value carried forward) and **seasonal-naive** (the value one season earlier) with 90% confidence; the best that passes becomes the next version. - -A version that passes becomes the active one on its own. - -## Rules - -Its trainer follows these rules, and the card of each version states them: - -- Train on the rows given and on nothing else diff --git a/capabilities/prediction/src/overview/trainer-words.ts b/capabilities/prediction/src/overview/trainer-words.ts deleted file mode 100644 index 234343b75..000000000 --- a/capabilities/prediction/src/overview/trainer-words.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { durationInWords, named, paragraph, sectionOf, templateLead, type OverviewSection } from '@on.auto/definitions'; -import { readableTemplate, type TemplateText } from '@on.auto/definitions/template'; - -import { predictionBounds } from '../capability/prediction-bounds.ts'; -import type { BuildSource } from '../document/prediction-document.ts'; - -const { candidatesInABuild, scoringACandidateMs } = predictionBounds; - -function askedThrough({ through, to }: BuildSource) { - return through === undefined - ? paragraph("Each build asks for candidate models in the playbook's inbox, as a request to ", named(to), '.') - : paragraph('Each build asks for candidate models through the interaction function ', named(through), '.'); -} - -function briefBlocks(written: TemplateText) { - const brief = readableTemplate(written); - return brief.length === 0 - ? [paragraph('Its brief holds no text a trainer reads, so its trainer reads the rows and the rules alone.')] - : [templateLead('Its trainer reads the brief first', 'build'), ...brief]; -} - -export function trainerSection(buildSource: BuildSource, brief: TemplateText): OverviewSection { - return sectionOf( - 'Who trains it', - askedThrough(buildSource), - paragraph( - `A build waits at most ${durationInWords(buildSource.expiresMs)} for its candidates, and the playbook then scores up to ${candidatesInABuild} of them, each within ${durationInWords(scoringACandidateMs)}.`, - ), - ...briefBlocks(brief), - ); -} diff --git a/capabilities/reasoning/README.md b/capabilities/reasoning/README.md index 9163fe307..ac77822ed 100644 --- a/capabilities/reasoning/README.md +++ b/capabilities/reasoning/README.md @@ -36,7 +36,7 @@ Effect.runPromise(decide.pipe(Effect.provide(languageModelLayer(process.env)))); A request has `model`, optional `instructions`, `messages` (roles `user` and `assistant`, each with text parts), `output`, `settings` (`max_output_tokens`, and optionally `temperature`, `top_p`, `seed`, `stop_sequences` and `reasoning`), optional `provider_options`, `timeout_ms`, `signal` and `retries`. Some providers drop sampling settings for newer models and say so in `warnings`. `provider_options` passes options to the provider, keyed by the AI SDK's namespace for it: `anthropic`, `openai`, `azure`, `google`, `vertex`, `googleVertex`, `amazonBedrock`, `bedrock`, or the gateway's name. A call fails as `definition_invalid` before any provider is called when it holds a namespace no configured provider reads, or an option for a gateway its `allowed_provider_options` does not list; which options of the built-in providers a definition may set is checked when the definition is parsed (see [Provider options](../../docs/engineering/reference/reasoning-format.md#provider-options)). `requestIssues(request)` gives the problems of a request before it is sent, and the model's `admit(request)` everything `generate` would refuse before calling a provider: those problems, a model that resolves to no configured provider or is not allowed, and provider options that are not allowed. -A definition whose `output.shape` names an [output shape](../../packages/definitions/README.md#output-shapes) asks for JSON of that shape: its `output` is `{ type: 'json', schema, shape }`, `schema` the shape's compiled by `shapedAnswerSchema(shape)` at the answer's nesting of 128 with the shape's further check, so the answer is checked as any JSON answer is and refused as `output_invalid` with the shape's issues too, and its warnings are the schema's, placed at `/output/shape`. `output.schema` beside it and `format: text` are refused at their lines. The summary publishes the shape's schema as the output schema and its name as `details.shape`, and the overview says the shape. +A definition whose `output.shape` names an [output shape](../../packages/definitions/README.md#output-shapes) asks for JSON of that shape: its `output` is `{ type: 'json', schema, shape }`, `schema` the shape's compiled by `shapedAnswerSchema(shape)` at the answer's nesting of 128 with the shape's further check, so the answer is checked as any JSON answer is and refused as `output_invalid` with the shape's issues too, and its warnings are the schema's, placed at `/output/shape`. `output.schema` beside it and `format: text` are refused at their lines. The summary publishes the shape's schema as the output schema and its name as `details.shape`. `makeModelAccess(settings, options)` builds the same model and also returns `status`, and the `catalog` that [`list_models`](../../docs/reference/model-settings.md#listing-the-models) reads, whose `list(provider?)` gives the list. Its options inject a `fetch` and credential sources (`aws`, `google`, `azure`), which is how tests run without a network and how per-tenant credentials will be added, `reportProviderMessage`, which receives the [provider messages](../../docs/reference/model-settings.md#provider-messages) a caller does not see, and `reportOperatorHint`, which receives the [operator hints](../../docs/reference/model-settings.md#operator-hints). A request may carry a `run_id`, which only those reports use; the reasoning function adapter sets it to the run's id. @@ -74,4 +74,4 @@ Two tests wait on real time on purpose, where a slow machine only lengthens the ## Source -`src/index.ts` is the entry point and `src/testing/index.ts` the entry point of the test support. `src/model` holds the interface: the request, the result, the `LanguageModel` service and the request checks. `src/failure` holds one class per failure. `src/schema` holds answer schemas: their limits and their compilation, made with the JSON Schema compiler of [`@on.auto/json-schema`](../../packages/json-schema/README.md), and `checkAnswerSchema`, the report of what in a schema is unsupported, not portable across providers or not checked. `src/settings` reads the settings from the environment with Effect `Config`. `src/adapter` and `src/tools` are the only production code that imports the AI SDK, and `src/adapter` the only code that imports the cloud credential libraries. `src/listing` reads the list of models of each provider, and `src/catalog` keeps the lists, combines them with the declared models and the aliases, applies the allow list, and defines `list_models`. `src/template` is the only code that imports liquidjs: its instance of the shared engine of [`@on.auto/definitions/template`](../../packages/definitions/README.md#templates-of-a-capability), with the registrations that stay this capability's own, its prompt filters and its system block, and compiling and rendering a prompt over the shared parse and render, behind readonly types of its own; `rendering-corpus.json` holds what each template of the rendering tests rendered before the engine moved into the shared factory, and `rendering-corpus.test.ts` holds the engine to it. `src/definition` parses and validates a definition document with the reader of `@on.auto/definitions/document`, which splits it and reads its YAML: the keys of its front matter, the settings, the schemas and the variables a template reads, and gives the capability its `parse` and `summarize`. `src/tools` gives the model a run's tools: the tools as the AI SDK takes them, the last step without them, opening a run's tools and its endings. `src/capability` is the capability: its guide, the public reference page, served to agents as `reasoning-function` and ending with the sentence `onThisServer` makes of what this server offers, preparing the input, rendering the prompt, the request, the answer, the rejections and the record. `src/overview` is the function's overview (see [The overview of a definition](../../packages/definitions/README.md#the-overview-of-a-definition)): what it needs, with the value `input.default` gives a field, what it produces, text or the fields of its JSON answer, its model and each tool, drawn, when it names any, as the function, the model it reasons with and each tool the model may call, and what it is told, its instructions and its message, read from the prompt its compiled template keeps with the line it starts on, by `readableParts` of `@on.auto/definitions/template`, the `{% system %}` block apart; `reference-example.overview.md` is the overview of the example of the public reference page and `reference-tools.overview.md` the overview of that example with the `tools` its section Tools names, which `reasoning-overview.test.ts` writes again and compares byte for byte. `src/testing` holds the fake and what the tests share, including the SDK's mock model. +`src/index.ts` is the entry point and `src/testing/index.ts` the entry point of the test support. `src/model` holds the interface: the request, the result, the `LanguageModel` service and the request checks. `src/failure` holds one class per failure. `src/schema` holds answer schemas: their limits and their compilation, made with the JSON Schema compiler of [`@on.auto/json-schema`](../../packages/json-schema/README.md), and `checkAnswerSchema`, the report of what in a schema is unsupported, not portable across providers or not checked. `src/settings` reads the settings from the environment with Effect `Config`. `src/adapter` and `src/tools` are the only production code that imports the AI SDK, and `src/adapter` the only code that imports the cloud credential libraries. `src/listing` reads the list of models of each provider, and `src/catalog` keeps the lists, combines them with the declared models and the aliases, applies the allow list, and defines `list_models`. `src/template` is the only code that imports liquidjs: its instance of the shared engine of [`@on.auto/definitions/template`](../../packages/definitions/README.md#templates-of-a-capability), with the registrations that stay this capability's own, its prompt filters and its system block, and compiling and rendering a prompt over the shared parse and render, behind readonly types of its own; `rendering-corpus.json` holds what each template of the rendering tests rendered before the engine moved into the shared factory, and `rendering-corpus.test.ts` holds the engine to it. `src/definition` parses and validates a definition document with the reader of `@on.auto/definitions/document`, which splits it and reads its YAML: the keys of its front matter, the settings, the schemas and the variables a template reads, and gives the capability its `parse` and `summarize`. `src/tools` gives the model a run's tools: the tools as the AI SDK takes them, the last step without them, opening a run's tools and its endings. `src/capability` is the capability: its guide, the public reference page, served to agents as `reasoning-function` and ending with the sentence `onThisServer` makes of what this server offers, preparing the input, rendering the prompt, the request, the answer, the rejections and the record. `src/testing` holds the fake and what the tests share, including the SDK's mock model. diff --git a/capabilities/reasoning/src/capability/reasoning-function.test.ts b/capabilities/reasoning/src/capability/reasoning-function.test.ts index 68d671546..4e687bddd 100644 --- a/capabilities/reasoning/src/capability/reasoning-function.test.ts +++ b/capabilities/reasoning/src/capability/reasoning-function.test.ts @@ -117,7 +117,7 @@ describe('preparing a reasoning function definition that is not valid', () => { { pointer: '', detail: - 'Line 3, /seed: seed is not a key of the front matter; it takes description, about, model, config, input, output, provider_options, tools', + 'Line 3, /seed: seed is not a key of the front matter; it takes description, model, config, input, output, provider_options, tools', }, ], }), diff --git a/capabilities/reasoning/src/capability/reasoning-function.ts b/capabilities/reasoning/src/capability/reasoning-function.ts index d41b018a6..b7f7b4b75 100644 --- a/capabilities/reasoning/src/capability/reasoning-function.ts +++ b/capabilities/reasoning/src/capability/reasoning-function.ts @@ -9,7 +9,6 @@ import { handCallPresenter } from '@on.auto/tool-servers'; import { parse, summarize } from '../definition/definition-reading.ts'; import type { OfferedModels } from '../model/offered-models.ts'; -import { reasoningOverview } from '../overview/reasoning-overview.ts'; import { longestRequestMs, longestRunMsOf } from './definition-request.ts'; import { definitionRun, type RunServices } from './definition-run.ts'; import { onThisServer } from './on-this-server.ts'; @@ -33,7 +32,6 @@ export function makeReasoningFunctionAdapter(options: ReasoningFunctionAdapterOp mediaType: 'text/markdown', parse, summarize, - overview: reasoningOverview, run: (definition, input, context) => run(definition, input, context), longestAnyRunMs: longestRequestMs, longestRunOf: longestRunMsOf, diff --git a/capabilities/reasoning/src/definition/definition-parsing.test.ts b/capabilities/reasoning/src/definition/definition-parsing.test.ts index 5a2761939..9d31509f9 100644 --- a/capabilities/reasoning/src/definition/definition-parsing.test.ts +++ b/capabilities/reasoning/src/definition/definition-parsing.test.ts @@ -113,7 +113,7 @@ describe('the issues of a document', () => { ).toEqual([ 'Line 2, /model: Expected provider/model, for example anthropic/claude-sonnet-4-5', 'Line 4, /config/temperature: Expected number', - 'Line 5, /flavour: flavour is not a key of the front matter; it takes description, about, model, config, input, output, provider_options, tools', + 'Line 5, /flavour: flavour is not a key of the front matter; it takes description, model, config, input, output, provider_options, tools', 'Line 7: customer is not a variable of a reasoning function’s prompt template, which reads input, today and now; assign it first', ]); }); diff --git a/capabilities/reasoning/src/definition/definition-parsing.ts b/capabilities/reasoning/src/definition/definition-parsing.ts index db38dc0bb..ac716baeb 100644 --- a/capabilities/reasoning/src/definition/definition-parsing.ts +++ b/capabilities/reasoning/src/definition/definition-parsing.ts @@ -56,7 +56,6 @@ function definitionFrom({ root, lines, issues }: ReadFrontMatter, template: () = (section) => outputContractOf(section, lines), ); const description = checkedSection(() => decodeSection.description(root['description']), Result.succeed); - const about = checkedSection(() => decodeSection.about(root['about']), Result.succeed); const providerOptions = checkedSection( () => decodeSection.provider_options(root['provider_options']), (options) => providerOptionsOf(options, lines), @@ -80,7 +79,7 @@ function definitionFrom({ root, lines, issues }: ReadFrontMatter, template: () = ]; return found.length > 0 ? Result.fail(found) - : Result.all({ model, settings, input, output, template: template(), description, about, providerOptions, tools }); + : Result.all({ model, settings, input, output, template: template(), description, providerOptions, tools }); } function definitionOf(parts: DocumentParts): Checked { @@ -93,7 +92,6 @@ function definitionOf(parts: DocumentParts): Checked template), (definition) => ({ ...(definition.description === undefined ? {} : { description: definition.description }), - ...(definition.about === undefined ? {} : { about: definition.about }), model: definition.model, settings: definition.settings, input: definition.input, diff --git a/capabilities/reasoning/src/definition/definition-reading.ts b/capabilities/reasoning/src/definition/definition-reading.ts index af802b9f3..f7ec6e9bf 100644 --- a/capabilities/reasoning/src/definition/definition-reading.ts +++ b/capabilities/reasoning/src/definition/definition-reading.ts @@ -21,14 +21,12 @@ export function parse(source: string): Effect.Effect { ), ), ).toEqual([ - 'Line 3, /prompt: prompt is not a key of the front matter; it takes description, about, model, config, input, output, provider_options, tools', + 'Line 3, /prompt: prompt is not a key of the front matter; it takes description, model, config, input, output, provider_options, tools', 'Line 5, /config/top_k: top_k is not a key of config; it takes max_output_tokens, temperature, top_p, seed, stop_sequences, reasoning', 'Line 7, /input/example: example is not a key of input; it takes schema, default', 'Line 9, /output/strict: strict is not a key of output; it takes format, schema, shape', @@ -70,31 +70,3 @@ describe('the values of the front matter keys', () => { ).toEqual({ openai: { textVerbosity: 'low', reasoningMode: 'pro' } }); }); }); - -describe('the about of the front matter', () => { - it('takes 1 to 4000 characters', () => { - expect(issuesIn(documentOf('model: openai/gpt-5\nabout: ""'))).toEqual([ - 'Line 3, /about: Expected a value with a length of at least 1', - ]); - expect(issuesIn(documentOf(`model: openai/gpt-5\nabout: ${'x'.repeat(4001)}`))).toEqual([ - 'Line 3, /about: Expected a value with a length of at most 4000', - ]); - expect(parsed(documentOf(`model: openai/gpt-5\nabout: ${'x'.repeat(4000)}`)).about).toHaveLength(4000); - }); - - it('takes no heading, nor does the description, each refused at its line', () => { - const frontMatter = [ - 'model: openai/gpt-5', - "description: '## Reviews a brief'", - 'about: |', - ' The campaign team uses it.', - '', - ' # When it runs', - ]; - - expect(issuesIn(documentOf(frontMatter.join('\n')))).toEqual([ - 'Line 3, /description: description takes no heading, since an overview writes its own: the line that begins “## Reviews a brief” makes one, with # before its words or as a line of = or - under the words above it', - 'Line 7, /about: about takes no heading, since an overview writes its own: this line makes one, with # before its words or as a line of = or - under the words above it', - ]); - }); -}); diff --git a/capabilities/reasoning/src/definition/front-matter-schema.ts b/capabilities/reasoning/src/definition/front-matter-schema.ts index 513f06c2a..79cd48a74 100644 --- a/capabilities/reasoning/src/definition/front-matter-schema.ts +++ b/capabilities/reasoning/src/definition/front-matter-schema.ts @@ -1,5 +1,4 @@ import { - AboutSchema, DescriptionSchema, frontMatterIn as sharedFrontMatterIn, type DocumentIssue, @@ -43,7 +42,6 @@ const ProviderOptionsSchema = Schema.Record(Schema.String, Schema.JsonObject); const FrontMatterSchema = Schema.Struct({ description: Schema.optionalKey(DescriptionSchema), - about: Schema.optionalKey(AboutSchema), model: Schema.String, config: Schema.optionalKey(ConfigSchema), input: Schema.optionalKey(InputSectionSchema), @@ -60,7 +58,6 @@ export type OutputSection = typeof OutputSectionSchema.Type | undefined; export const decodeSection = { description: Schema.decodeUnknownOption(Schema.UndefinedOr(DescriptionSchema)), - about: Schema.decodeUnknownOption(Schema.UndefinedOr(AboutSchema)), model: Schema.decodeUnknownOption(Schema.String), config: Schema.decodeUnknownOption(Schema.UndefinedOr(ConfigSchema)), input: Schema.decodeUnknownOption(Schema.UndefinedOr(InputSectionSchema)), diff --git a/capabilities/reasoning/src/definition/reasoning-function-definition.ts b/capabilities/reasoning/src/definition/reasoning-function-definition.ts index 5be803ce6..8f813e173 100644 --- a/capabilities/reasoning/src/definition/reasoning-function-definition.ts +++ b/capabilities/reasoning/src/definition/reasoning-function-definition.ts @@ -12,7 +12,6 @@ export interface InputContract { export interface ReasoningFunctionDefinitionDocument { readonly description?: string; - readonly about?: string; readonly model: string; readonly settings: GenerationSettings; readonly input: InputContract; diff --git a/capabilities/reasoning/src/overview/reasoning-overview.test.ts b/capabilities/reasoning/src/overview/reasoning-overview.test.ts deleted file mode 100644 index 21762c10b..000000000 --- a/capabilities/reasoning/src/overview/reasoning-overview.test.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { readFileSync } from 'node:fs'; - -import { markdownOf, overviewAtRead, type DefinitionUser } from '@on.auto/definitions'; -import { mostReadCharacters } from '@on.auto/definitions/template'; -import { sectionIn } from '@on.auto/definitions/testing'; -import { describe, expect, it } from 'vitest'; - -import { reasoningWith } from '../testing/reasoning-runs.ts'; - -const page = readFileSync(new URL('../../../../docs/reference/reasoning-format.md', import.meta.url), 'utf8'); - -const [, example = ''] = /```markdown\n([\s\S]*?)```/u.exec(page) ?? []; - -const [, toolsOfTheReference = ''] = /```yaml\n(tools: .+)\n```/u.exec(page) ?? []; - -const exampleWithTools = example.replace(/^model: .+$/mu, (modelLine) => `${modelLine}\n${toolsOfTheReference}`); - -const shapesPage = readFileSync(new URL('../../../../docs/reference/output-shapes.md', import.meta.url), 'utf8'); - -const [, shapedExample = ''] = /```markdown\n([\s\S]*?)```/u.exec(shapesPage) ?? []; - -const { prepared } = reasoningWith(); - -const counted = new Intl.NumberFormat('en'); - -function overviewOf(...lines: readonly string[]): string { - return markdownOf(prepared(lines.join('\n')).overview('review-brief')); -} - -function goldenOf(file: string): string { - return readFileSync(new URL(file, import.meta.url), 'utf8'); -} - -function modelAndToolsOf(...tools: readonly string[]): string { - const overview = overviewOf('---', 'model: openai/gpt-5', `tools: [${tools.join(', ')}]`, '---', 'Find it.'); - return sectionIn(overview, 'Which model and tools'); -} - -describe('the overviews of the example of the reference of reasoning functions', () => { - it('are, byte for byte, the overviews pinned beside this test, without tools and with the tools of the reference', () => { - expect(markdownOf(prepared(example).overview('review-brief'))).toBe(goldenOf('reference-example.overview.md')); - expect(markdownOf(prepared(exampleWithTools).overview('review-brief'))).toBe( - goldenOf('reference-tools.overview.md'), - ); - }); -}); - -describe('the overview of the reasoning function of the reference of output shapes', () => { - it('is, byte for byte, the overview pinned beside this test', () => { - expect(markdownOf(prepared(shapedExample).overview('queue-hours'))).toBe( - readFileSync(new URL('shaped-example.overview.md', import.meta.url), 'utf8'), - ); - }); -}); - -describe('the reference of reasoning functions', () => { - it('gives the bound of the instructions and the message an overview shows', () => { - expect(page).toContain(`each cut past ${counted.format(mostReadCharacters)} characters`); - }); -}); - -describe('the overview of a reasoning function whose document has an about', () => { - it('writes the about as its author wrote it, after the description and before what the function needs', () => { - const overview = overviewOf( - '---', - 'description: Reviews a campaign brief', - 'model: openai/gpt-5', - 'about: |', - ' The **campaign team** uses it to check each brief before it goes to a client.', - '', - ' - It runs when someone asks for a review.', - ' - It answers with the evidence the brief lacks.', - '---', - 'Review {{ input.brief }}', - ); - - expect(overview.split('\n\n').slice(0, 5)).toEqual([ - '# review-brief', - 'Reviews a campaign brief', - 'The **campaign team** uses it to check each brief before it goes to a client.', - '- It runs when someone asks for a review.\n- It answers with the evidence the brief lacks.', - '## What it needs', - ]); - }); -}); - -describe('the overview of a reasoning function', () => { - it('says the fields of a JSON answer, and the defaults its input takes in place of what a run leaves out', () => { - const overview = overviewOf( - '---', - 'model: openai/gpt-5', - 'input:', - ' schema: {type: object, properties: {brief: {type: string}, tone: {type: string}, max: true}}', - ' default: {tone: warm, max: 3, other: 1}', - 'output:', - ' format: json', - ' schema: {type: object, properties: {score: {type: integer, maximum: 10}}, required: [score]}', - '---', - 'Score {{ input.brief }}', - ); - - expect(sectionIn(overview, 'What it needs')).toContain('| **tone** | no | Text, “warm” when left out. |'); - expect(sectionIn(overview, 'What it needs')).toContain('| **max** | no | Any value. |'); - expect(sectionIn(overview, 'What it produces')).toContain('| **score** | yes | A whole number, at most 10. |'); - }); -}); - -describe('the overview of a reasoning function whose output names a shape', () => { - it('says the shape in what it produces, in place of the fields of its schema', () => { - const overview = overviewOf('---', 'model: openai/gpt-5', 'output: {shape: text}', '---', 'Write it.'); - - expect(sectionIn(overview, 'What it produces')).toBe('It answers a **text**: Markdown of at most 65,536 bytes.'); - }); -}); - -describe('the model and tools of a reasoning function in its overview', () => { - it('say the model and that it calls no tools, with nothing drawn, when its document names none', () => { - const overview = overviewOf('---', 'model: openai/gpt-5', '---', 'Find it.'); - - expect(sectionIn(overview, 'Which model and tools')).toBe( - ['It reasons with the model `openai/gpt-5`.', 'It calls no tools.'].join('\n\n'), - ); - }); - - it('name each tool with its server, and every tool of a server named with a star', () => { - const [model, mayCall, tools] = modelAndToolsOf('graph/search', 'notes/*').split('\n\n'); - - expect([model, mayCall, tools]).toEqual([ - 'It reasons with the model `openai/gpt-5`.', - 'It may call these tools:', - '- the tool **search** of **graph**\n- every tool of **notes**', - ]); - }); - - it('draw the function, the model it reasons with and each tool the model may call, then say how it calls them', () => { - const [, , , diagram, howItCalls] = modelAndToolsOf('graph/search', 'notes/*').split('\n\n'); - - expect(diagram).toBe( - [ - '```mermaid', - 'flowchart LR', - ' n1["review-brief"]', - ' n2["openai/gpt-5"]', - ' n3["graph/search"]', - ' n4["every tool of notes"]', - ' n1 -->|"reasons with"| n2', - ' n2 -->|"may call"| n3', - ' n2 -->|"may call"| n4', - '```', - ].join('\n'), - ); - expect(howItCalls).toBe( - "The model may call these tools before it answers, several in one step and over several steps, within a run's limits on calls, time and failures.", - ); - }); -}); - -describe('the overview a read writes of a reasoning function whose prompt holds what an overview reads as its own', () => { - const fence = '`'.repeat(3); - const lead = 'Its message, with the values of each run in place of the names in bold:'; - const users: readonly DefinitionUser[] = [ - { type: 'workflow', name: 'weekly-review', relation: 'calls', steps: [{ number: 2, name: 'review' }] }, - ]; - - function partsOf(...prompt: readonly string[]): readonly unknown[] { - const overview = prepared(['---', 'model: openai/gpt-5', '---', ...prompt].join('\n')).overview('review-brief'); - const [story = '', ...details] = overviewAtRead(overview, { users, called: [] }).split('\n## Details\n'); - return [sectionIn(story, 'What it is told'), sectionIn(story, 'Used by'), details]; - } - - const usedBy = '- The workflow **Weekly review** calls it in step 2, **Review**.\n'; - - const detailsOfUsedBy = ['\n### Used by\n\n- `weekly-review`, a workflow\n']; - - it('writes a line ## Details of the prompt with its first character escaped, so the page splits it at its own alone', () => { - expect(partsOf('Review the brief.', '## Details', 'Name what it lacks.')).toEqual([ - `${lead}\n\nReview the brief.\n\\## Details\nName what it lacks.`, - usedBy, - detailsOfUsedBy, - ]); - }); - - it('closes a fence the prompt leaves open at its end, so what follows is read as the overview', () => { - expect(partsOf('Review the brief as in:', fence, 'an example')).toEqual([ - `${lead}\n\nReview the brief as in:\n${fence}\nan example\n${fence}`, - usedBy, - detailsOfUsedBy, - ]); - }); -}); - -describe('the overview of a reasoning function without an input schema or instructions', () => { - it('says what it takes and what it is told', () => { - const overview = overviewOf( - '---', - 'model: openai/gpt-5', - '---', - '{% if input.vip %}Treat them as a VIP.{% endif %} Hello {{ input.name | default: "friend" }}.', - ); - - expect(sectionIn(overview, 'What it needs')).toBe('It takes any input, which its document does not describe.'); - expect(sectionIn(overview, 'What it is told')).toBe( - [ - 'Its message, with the values of each run in place of the names in bold:', - 'Treat them as a VIP when input.vip. Hello **name**.\n', - ].join('\n\n'), - ); - }); -}); diff --git a/capabilities/reasoning/src/overview/reasoning-overview.ts b/capabilities/reasoning/src/overview/reasoning-overview.ts deleted file mode 100644 index f8bca3e6e..000000000 --- a/capabilities/reasoning/src/overview/reasoning-overview.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { - bulletList, - inputSection, - isJsonSchema, - named, - outputSection, - paragraph, - sectionOf, - templateLead, - theToolOf, - type Diagram, - type OverviewBlock, - type OverviewParts, - type OverviewSection, - type Phrases, -} from '@on.auto/definitions'; -import { readableParts } from '@on.auto/definitions/template'; -import { namesEveryTool, writtenOf, type ToolReference } from '@on.auto/tool-servers/policy'; -import type { Schema } from 'effect'; - -import type { - InputContract, - ReasoningFunctionDefinitionDocument, -} from '../definition/reasoning-function-definition.ts'; -import type { OutputRequest } from '../model/model-request.ts'; - -type Entry = readonly [string, Schema.Json]; - -const everyToolOf = 'every tool of '; - -const howTheModelCalls = - "The model may call these tools before it answers, several in one step and over several steps, within a run's limits on calls, time and failures."; - -const functionNode = 0; - -const modelNode = 1; - -const firstToolNode = 2; - -function withDefault([name, property]: Entry, defaults: Schema.JsonObject): Entry { - const value = defaults[name]; - return value === undefined || !isJsonSchema(property) - ? [name, property] - : [name, Object.fromEntries([...Object.entries(property), ['default', value]])]; -} - -function documentedInput({ schema, defaults }: InputContract): Schema.JsonObject | undefined { - const properties = schema?.document['properties']; - const namesItsFields = isJsonSchema(properties); - if (schema === undefined || !namesItsFields) { - return schema?.document; - } - const withDefaults = Object.entries(properties).map((entry: Entry) => withDefault(entry, defaults)); - return Object.fromEntries([...Object.entries(schema.document), ['properties', Object.fromEntries(withDefaults)]]); -} - -function producedSection(output: OutputRequest): OverviewSection { - return output.type === 'json' - ? outputSection(output.schema.document, output.shape) - : sectionOf('What it produces', paragraph('Text: the answer the model writes.')); -} - -function toolPhrases(reference: ToolReference): Phrases { - return namesEveryTool(reference) ? [everyToolOf, named(reference.server)] : theToolOf(reference); -} - -function toolLabel(reference: ToolReference): string { - return namesEveryTool(reference) ? `${everyToolOf}${reference.server}` : writtenOf(reference); -} - -function modelAndTools(name: string, { model, tools }: ReasoningFunctionDefinitionDocument): Diagram { - return { - kind: 'diagram', - nodes: [{ label: name }, { label: model }, ...tools.map((tool) => ({ label: toolLabel(tool) }))], - edges: [ - { from: functionNode, to: modelNode, label: 'reasons with' }, - ...tools.map((_, index) => ({ from: modelNode, to: firstToolNode + index, label: 'may call' })), - ], - }; -} - -function toolBlocks(name: string, definition: ReasoningFunctionDefinitionDocument): readonly OverviewBlock[] { - const { tools } = definition; - return tools.length === 0 - ? [paragraph('It calls no tools.')] - : [ - paragraph('It may call these tools:'), - bulletList(tools.map((tool) => toolPhrases(tool))), - modelAndTools(name, definition), - paragraph(howTheModelCalls), - ]; -} - -function modelSection(name: string, definition: ReasoningFunctionDefinitionDocument): OverviewSection { - return sectionOf( - 'Which model and tools', - paragraph('It reasons with the model ', { code: definition.model }, '.'), - ...toolBlocks(name, definition), - ); -} - -function toldSection({ template }: ReasoningFunctionDefinitionDocument): OverviewSection { - const { inside, outside } = readableParts(template, 'system'); - const instructions = - inside.length === 0 ? [] : [paragraph('Its instructions, which it follows on every run:'), ...inside]; - return sectionOf('What it is told', ...instructions, templateLead('Its message', 'run'), ...outside); -} - -export function reasoningOverview(definition: ReasoningFunctionDefinitionDocument, name: string): OverviewParts { - return { - body: [], - sections: [ - inputSection(documentedInput(definition.input)), - producedSection(definition.output), - modelSection(name, definition), - toldSection(definition), - ], - appendix: [], - }; -} diff --git a/capabilities/reasoning/src/overview/reference-example.overview.md b/capabilities/reasoning/src/overview/reference-example.overview.md deleted file mode 100644 index faee19d60..000000000 --- a/capabilities/reasoning/src/overview/reference-example.overview.md +++ /dev/null @@ -1,31 +0,0 @@ -# review-brief - -Reviews a campaign brief against the team's criteria - -## What it needs - -| Field | Required | What it is | -| --- | --- | --- | -| **brief** | yes | Text. | -| **criteria** | yes | Text. | - -## What it produces - -Text: the answer the model writes. - -## Which model and tools - -It reasons with the model `anthropic/claude-sonnet-4-5`. - -It calls no tools. - -## What it is told - -Its instructions, which it follows on every run: - -Review the brief using the supplied criteria. Identify missing evidence. Do not invent facts. - -Its message, with the values of each run in place of the names in bold: - -Brief: **brief** -Criteria: **criteria** diff --git a/capabilities/reasoning/src/overview/reference-tools.overview.md b/capabilities/reasoning/src/overview/reference-tools.overview.md deleted file mode 100644 index 281a8d6bb..000000000 --- a/capabilities/reasoning/src/overview/reference-tools.overview.md +++ /dev/null @@ -1,50 +0,0 @@ -# review-brief - -Reviews a campaign brief against the team's criteria - -## What it needs - -| Field | Required | What it is | -| --- | --- | --- | -| **brief** | yes | Text. | -| **criteria** | yes | Text. | - -## What it produces - -Text: the answer the model writes. - -## Which model and tools - -It reasons with the model `anthropic/claude-sonnet-4-5`. - -It may call these tools: - -- the tool **search** of **graph** -- the tool **query** of **graph** -- every tool of **notes** - -```mermaid -flowchart LR - n1["review-brief"] - n2["anthropic/claude-sonnet-4-5"] - n3["graph/search"] - n4["graph/query"] - n5["every tool of notes"] - n1 -->|"reasons with"| n2 - n2 -->|"may call"| n3 - n2 -->|"may call"| n4 - n2 -->|"may call"| n5 -``` - -The model may call these tools before it answers, several in one step and over several steps, within a run's limits on calls, time and failures. - -## What it is told - -Its instructions, which it follows on every run: - -Review the brief using the supplied criteria. Identify missing evidence. Do not invent facts. - -Its message, with the values of each run in place of the names in bold: - -Brief: **brief** -Criteria: **criteria** diff --git a/capabilities/reasoning/src/overview/shaped-example.overview.md b/capabilities/reasoning/src/overview/shaped-example.overview.md deleted file mode 100644 index a35b12ae6..000000000 --- a/capabilities/reasoning/src/overview/shaped-example.overview.md +++ /dev/null @@ -1,31 +0,0 @@ -# queue-hours - -Estimates the hours each support queue needs next week, with a range for each - -## What it needs - -| Field | Required | What it is | -| --- | --- | --- | -| **tickets** | yes | The open tickets, one a line, each with its queue. Text. | - -## What it produces - -It answers 1 to 200 **values**, in **unit**, each a **value** under a **name** of its own, with a **low** and a **high** where given. - -## Which model and tools - -It reasons with the model `anthropic/claude-sonnet-4-5`. - -It calls no tools. - -## What it is told - -Its instructions, which it follows on every run: - -Estimate from the tickets alone. Name each queue as the tickets name it. - -Its message, with the values of each run in place of the names in bold: - -For each queue, estimate the hours its team needs next week, with a low and a high, in the unit hours. - -**tickets** diff --git a/capabilities/reasoning/src/template/compiled-template.ts b/capabilities/reasoning/src/template/compiled-template.ts index 59bc809d9..2bdcaf927 100644 --- a/capabilities/reasoning/src/template/compiled-template.ts +++ b/capabilities/reasoning/src/template/compiled-template.ts @@ -1,7 +1,6 @@ import type { RenderClock, RenderFailure as EngineRenderFailure, - TemplateText, VariableReference, } from '@on.auto/definitions/template'; import type { Result, Schema } from 'effect'; @@ -25,7 +24,7 @@ export type RenderFailure = | EngineRenderFailure | { readonly reason: 'too_long'; readonly part: PromptPart; readonly line: number }; -export interface CompiledTemplate extends TemplateText { +export interface CompiledTemplate { readonly hasInstructions: boolean; readonly hasMessage: boolean; readonly variables: readonly VariableReference[]; diff --git a/capabilities/reasoning/src/template/template-compilation.test.ts b/capabilities/reasoning/src/template/template-compilation.test.ts index dae60185d..c4187b677 100644 --- a/capabilities/reasoning/src/template/template-compilation.test.ts +++ b/capabilities/reasoning/src/template/template-compilation.test.ts @@ -21,10 +21,6 @@ describe('a template', () => { }); }); - it('keeps the prompt it was compiled from and the line of the document it starts on, as the overview reads it', () => { - expect(compiled('Hello {{ input.name }}', 6)).toMatchObject({ text: 'Hello {{ input.name }}', firstLine: 6 }); - }); - it('knows when it writes nothing outside the system block', () => { expect(compiled('\n \n').hasMessage).toBe(false); expect(compiled(' {% system %}Be brief. {{ input.x }}{% endsystem %}\n').hasMessage).toBe(false); diff --git a/capabilities/reasoning/src/template/template-compilation.ts b/capabilities/reasoning/src/template/template-compilation.ts index 0492bfd93..a4d6dc6ae 100644 --- a/capabilities/reasoning/src/template/template-compilation.ts +++ b/capabilities/reasoning/src/template/template-compilation.ts @@ -22,8 +22,6 @@ export function compileTemplate( } const hasInstructions = outline.top.length > 0; return Result.succeed({ - text: body, - firstLine, hasInstructions, hasMessage: outline.hasMessage, variables: parsed.variables, diff --git a/capabilities/recall/README.md b/capabilities/recall/README.md index 267e8860e..26183a802 100644 --- a/capabilities/recall/README.md +++ b/capabilities/recall/README.md @@ -8,13 +8,11 @@ User documentation is [Recall function format](../../docs/reference/recall-forma ## The document -`parseRecallDocument(source)` reads a document with the shared reader of [`@on.auto/definitions/document`](../../packages/definitions/README.md#reading-function-documents) and gives a `RecallFunctionDefinitionDocument`: an optional `description`, an optional `about`, which `AboutSchema` of `@on.auto/definitions/document` bounds and the summary carries into the overview, the `language`, `typescript`, the compiled `input.schema` and `output.schema` when the document has them, `answers`, whether the module exports a function `answer`, `filterExpressions`, the `data` expression of each filter that has one with its pointer and its line, for the check, and the `details` the host folds the view by: the module and the line it starts on, the filters, `initial` and the view's schema. The front matter's keys are `description`, `about`, `language`, `source.events`, `view.initial`, `view.schema`, `input.schema` and `output.schema`; every other key, `model`, `config`, `tools`, `output.format`, `input.default` and `answer` among them, is an issue at its line, and the message for empty front matter names `the language and the events it folds`. A language other than `typescript` is refused with the engine's `oneLanguage`. +`parseRecallDocument(source)` reads a document with the shared reader of [`@on.auto/definitions/document`](../../packages/definitions/README.md#reading-function-documents) and gives a `RecallFunctionDefinitionDocument`: an optional `description`, the `language`, `typescript`, the compiled `input.schema` and `output.schema` when the document has them, `answers`, whether the module exports a function `answer`, `filterExpressions`, the `data` expression of each filter that has one with its pointer and its line, for the check, and the `details` the host folds the view by: the module and the line it starts on, the filters, `initial` and the view's schema. The front matter's keys are `description`, `language`, `source.events`, `view.initial`, `view.schema`, `input.schema` and `output.schema`; every other key, `model`, `config`, `tools`, `output.format`, `input.default` and `answer` among them, is an issue at its line, and the message for empty front matter names `the language and the events it folds`. A language other than `typescript` is refused with the engine's `oneLanguage`. The module and the filters' expressions are checked when the document is saved, as one job, by the capability's `check` (`src/document/document-check.ts`), which asks the pool's check worker of [`@on.auto/definitions`](../../packages/definitions/README.md#the-check-at-save), against `View`, `Input`, `Output` and `Event`, each problem refused with its line in the document, a filter's under its pointer; a check that a ready checker does not finish within 2 seconds leaves the document unsaved, `invalid_input`, and a save that finds no checker ready is `unavailable`: parsing keeps the module as it is written, and a document is recognised as answering when its module declares `export function answer` at the start of a line, the one form the check lets an export take. A filter (`src/document/source-filters.ts`) is read with the engine's `literalFilterOf`, so its `type`, `source` and `subject` are written out and it tests no other attribute; its `data` expression names nothing but `$data`, which the check holds it to. `initial` must fit the bound on a view, 524,288 bytes as JSON, and match the view's schema; the shared reader already bounds the front matter's nesting at 72 levels. -`summarize` answers the description, the about, the schemas and the `details`, which the registry keeps with the definition record and never answers, so the host can fold a view without this package's parser. - -`recallOverview` (`src/overview`) is the function's overview (see [The overview of a definition](../../packages/definitions/README.md#the-overview-of-a-definition)): what it remembers, each filter as a sentence after `each time`, in the words `eventFilterPhrases` of `@on.auto/definitions` gives every event filter: its event type, `run_started`, `run_succeeded`, `run_failed`, `run_rejected`, `definition_created`, `definition_updated`, `definition_retired` or any other type as an event published to the playbook, and the definition its `subject` or, for a definition's event, its `source` names, every run or definition when it names none, then each other attribute it names in words; what it keeps, the view's schema in words and `initial` unless it is empty, as JSON within `mostInitialViewCharacters`, 300 characters counted as code points by `charactersIn` of `@on.auto/definitions`, and past them named as shown under Source; what it answers, its input and its output, or the view itself for a module without `answer`; and how the view is kept, a diagram from each filter, labelled with its words in plain text by `plainTextOf` of `@on.auto/definitions`, to the view, drawn as a cylinder, and from the view to the answer, the sentence that the view is built from the playbook's history and kept up to date, and one line that the program is TypeScript and is shown under Source. `reference-example.overview.md` and `reference-common-case.overview.md` are the overviews of the two examples of the public reference page, which `recall-overview.test.ts` writes again and compares byte for byte. +`summarize` answers the description, the schemas and the `details`, which the registry keeps with the definition record and never answers, so the host can fold a view without this package's parser. ## A run @@ -55,4 +53,4 @@ The tests that fold a page of the views of the example in a real worker, `src/do ## Source -`src/index.ts` is the entry point and `src/testing/index.ts` the entry point of the test support. `src/document` holds the document: its type, the front matter's keys, the filters and parsing. `src/run` holds a run and the folding settings: the bounds, the input's checks, the answer, its endings and the words of a view not ready. `src/capability` holds the capability, its parsing and summary, its standing, and its guide, the public reference page, served to agents as `recall-function`. `src/overview` holds the overview and the words of the filters. `src/testing` holds the example and what the tests share. +`src/index.ts` is the entry point and `src/testing/index.ts` the entry point of the test support. `src/document` holds the document: its type, the front matter's keys, the filters and parsing. `src/run` holds a run and the folding settings: the bounds, the input's checks, the answer, its endings and the words of a view not ready. `src/capability` holds the capability, its parsing and summary, its standing, and its guide, the public reference page, served to agents as `recall-function`. `src/testing` holds the example and what the tests share. diff --git a/capabilities/recall/src/capability/recall-definitions.ts b/capabilities/recall/src/capability/recall-definitions.ts index 5452705c9..e9697a05e 100644 --- a/capabilities/recall/src/capability/recall-definitions.ts +++ b/capabilities/recall/src/capability/recall-definitions.ts @@ -18,14 +18,12 @@ export function parse(source: string): Effect.Effect run({ document, program: stripped.module }, input, context), longestAnyRunMs: deadlineMs, mostActive: mostFunctions, diff --git a/capabilities/recall/src/document/document-parsing.test.ts b/capabilities/recall/src/document/document-parsing.test.ts index a95616f01..9bb5e2296 100644 --- a/capabilities/recall/src/document/document-parsing.test.ts +++ b/capabilities/recall/src/document/document-parsing.test.ts @@ -78,16 +78,16 @@ describe('the front matter of a recall function definition', () => { const front = `${succeeded}\nmodel: anthropic/claude-sonnet-4-5\nconfig: {temperature: 0}\ntools: [graph/*]\ninput: {default: {}}\noutput: {format: json}\nview: {limit: 3}\nsource2: x`; expect(issuesIn(recallDocument(adding, front))).toEqual([ - 'Line 6, /model: model is not a key of the front matter; it takes description, about, language, source, view, input, output', - 'Line 7, /config: config is not a key of the front matter; it takes description, about, language, source, view, input, output', - 'Line 8, /tools: tools is not a key of the front matter; it takes description, about, language, source, view, input, output', + 'Line 6, /model: model is not a key of the front matter; it takes description, language, source, view, input, output', + 'Line 7, /config: config is not a key of the front matter; it takes description, language, source, view, input, output', + 'Line 8, /tools: tools is not a key of the front matter; it takes description, language, source, view, input, output', 'Line 9, /input/default: default is not a key of input; it takes schema', 'Line 10, /output/format: format is not a key of output; it takes schema', 'Line 11, /view/limit: limit is not a key of view; it takes initial, schema', - 'Line 12, /source2: source2 is not a key of the front matter; it takes description, about, language, source, view, input, output', + 'Line 12, /source2: source2 is not a key of the front matter; it takes description, language, source, view, input, output', ]); expect(issuesIn(recallDocument(adding, `${succeeded}\nanswer: '.'`))).toEqual([ - 'Line 6, /answer: answer is not a key of the front matter; it takes description, about, language, source, view, input, output', + 'Line 6, /answer: answer is not a key of the front matter; it takes description, language, source, view, input, output', ]); }); @@ -150,20 +150,3 @@ describe('the module of a recall function definition', () => { ).toBe('export function fold(view: View, event: Event): View {\n return view.perod;\n}'); }); }); - -describe('the about of a recall function definition', () => { - it('is kept as its author wrote it', () => { - expect(parsed(recallDocument(adding, `${succeeded}\nabout: |\n The campaign team asks it.`)).about).toBe( - 'The campaign team asks it.\n', - ); - }); - - it('takes no heading, nor does the description, each refused at its line', () => { - const written = `${succeeded}\ndescription: '# Reviews'\nabout: |\n The campaign team asks it.\n\n ## When`; - - expect(issuesIn(recallDocument(adding, written))).toEqual([ - 'Line 6, /description: description takes no heading, since an overview writes its own: the line that begins “# Reviews” makes one, with # before its words or as a line of = or - under the words above it', - 'Line 10, /about: about takes no heading, since an overview writes its own: this line makes one, with # before its words or as a line of = or - under the words above it', - ]); - }); -}); diff --git a/capabilities/recall/src/document/document-parsing.ts b/capabilities/recall/src/document/document-parsing.ts index 9b85d3966..b6b63d1d7 100644 --- a/capabilities/recall/src/document/document-parsing.ts +++ b/capabilities/recall/src/document/document-parsing.ts @@ -116,14 +116,13 @@ function filterExpressionsOf(filters: readonly ViewFilter[], lines: SourceLines) } function assembled( - { description, about }: Pick, + { description }: Pick, parts: Parts, lines: SourceLines, ): RecallFunctionDefinitionDocument { const { input, output, view, filters, fold } = parts; return { ...(description === undefined ? {} : { description }), - ...(about === undefined ? {} : { about }), language, input, output, diff --git a/capabilities/recall/src/document/front-matter.ts b/capabilities/recall/src/document/front-matter.ts index ad0b738d7..258a44782 100644 --- a/capabilities/recall/src/document/front-matter.ts +++ b/capabilities/recall/src/document/front-matter.ts @@ -1,9 +1,4 @@ -import { - AboutSchema, - DescriptionSchema, - type FrontMatterSection, - type FrontMatterShape, -} from '@on.auto/definitions/document'; +import { DescriptionSchema, type FrontMatterSection, type FrontMatterShape } from '@on.auto/definitions/document'; import { Schema } from 'effect'; const SectionSchema = Schema.Struct({ schema: Schema.optionalKey(Schema.JsonObject) }); @@ -17,7 +12,6 @@ const ViewSchema = Schema.Struct({ const FrontMatterSchema = Schema.Struct({ description: Schema.optionalKey(DescriptionSchema), - about: Schema.optionalKey(AboutSchema), language: Schema.String, source: SourceSchema, view: Schema.optionalKey(ViewSchema), diff --git a/capabilities/recall/src/document/recall-document.ts b/capabilities/recall/src/document/recall-document.ts index ccf04d2c2..db1657086 100644 --- a/capabilities/recall/src/document/recall-document.ts +++ b/capabilities/recall/src/document/recall-document.ts @@ -9,7 +9,6 @@ export interface FilterExpression { export interface RecallFunctionDefinitionDocument { readonly description?: string; - readonly about?: string; readonly language: 'typescript'; readonly input: ValueContract; readonly output: ValueContract; diff --git a/capabilities/recall/src/overview/recall-overview.test.ts b/capabilities/recall/src/overview/recall-overview.test.ts deleted file mode 100644 index d286f8f67..000000000 --- a/capabilities/recall/src/overview/recall-overview.test.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { readFileSync } from 'node:fs'; - -import { markdownOf } from '@on.auto/definitions'; -import { sectionIn } from '@on.auto/definitions/testing'; -import { describe, expect, it } from 'vitest'; - -import { foldOf, recallDocument } from '../testing/campaign-reviews.ts'; -import { recallWith } from '../testing/recall-runs.ts'; - -const page = readFileSync(new URL('../../../../docs/reference/recall-format.md', import.meta.url), 'utf8'); - -const [reviewsExample = '', announcementsExample = ''] = [...page.matchAll(/```markdown\n([\s\S]*?)```/gu)].map( - ([, body = '']: readonly string[]) => body, -); - -const twoFilters = [ - 'language: typescript', - 'source:', - ' events:', - ' - { type: run_succeeded, subject: reasoning/review-brief }', - ' - { type: run_rejected, subject: reasoning/review-brief }', -]; - -function goldenOf(file: string): string { - return readFileSync(new URL(file, import.meta.url), 'utf8'); -} - -function overviewOf(frontMatter: readonly string[], answer?: string): string { - const source = recallDocument(foldOf('return view;', answer), frontMatter.join('\n')); - return markdownOf(recallWith().prepared(source).overview('reviews')); -} - -function rememberedOf(...filters: readonly string[]): readonly string[] { - const events = filters.map((filter) => ` - ${filter}`); - const remembered = sectionIn( - overviewOf(['language: typescript', 'source:', ' events:', ...events]), - 'What it remembers', - ); - return remembered - .split('\n') - .filter((line) => line.startsWith('- ')) - .map((line) => line.slice(2)); -} - -function initialOf(initial: string): string { - return sectionIn(overviewOf([...twoFilters, 'view:', ` initial: ${initial}`]), 'What it keeps'); -} - -describe('the overviews of the examples of the reference of recall functions', () => { - it('are, byte for byte, the overviews pinned beside this test', () => { - const { prepared } = recallWith(); - - expect(markdownOf(prepared(reviewsExample).overview('campaign-reviews'))).toBe( - goldenOf('reference-example.overview.md'), - ); - expect(markdownOf(prepared(announcementsExample).overview('announcements'))).toBe( - goldenOf('reference-common-case.overview.md'), - ); - }); -}); - -describe('the events a recall function remembers, in its overview', () => { - it('say each filter after each time, in the words of an event filter, with each attribute it names', () => { - expect( - rememberedOf( - '{ type: run_succeeded, subject: reasoning/review-brief, caller: acme-admin }', - '{ type: definition_retired, source: /definitions/workflow/greet }', - "{ type: order_placed, data: '${ $data.revenue > 100 }' }", - ), - ).toEqual([ - 'each time a run of the reasoning function **review-brief** succeeds, when the caller is `acme-admin`', - 'each time the workflow **greet** is retired', - 'each time an event `order_placed` is published to the playbook, when `$data.revenue > 100` holds for its data', - ]); - }); -}); - -describe('how the view of a recall function is kept, in its overview', () => { - it('is a diagram from each filter to the view, drawn as a cylinder, and from the view to the answer', () => { - const overview = overviewOf(twoFilters, 'return view;'); - - expect(sectionIn(overview, 'How it is kept').split('\n\n')[0]).toBe( - [ - '```mermaid', - 'flowchart LR', - ' n1["A run of the reasoning function review-brief succeeds"]', - ' n2["A run of the reasoning function review-brief is rejected"]', - ' n3[("The view")]', - ' n4["The answer of a run"]', - ' n1 --> n3', - ' n2 --> n3', - ' n3 --> n4', - '```', - ].join('\n'), - ); - expect(sectionIn(overview, 'What it remembers')).toMatch( - /\n\nIt remembers an event that matches more than one of these once\.$/u, - ); - }); -}); - -describe('what a recall function keeps and answers, in its overview', () => { - it('says the view it starts with, unless it starts empty', () => { - expect(initialOf('{ count: 0 }')).toMatch(/\n\nBefore the first event, the view is `\{"count":0\}`\.$/u); - expect(initialOf('0')).toMatch(/\n\nBefore the first event, the view is `0`\.$/u); - expect(initialOf(`'${'x'.repeat(300)}'`)).toMatch( - /\n\nBefore the first event, the view is the value of `view\.initial`, shown under Source\.$/u, - ); - expect(['{}', '[]', "''", 'null'].map((initial) => initialOf(initial))).toEqual( - ['{}', '[]', "''", 'null'].map(() => - [ - 'It keeps one view, which its program updates with each event it remembers:', - 'Any value, which its document does not describe.', - ].join('\n\n'), - ), - ); - }); - - it('says a function without an answer answers with its view, and the schema its view is checked against', () => { - const answered = ['Each run takes:', 'Any input, which its document does not describe.']; - - expect(sectionIn(overviewOf(twoFilters), 'What it answers')).toBe( - [...answered, 'It answers with the view itself, as it stands.'].join('\n\n'), - ); - expect(sectionIn(overviewOf([...twoFilters, 'output:', ' schema: { type: object }']), 'What it answers')).toBe( - [...answered, 'It answers with the view itself, as it stands.', 'A record.'].join('\n\n'), - ); - }); - - it('says what a function with an answer but no schemas takes and answers', () => { - expect(sectionIn(overviewOf(twoFilters, 'return view;'), 'What it answers')).toBe( - [ - 'Each run takes:', - 'Any input, which its document does not describe.', - 'It answers with what its program makes of the view and that input:', - 'Any output, which its document does not describe.', - ].join('\n\n'), - ); - }); -}); - -describe('the view a recall function starts with, in its overview', () => { - it('is said within 300 characters and named under Source at 301, a character beyond the Basic Multilingual Plane counted once', () => { - const underSource = 'Before the first event, the view is the value of `view.initial`, shown under Source.'; - - expect( - [`'${'x'.repeat(298)}'`, `'${'x'.repeat(299)}'`, `'${'😀'.repeat(298)}'`, `'${'😀'.repeat(299)}'`].map( - (initial) => initialOf(initial).split('\n\n').at(-1), - ), - ).toEqual([ - `Before the first event, the view is \`"${'x'.repeat(298)}"\`.`, - underSource, - `Before the first event, the view is \`"${'😀'.repeat(298)}"\`.`, - underSource, - ]); - }); -}); - -describe('the overview of a recall function whose document has an about', () => { - it('writes the about as its author wrote it, after the description and before what it remembers', () => { - const about = [ - 'description: The reviews of each campaign', - 'about: |', - ' The campaign team asks it what the reviews of a campaign have said so far.', - '', - ' It answers with them latest last.', - ]; - - expect( - overviewOf([...twoFilters, ...about]) - .split('\n\n') - .slice(0, 5), - ).toEqual([ - '# reviews', - 'The reviews of each campaign', - 'The campaign team asks it what the reviews of a campaign have said so far.', - 'It answers with them latest last.', - '## What it remembers', - ]); - }); -}); diff --git a/capabilities/recall/src/overview/recall-overview.ts b/capabilities/recall/src/overview/recall-overview.ts deleted file mode 100644 index 24e4b06a7..000000000 --- a/capabilities/recall/src/overview/recall-overview.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { - bulletList, - charactersIn, - describedSchemaBlocks, - eventFilterPhrases, - paragraph, - plainTextOf, - schemaBlocks, - sectionOf, - shownUnderSource, - undescribedSentence, - type Diagram, - type OverviewBlock, - type OverviewParts, - type OverviewSection, -} from '@on.auto/definitions'; -import { capitalized } from '@on.auto/operations'; -import type { ViewFilter } from '@on.auto/workflow-host'; -import type { Schema } from 'effect'; - -import type { RecallFunctionDefinitionDocument } from '../document/recall-document.ts'; - -const mostInitialViewCharacters = 300; - -function remembersSection(filters: readonly ViewFilter[]): OverviewSection { - const once = filters.length > 1 ? [paragraph('It remembers an event that matches more than one of these once.')] : []; - return sectionOf( - 'What it remembers', - paragraph("It remembers, from the first event of the playbook's history on:"), - bulletList(filters.map((filter) => ['each time ', ...eventFilterPhrases(filter)])), - ...once, - ); -} - -function isEmpty(value: Schema.Json): boolean { - return value === null || value === '' || (typeof value === 'object' && Object.keys(value).length === 0); -} - -function initialBlocks(initial: Schema.Json): readonly OverviewBlock[] { - if (isEmpty(initial)) { - return []; - } - const written = JSON.stringify(initial); - return charactersIn(written) > mostInitialViewCharacters - ? [shownUnderSource('value', 'Before the first event, the view is the value of ', { code: 'view.initial' })] - : [paragraph('Before the first event, the view is ', { code: written }, '.')]; -} - -function keepsSection({ details }: RecallFunctionDefinitionDocument): OverviewSection { - return sectionOf( - 'What it keeps', - paragraph('It keeps one view, which its program updates with each event it remembers:'), - ...schemaBlocks(details.schema, undescribedSentence('value', 'alone')), - ...initialBlocks(details.initial), - ); -} - -function answerBlocks({ output, answers }: RecallFunctionDefinitionDocument): readonly OverviewBlock[] { - if (answers) { - return [ - paragraph('It answers with what its program makes of the view and that input:'), - ...schemaBlocks(output.schema?.document, undescribedSentence('output', 'alone')), - ]; - } - const checked = output.schema === undefined ? [] : describedSchemaBlocks(output.schema.document); - return [paragraph('It answers with the view itself, as it stands.'), ...checked]; -} - -function answersSection(document: RecallFunctionDefinitionDocument): OverviewSection { - return sectionOf( - 'What it answers', - paragraph('Each run takes:'), - ...schemaBlocks(document.input.schema?.document, undescribedSentence('input', 'alone')), - ...answerBlocks(document), - ); -} - -function diagramOf(filters: readonly ViewFilter[], answers: boolean): Diagram { - const view = filters.length; - return { - kind: 'diagram', - nodes: [ - ...filters.map((filter) => ({ label: capitalized(plainTextOf(eventFilterPhrases(filter))) })), - { label: 'The view', shape: 'cylinder' }, - { label: answers ? 'The answer of a run' : 'The answer of a run, the view itself' }, - ], - edges: [...filters.map((_, index) => ({ from: index, to: view })), { from: view, to: view + 1 }], - }; -} - -function keptSection({ details, answers }: RecallFunctionDefinitionDocument): OverviewSection { - return sectionOf( - 'How it is kept', - diagramOf(details.filters, answers), - paragraph( - "Its view is built from the first event of the playbook's history when the function is saved, and again whenever it changes, and is kept up to date as the events it remembers arrive.", - ), - shownUnderSource( - 'program', - 'The program, its ', - { code: 'fold' }, - ...(answers ? [' and its ', { code: 'answer' }] : []), - ',', - ), - ); -} - -export function recallOverview(document: RecallFunctionDefinitionDocument): OverviewParts { - return { - body: [], - sections: [ - remembersSection(document.details.filters), - keepsSection(document), - answersSection(document), - keptSection(document), - ], - appendix: [], - }; -} diff --git a/capabilities/recall/src/overview/reference-common-case.overview.md b/capabilities/recall/src/overview/reference-common-case.overview.md deleted file mode 100644 index 487e5b61f..000000000 --- a/capabilities/recall/src/overview/reference-common-case.overview.md +++ /dev/null @@ -1,38 +0,0 @@ -# announcements - -What the post-announcement function returned, oldest first, the last 50 - -## What it remembers - -It remembers, from the first event of the playbook's history on: - -- each time a run of the reasoning function **post-announcement** succeeds - -## What it keeps - -It keeps one view, which its program updates with each event it remembers: - -A list of at most 50 items. - -## What it answers - -Each run takes: - -Any input, which its document does not describe. - -It answers with the view itself, as it stands. - -## How it is kept - -```mermaid -flowchart LR - n1["A run of the reasoning function post-announcement succeeds"] - n2[("The view")] - n3["The answer of a run, the view itself"] - n1 --> n2 - n2 --> n3 -``` - -Its view is built from the first event of the playbook's history when the function is saved, and again whenever it changes, and is kept up to date as the events it remembers arrive. - -The program, its `fold`, is TypeScript and is shown under Source. diff --git a/capabilities/recall/src/overview/reference-example.overview.md b/capabilities/recall/src/overview/reference-example.overview.md deleted file mode 100644 index 911519ec7..000000000 --- a/capabilities/recall/src/overview/reference-example.overview.md +++ /dev/null @@ -1,43 +0,0 @@ -# campaign-reviews - -The reviews of each campaign, latest last, as the review-brief function wrote them - -## What it remembers - -It remembers, from the first event of the playbook's history on: - -- each time a run of the reasoning function **review-brief** succeeds - -## What it keeps - -It keeps one view, which its program updates with each event it remembers: - -A record of at most 50 entries, each a list of at most 20 records with: **at** (text), **verdict** (text), **run** (text). - -## What it answers - -Each run takes: - -| Field | Required | What it is | -| --- | --- | --- | -| **campaign** | yes | Text. | -| **last** | no | A whole number, from 1 to 50. | - -It answers with what its program makes of the view and that input: - -A list of records with: **at** (text), **verdict** (text), **run** (text, optional). - -## How it is kept - -```mermaid -flowchart LR - n1["A run of the reasoning function review-brief succeeds"] - n2[("The view")] - n3["The answer of a run"] - n1 --> n2 - n2 --> n3 -``` - -Its view is built from the first event of the playbook's history when the function is saved, and again whenever it changes, and is kept up to date as the events it remembers arrive. - -The program, its `fold` and its `answer`, is TypeScript and is shown under Source. diff --git a/docs/assets/openapi.json b/docs/assets/openapi.json index 31f58e7dd..d1e7bd282 100644 --- a/docs/assets/openapi.json +++ b/docs/assets/openapi.json @@ -5003,7 +5003,7 @@ "post": { "operationId": "create_definition", "summary": "Create definition", - "description": "Saves a new function or workflow definition in the playbook from its document and returns it without running it. Use it once the person has agreed to the definition; update_definition changes one that exists, and a name is never reused in a playbook. `type` is the definition's type and `name` is how workflows and tools refer to it: reasoning, a reasoning function; interaction, an interaction function; computation, a computation function; recall, a recall function; prediction, a prediction function; workflow, a workflow. `source` is the whole document in that type's format, which get_guide gives. A document that does not fit its format is refused with the line and what is wrong, and nothing is saved.", + "description": "Saves a new function or workflow definition in the playbook from its document and returns it without running it. Use it once the person has agreed to the definition; update_definition changes one that exists, and a name is never reused in a playbook. `type` is the definition's type and `name` is how workflows and tools refer to it: reasoning, a reasoning function; interaction, an interaction function; computation, a computation function; recall, a recall function; prediction, a prediction function; workflow, a workflow. `source` is the whole document in that type's format, which get_guide gives, and `overview` the Markdown written after it for the person who runs the business. A document that does not fit its format is refused with the line and what is wrong, and nothing is saved.", "tags": [ "definitions" ], @@ -5051,6 +5051,12 @@ "type": "string", "description": "The definition document in its type's format, at most 65536 bytes in UTF-8", "maxLength": 65536 + }, + "overview": { + "type": "string", + "description": "Markdown for the person who runs the business, written from everything you know of them and of the playbook: why this exists, what it does and how it fits, in one to three short paragraphs, with a Mermaid diagram in a fenced block where a flow is worth a picture; less technical, more value.", + "minLength": 1, + "maxLength": 16384 } }, "required": [ @@ -5082,7 +5088,7 @@ "version": { "type": "integer", "minimum": 1, - "description": "The definition version: 1 when created, and one more for every update that changes its document" + "description": "The definition version: 1 when created, and one more for every update that changes its document or its overview" }, "status": { "type": "string", @@ -5255,7 +5261,7 @@ }, "overview": { "type": "string", - "description": "The definition in plain words for a person who does not read its document: Markdown with headings, lists, tables of its fields and, where one helps, a diagram, saying, after its description and its about, as their author wrote them, what it needs and produces and what it does, as its type has it, composed by the runtime from the document when this version was saved and written each time it is read, with, after what the document says, the definitions a workflow calls, each with its description, and the active definitions of the playbook that use this one, a workflow with the steps that call it, and their exact names under Details at the end" + "description": "The overview its author wrote: Markdown for the person who runs the business, kept as written with this version" }, "triggers_since": { "type": "string", @@ -5264,6 +5270,92 @@ "standing": { "type": "object", "description": "How the definition stands where its runtime adapter keeps something for it, read when the definition is: for a recall function, the view it keeps, with its state (live, rebuilding, waiting or stalled), its version, its checkpoint, the events folded and how far it lags the playbook" + }, + "used_by": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "The type of the definition that uses this one" + }, + "name": { + "type": "string", + "description": "The name of the definition that uses this one" + }, + "relation": { + "type": "string", + "enum": [ + "calls" + ], + "description": "calls, for a workflow that runs this one in its steps" + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "properties": { + "number": { + "type": "integer", + "description": "The step's number: its place among the workflow's tasks in the order of its document, nested ones included" + }, + "name": { + "type": "string", + "description": "The name of its task" + } + }, + "required": [ + "number", + "name" + ], + "additionalProperties": false + }, + "description": "The steps of the workflow that call this one" + } + }, + "required": [ + "type", + "name", + "relation", + "steps" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "The type of the definition that uses this one" + }, + "name": { + "type": "string", + "description": "The name of the definition that uses this one" + }, + "relation": { + "type": "string", + "enum": [ + "sends-builds", + "asks-approval", + "remembers-runs", + "remembers-changes" + ], + "description": "sends-builds or asks-approval, for a prediction function that sends this one its builds to train or asks it to approve a version; remembers-runs or remembers-changes, for a recall function that folds its runs or its changes" + } + }, + "required": [ + "type", + "name", + "relation" + ], + "additionalProperties": false + } + ] + }, + "description": "The active definitions of the playbook that use this one, read from the playbook when the definition is read: a workflow with the steps that call it, a prediction function that sends it builds or asks it to approve, a recall function that remembers its runs or its changes" } }, "required": [ @@ -5275,8 +5367,7 @@ "created_at", "created_by", "updated_at", - "source", - "overview" + "source" ], "additionalProperties": false, "description": "A saved definition, with its document" @@ -5406,7 +5497,7 @@ "version": { "type": "integer", "minimum": 1, - "description": "The definition version: 1 when created, and one more for every update that changes its document" + "description": "The definition version: 1 when created, and one more for every update that changes its document or its overview" }, "status": { "type": "string", @@ -5699,7 +5790,7 @@ "version": { "type": "integer", "minimum": 1, - "description": "The definition version: 1 when created, and one more for every update that changes its document" + "description": "The definition version: 1 when created, and one more for every update that changes its document or its overview" }, "status": { "type": "string", @@ -5872,7 +5963,7 @@ }, "overview": { "type": "string", - "description": "The definition in plain words for a person who does not read its document: Markdown with headings, lists, tables of its fields and, where one helps, a diagram, saying, after its description and its about, as their author wrote them, what it needs and produces and what it does, as its type has it, composed by the runtime from the document when this version was saved and written each time it is read, with, after what the document says, the definitions a workflow calls, each with its description, and the active definitions of the playbook that use this one, a workflow with the steps that call it, and their exact names under Details at the end" + "description": "The overview its author wrote: Markdown for the person who runs the business, kept as written with this version" }, "triggers_since": { "type": "string", @@ -5881,6 +5972,92 @@ "standing": { "type": "object", "description": "How the definition stands where its runtime adapter keeps something for it, read when the definition is: for a recall function, the view it keeps, with its state (live, rebuilding, waiting or stalled), its version, its checkpoint, the events folded and how far it lags the playbook" + }, + "used_by": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "The type of the definition that uses this one" + }, + "name": { + "type": "string", + "description": "The name of the definition that uses this one" + }, + "relation": { + "type": "string", + "enum": [ + "calls" + ], + "description": "calls, for a workflow that runs this one in its steps" + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "properties": { + "number": { + "type": "integer", + "description": "The step's number: its place among the workflow's tasks in the order of its document, nested ones included" + }, + "name": { + "type": "string", + "description": "The name of its task" + } + }, + "required": [ + "number", + "name" + ], + "additionalProperties": false + }, + "description": "The steps of the workflow that call this one" + } + }, + "required": [ + "type", + "name", + "relation", + "steps" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "The type of the definition that uses this one" + }, + "name": { + "type": "string", + "description": "The name of the definition that uses this one" + }, + "relation": { + "type": "string", + "enum": [ + "sends-builds", + "asks-approval", + "remembers-runs", + "remembers-changes" + ], + "description": "sends-builds or asks-approval, for a prediction function that sends this one its builds to train or asks it to approve a version; remembers-runs or remembers-changes, for a recall function that folds its runs or its changes" + } + }, + "required": [ + "type", + "name", + "relation" + ], + "additionalProperties": false + } + ] + }, + "description": "The active definitions of the playbook that use this one, read from the playbook when the definition is read: a workflow with the steps that call it, a prediction function that sends it builds or asks it to approve, a recall function that remembers its runs or its changes" } }, "required": [ @@ -5892,8 +6069,7 @@ "created_at", "created_by", "updated_at", - "source", - "overview" + "source" ], "additionalProperties": false, "description": "A saved definition, with its document" @@ -5946,7 +6122,7 @@ "put": { "operationId": "update_definition", "summary": "Update definition", - "description": "Replaces the whole document of an active function or workflow definition and returns its new version, which every run uses from then on. Use it when the person changes a saved definition; create_definition saves a new one, and a retired definition cannot change. `type` and `name` say which definition, and `source` is the whole new document in its type's format, which get_guide gives. A document that does not fit its format is refused with the line and what is wrong, and one the same as the saved document records nothing. A new version of a recall function builds its view again from the playbook's history.", + "description": "Replaces the whole document of an active function or workflow definition and returns its new version, which every run uses from then on. Use it when the person changes a saved definition; create_definition saves a new one, and a retired definition cannot change. `type` and `name` say which definition, `source` is the whole new document in its type's format, which get_guide gives, and `overview` the Markdown for the person who runs the business, written again with every update; left out, the saved overview stays. A document that does not fit its format is refused with the line and what is wrong, and one the same as the saved document, with its overview the same or left out, records nothing. A new version of a recall function builds its view again from the playbook's history.", "tags": [ "definitions" ], @@ -5999,6 +6175,12 @@ "type": "string", "description": "The definition document in its type's format, at most 65536 bytes in UTF-8", "maxLength": 65536 + }, + "overview": { + "type": "string", + "description": "Markdown for the person who runs the business, written from everything you know of them and of the playbook: why this exists, what it does and how it fits, in one to three short paragraphs, with a Mermaid diagram in a fenced block where a flow is worth a picture; less technical, more value.", + "minLength": 1, + "maxLength": 16384 } }, "required": [ @@ -6029,7 +6211,7 @@ "version": { "type": "integer", "minimum": 1, - "description": "The definition version: 1 when created, and one more for every update that changes its document" + "description": "The definition version: 1 when created, and one more for every update that changes its document or its overview" }, "status": { "type": "string", @@ -6202,7 +6384,7 @@ }, "overview": { "type": "string", - "description": "The definition in plain words for a person who does not read its document: Markdown with headings, lists, tables of its fields and, where one helps, a diagram, saying, after its description and its about, as their author wrote them, what it needs and produces and what it does, as its type has it, composed by the runtime from the document when this version was saved and written each time it is read, with, after what the document says, the definitions a workflow calls, each with its description, and the active definitions of the playbook that use this one, a workflow with the steps that call it, and their exact names under Details at the end" + "description": "The overview its author wrote: Markdown for the person who runs the business, kept as written with this version" }, "triggers_since": { "type": "string", @@ -6211,6 +6393,92 @@ "standing": { "type": "object", "description": "How the definition stands where its runtime adapter keeps something for it, read when the definition is: for a recall function, the view it keeps, with its state (live, rebuilding, waiting or stalled), its version, its checkpoint, the events folded and how far it lags the playbook" + }, + "used_by": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "The type of the definition that uses this one" + }, + "name": { + "type": "string", + "description": "The name of the definition that uses this one" + }, + "relation": { + "type": "string", + "enum": [ + "calls" + ], + "description": "calls, for a workflow that runs this one in its steps" + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "properties": { + "number": { + "type": "integer", + "description": "The step's number: its place among the workflow's tasks in the order of its document, nested ones included" + }, + "name": { + "type": "string", + "description": "The name of its task" + } + }, + "required": [ + "number", + "name" + ], + "additionalProperties": false + }, + "description": "The steps of the workflow that call this one" + } + }, + "required": [ + "type", + "name", + "relation", + "steps" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "The type of the definition that uses this one" + }, + "name": { + "type": "string", + "description": "The name of the definition that uses this one" + }, + "relation": { + "type": "string", + "enum": [ + "sends-builds", + "asks-approval", + "remembers-runs", + "remembers-changes" + ], + "description": "sends-builds or asks-approval, for a prediction function that sends this one its builds to train or asks it to approve a version; remembers-runs or remembers-changes, for a recall function that folds its runs or its changes" + } + }, + "required": [ + "type", + "name", + "relation" + ], + "additionalProperties": false + } + ] + }, + "description": "The active definitions of the playbook that use this one, read from the playbook when the definition is read: a workflow with the steps that call it, a prediction function that sends it builds or asks it to approve, a recall function that remembers its runs or its changes" } }, "required": [ @@ -6222,8 +6490,7 @@ "created_at", "created_by", "updated_at", - "source", - "overview" + "source" ], "additionalProperties": false, "description": "A saved definition, with its document" @@ -6355,7 +6622,7 @@ "version": { "type": "integer", "minimum": 1, - "description": "The definition version: 1 when created, and one more for every update that changes its document" + "description": "The definition version: 1 when created, and one more for every update that changes its document or its overview" }, "status": { "type": "string", @@ -6528,7 +6795,7 @@ }, "overview": { "type": "string", - "description": "The definition in plain words for a person who does not read its document: Markdown with headings, lists, tables of its fields and, where one helps, a diagram, saying, after its description and its about, as their author wrote them, what it needs and produces and what it does, as its type has it, composed by the runtime from the document when this version was saved and written each time it is read, with, after what the document says, the definitions a workflow calls, each with its description, and the active definitions of the playbook that use this one, a workflow with the steps that call it, and their exact names under Details at the end" + "description": "The overview its author wrote: Markdown for the person who runs the business, kept as written with this version" }, "triggers_since": { "type": "string", @@ -6537,6 +6804,92 @@ "standing": { "type": "object", "description": "How the definition stands where its runtime adapter keeps something for it, read when the definition is: for a recall function, the view it keeps, with its state (live, rebuilding, waiting or stalled), its version, its checkpoint, the events folded and how far it lags the playbook" + }, + "used_by": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "The type of the definition that uses this one" + }, + "name": { + "type": "string", + "description": "The name of the definition that uses this one" + }, + "relation": { + "type": "string", + "enum": [ + "calls" + ], + "description": "calls, for a workflow that runs this one in its steps" + }, + "steps": { + "type": "array", + "items": { + "type": "object", + "properties": { + "number": { + "type": "integer", + "description": "The step's number: its place among the workflow's tasks in the order of its document, nested ones included" + }, + "name": { + "type": "string", + "description": "The name of its task" + } + }, + "required": [ + "number", + "name" + ], + "additionalProperties": false + }, + "description": "The steps of the workflow that call this one" + } + }, + "required": [ + "type", + "name", + "relation", + "steps" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "The type of the definition that uses this one" + }, + "name": { + "type": "string", + "description": "The name of the definition that uses this one" + }, + "relation": { + "type": "string", + "enum": [ + "sends-builds", + "asks-approval", + "remembers-runs", + "remembers-changes" + ], + "description": "sends-builds or asks-approval, for a prediction function that sends this one its builds to train or asks it to approve a version; remembers-runs or remembers-changes, for a recall function that folds its runs or its changes" + } + }, + "required": [ + "type", + "name", + "relation" + ], + "additionalProperties": false + } + ] + }, + "description": "The active definitions of the playbook that use this one, read from the playbook when the definition is read: a workflow with the steps that call it, a prediction function that sends it builds or asks it to approve, a recall function that remembers its runs or its changes" } }, "required": [ @@ -6548,8 +6901,7 @@ "created_at", "created_by", "updated_at", - "source", - "overview" + "source" ], "additionalProperties": false, "description": "A saved definition, with its document" diff --git a/docs/concepts/terminology.md b/docs/concepts/terminology.md index 6674b3a5b..3605ce7b1 100644 --- a/docs/concepts/terminology.md +++ b/docs/concepts/terminology.md @@ -53,9 +53,8 @@ A reasoning function has a prompt and a language model; it is not limited to a m | Schedule trigger | A trigger that starts a run at the times its schedule names. | | Schedule | The `schedule` key of a workflow document, which holds its triggers: a cron rule and an every period, both read in UTC, and an event trigger. | | Version | A numbered revision. A definition gets a new version each time it is saved, and each run records the version it used; a prediction function also numbers its trained models. A workflow document's own `document.version`, such as 1.0.0, is text the author writes. | -| Description | One or two sentences of what a definition does, in its document: a function's `description`, a workflow's `summary`, or its `title` where it has no summary. Lists of definitions show it, its overview opens with it, and it takes no heading, which the overview keeps for its own. | -| About | The author's own words beside the description, for a business reader: what the function or workflow is for, who it serves, when it runs and what happens. A function's `about`, a workflow's `document.metadata.about`; its overview shows it after the description, as it is written, and it takes no heading either. | -| Overview | A plain-words description of a definition for a person who does not read its document. The runtime composes one for each version it saves, from that version's document, and writes it each time the definition is read: after what the document says, the definitions a workflow calls, each with its description, then the active definitions of the playbook that use this one, a workflow with the steps that call it, and last, under Details, their exact names. | +| Description | One or two sentences of what a definition does, in its document: a function's `description`, a workflow's `summary`, or its `title` where it has no summary. Lists of definitions show it. | +| Overview | Markdown the author gives beside a definition's document for the person who runs the business: why the definition exists, what it does and how it fits. Each version keeps the overview it was saved with as it was written, and an update that gives none keeps the last. | | Run | A workflow or function carried out once against particular inputs. | | Step run | One workflow step carried out once. | | Attempt | One try at a step's work, or at a run started again under its id. A retry is another attempt, not another function. | diff --git a/docs/engineering/reference/mcp.md b/docs/engineering/reference/mcp.md index 6d5563905..850f18ed9 100644 --- a/docs/engineering/reference/mcp.md +++ b/docs/engineering/reference/mcp.md @@ -18,7 +18,7 @@ The server is also an [MCP](https://modelcontextprotocol.io) server, so an agent Most MCP clients take an entry of this shape. In Claude Code, `claude mcp add --transport http auto-runtime http://localhost:5555/mcp --header "Authorization: Bearer "` adds the same. In [local mode](../../self-host/security.md#local-mode), leave out the header. Where `PUBLIC_URL` turns sign-in on, a client that runs the MCP authorization flow connects without the header and signs the person in through the Auto identity service, as the [security guide](../../self-host/security.md#what-signed-in-people-can-do) describes. -`/mcp` serves every tool on one connection, so an agent can create a playbook and work in it at once. The org is the key's own, since a key belongs to one org, or, for a signed-in person, the org their token names, and never an argument; in local mode, where no key names one, it is `local`, so the playbooks an agent creates there are the ones `GET /v1/orgs/local/playbooks` lists. The org id `local` is reserved for local mode: the key command refuses it, and an `API_KEYS` entry with it stops the server at start-up, so no key can reach the playbooks made in local mode. The playbook tools (`create_playbook`, `list_playbooks`, `update_playbook` and `retire_playbook`), `list_models` and the connection tools (`add_connection`, `list_connections` and `remove_connection`) are as on HTTP; the five routes of Studio's page that connects a connection are HTTP alone, and so are `list_providers`, which a page reads, `get_playbook`, `get_connection`, `get_run_history`, `get_playbook_analytics` and `validate`, since `list_playbooks`, `list_connections`, `list_playbook_events` given a `run_id` and `dry_run` answer what an agent needs of them and Studio reads the rest. Every tool that works inside a playbook (`create_definition`, `list_definitions`, `get_definition`, `update_definition`, `retire_definition`, `run_definition`, `get_run`, `cancel_run`, `list_runs`, `get_event`, `list_playbook_events`, `publish_event`, `introspect`, `dry_run`, `execute`, `list_interactions`, `answer_interaction` and `send_run_event`) takes the playbook's id as a required `playbook` argument, `search` takes it as an optional one, and `get_guide` reads the guides on every endpoint: twenty-eight tools for a key that may call them all. `search` answers the tool servers the playbook may use and their tools, which an agent names in a reasoning function's `tools`, found by the words of `terms` where it gives them, and without `playbook` every server set up for the org, each with the playbooks it serves, so an agent sees what is set up before it makes a playbook, and, to a caller who may add a connection, `providers`, the providers a person may connect that no server serves yet, so an agent asked for a service no server serves can offer to connect one; it is the org's operation, `GET /v1/orgs/{org}/tool-servers/search`, which the org endpoint serves too; `introspect` reads one tool whole and `dry_run` answers what `execute` would check, its arguments against the tool's input schema among it, the three reading and calling nothing, and `execute` calls one tool as a run would and answers what the run's model would see, under `playbook:write` whatever the tool does, recorded in the playbook's history and never a run; the [HTTP reference](../../reference/http.md#tool-servers) describes them. `list_runs` and `list_playbook_events` page with `limit` and `cursor`, answer `has_more` and `next_cursor`, and read a retired playbook, and `get_event` reads one event whole, each field the 2 KiB rule of a page names, an input, output, record, answer, arguments, result or event data, whole within 64 KiB and its size beyond, since a tool result carries its output twice; the [HTTP reference](../../reference/http.md#run-history-and-playbook-events) describes their fields. A connection lists the tools its key may call, so a read-only key is offered the queries alone, and a key that may only read inside some playbooks is offered `list_playbooks` too, which lists those playbooks. Every endpoint gives a connecting agent instructions, `instructionsFor` of `@on.auto/api`, built per request for the tools listed and the definition types the server runs: what a playbook is and a sentence for each type it runs, that what the person wants kept goes in a recall function and not in the agent's own notes where the server runs recall functions, what the connection acts on, where the formats are, how a reasoning function names its model and tools, where `search` and `add_connection` are both listed that a person's accounts at services are connected through this server, by `search` with the service's words and `add_connection` with the name of a provider that matches, and that a service cannot be connected only when no provider or server matches, that `get_run` shows whether a run that finishes later ended or still waits, that an answer to what a run waits on goes to its request through `answer_interaction` and no new run, what to say of what was done and what the person can do next, in which words, and what a refusal says. The descriptions carry what the instructions leave out: `get_guide`'s names the recipes whose tools the connection lists, `execute`'s and `dry_run`'s say what each does, and those of `list_runs`, `list_playbook_events` and `list_interactions` the paging rule. With every tool the instructions take 1,993 characters on `/mcp`, 1,912 on `/orgs/{org}/mcp`, where `search` and `add_connection` are listed, and 1,618 on `/orgs/{org}/playbooks/{playbook}/mcp`, under a bound of 2,000, the most Claude Code shows of a server's instructions, as `packages/api/src/mcp/tool-calling-instructions.test.ts` pins them. `search`'s description takes 798 of the 800 characters a description may, as `packages/server/src/mcp/served-tools.test.ts` pins it. The listing of the tools takes 43,778 bytes on the wire on `/mcp`, 13,019 on the org endpoint and 30,932 on the playbook endpoint, as `packages/server/src/mcp/served-schemas.test.ts` pins them, under the 48,000, 13,100 and 40,000 the server checks when it starts, and a connection lists at most 32 tools. `get_guide`, the resources `guide://` and the prompts `first-playbook`, `remember`, `give-tools` and `schedule`, each offered where the connection lists the tools its steps call, serve the terminology, the reference page of each format and the recipes; the [API README](../../../packages/api/README.md#over-mcp) describes them, the annotations of each tool and the bounds. +`/mcp` serves every tool on one connection, so an agent can create a playbook and work in it at once. The org is the key's own, since a key belongs to one org, or, for a signed-in person, the org their token names, and never an argument; in local mode, where no key names one, it is `local`, so the playbooks an agent creates there are the ones `GET /v1/orgs/local/playbooks` lists. The org id `local` is reserved for local mode: the key command refuses it, and an `API_KEYS` entry with it stops the server at start-up, so no key can reach the playbooks made in local mode. The playbook tools (`create_playbook`, `list_playbooks`, `update_playbook` and `retire_playbook`), `list_models` and the connection tools (`add_connection`, `list_connections` and `remove_connection`) are as on HTTP; the five routes of Studio's page that connects a connection are HTTP alone, and so are `list_providers`, which a page reads, `get_playbook`, `get_connection`, `get_run_history`, `get_playbook_analytics` and `validate`, since `list_playbooks`, `list_connections`, `list_playbook_events` given a `run_id` and `dry_run` answer what an agent needs of them and Studio reads the rest. Every tool that works inside a playbook (`create_definition`, `list_definitions`, `get_definition`, `update_definition`, `retire_definition`, `run_definition`, `get_run`, `cancel_run`, `list_runs`, `get_event`, `list_playbook_events`, `publish_event`, `introspect`, `dry_run`, `execute`, `list_interactions`, `answer_interaction` and `send_run_event`) takes the playbook's id as a required `playbook` argument, `search` takes it as an optional one, and `get_guide` reads the guides on every endpoint: twenty-eight tools for a key that may call them all. `search` answers the tool servers the playbook may use and their tools, which an agent names in a reasoning function's `tools`, found by the words of `terms` where it gives them, and without `playbook` every server set up for the org, each with the playbooks it serves, so an agent sees what is set up before it makes a playbook, and, to a caller who may add a connection, `providers`, the providers a person may connect that no server serves yet, so an agent asked for a service no server serves can offer to connect one; it is the org's operation, `GET /v1/orgs/{org}/tool-servers/search`, which the org endpoint serves too; `introspect` reads one tool whole and `dry_run` answers what `execute` would check, its arguments against the tool's input schema among it, the three reading and calling nothing, and `execute` calls one tool as a run would and answers what the run's model would see, under `playbook:write` whatever the tool does, recorded in the playbook's history and never a run; the [HTTP reference](../../reference/http.md#tool-servers) describes them. `list_runs` and `list_playbook_events` page with `limit` and `cursor`, answer `has_more` and `next_cursor`, and read a retired playbook, and `get_event` reads one event whole, each field the 2 KiB rule of a page names, an input, output, record, answer, arguments, result or event data, whole within 64 KiB and its size beyond, since a tool result carries its output twice; the [HTTP reference](../../reference/http.md#run-history-and-playbook-events) describes their fields. A connection lists the tools its key may call, so a read-only key is offered the queries alone, and a key that may only read inside some playbooks is offered `list_playbooks` too, which lists those playbooks. Every endpoint gives a connecting agent instructions, `instructionsFor` of `@on.auto/api`, built per request for the tools listed and the definition types the server runs: what a playbook is and a sentence for each type it runs, that what the person wants kept goes in a recall function and not in the agent's own notes where the server runs recall functions, what the connection acts on, where the formats are and that each definition is given an overview, how a reasoning function names its model and tools, where `search` and `add_connection` are both listed that a person's accounts at services are connected through this server, by `search` with the service's words and `add_connection` with the name of a provider that matches, and that a service cannot be connected only when no provider or server matches, that `get_run` shows whether a run that finishes later ended or still waits, that an answer to what a run waits on goes to its request through `answer_interaction` and no new run, what to say of what was done and what the person can do next, in which words, and what a refusal says. The descriptions carry what the instructions leave out: `get_guide`'s names the recipes whose tools the connection lists, `execute`'s and `dry_run`'s say what each does, and those of `list_runs`, `list_playbook_events` and `list_interactions` the paging rule. With every tool the instructions take 1,996 characters on `/mcp`, 1,912 on `/orgs/{org}/mcp`, where `search` and `add_connection` are listed, and 1,621 on `/orgs/{org}/playbooks/{playbook}/mcp`, under a bound of 2,000, the most Claude Code shows of a server's instructions, as `packages/api/src/mcp/tool-calling-instructions.test.ts` pins them. `search`'s description takes 798 of the 800 characters a description may, as `packages/server/src/mcp/served-tools.test.ts` pins it. The listing of the tools takes 44,771 bytes on the wire on `/mcp`, 13,019 on the org endpoint and 31,925 on the playbook endpoint, as `packages/server/src/mcp/served-schemas.test.ts` pins them, under the 48,000, 13,100 and 40,000 the server checks when it starts, and a connection lists at most 32 tools. `get_guide`, the resources `guide://` and the prompts `first-playbook`, `remember`, `give-tools` and `schedule`, each offered where the connection lists the tools its steps call, serve the terminology, the reference page of each format and the recipes; the [API README](../../../packages/api/README.md#over-mcp) describes them, the annotations of each tool and the bounds. | Endpoint | Tools | Use it | | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | diff --git a/docs/guides/computation.md b/docs/guides/computation.md index ceed28367..038ec48a3 100644 --- a/docs/guides/computation.md +++ b/docs/guides/computation.md @@ -49,11 +49,11 @@ Check the document before you approve it: - The rules of [Keep every figure exact](#keep-every-figure-exact): a `maximum` on each amount and count, a `minimum` on each value the program divides by, and a whole number at the end of each division. Ask the agent to name the largest number the program forms and show the arithmetic. - The program is a function from `Input` to `Output`, exported as the default (`export default function`). -## 2. Save it and read its overview +## 2. Save it with its overview The agent saves it with `create_definition` and the type `computation`. Saving checks the program with the TypeScript compiler. A problem is refused with its line, counted from the top of the document, and the compiler's words. -Check that the agent confirms the function at version 1. Its `overview` lists the fields the function needs and produces in plain words, for a reader who does not read TypeScript. +Check that the agent confirms the function at version 1, and that it saved its `overview`: Markdown for the person who runs the business, on why the function exists, what it does and how it fits, for a reader who does not read TypeScript. ## 3. Run it with an input you can check by hand diff --git a/docs/reference/computation-format.md b/docs/reference/computation-format.md index 980997d36..e872f5670 100644 --- a/docs/reference/computation-format.md +++ b/docs/reference/computation-format.md @@ -115,7 +115,6 @@ the run succeeds with this output, and its record shows that it spent 0 checkpoi | Field | Required | Value | | --------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `description` | No | Text of 1 to 1,000 characters | -| `about` | No | Markdown of 1 to 4,000 characters, without headings, for a business reader; see [The overview](#the-overview) | | `language` | Yes | `typescript`, the playbook's one language | | `input.schema` | No | A JSON Schema of the input. The input may be any JSON value it allows | | `output.schema` | No | A JSON Schema of the output. An output that does not match it ends the run as a conflict | @@ -397,29 +396,9 @@ When the computation function's run is `unavailable`, the `compute` task retries ## The overview -`get_definition` returns a saved function with its `overview`: the function in plain words, for a person who does not read TypeScript. Auto Studio shows it before the document. It tells what happens, and holds the technical facts after it, under **Details**. For the example above, it opens: +`create_definition` and `update_definition` take an `overview` beside the document: Markdown for the person who runs the business, written from everything known of them and of the playbook. It says why the function exists, what it does and how it fits, in one to three short paragraphs, with a Mermaid diagram in a fenced block where a flow is worth a picture; less technical, more value. Write it after the document, and write it again with every update. -> Spend, pace and projection per campaign, in cents, for a reporting period. -> -> It is given the rows and the period, and a program works out the answer, the same every time for the same input: the campaigns and the total spend cents. - -It says: - -1. The `description`, as a sentence, or that the function has none yet, then the `about` as it is written. -2. What the function is given and what it answers with, each field by its meaning: the first clause of the field's `description`, such as "the month" for `The month, such as April 2026`, where that clause begins with an article or a question word and is short, or else the field's name in words, such as "the total spend cents". An output that declares a shape is named by it, such as "a series over time". -3. **Used by**: the active definitions of the playbook that use this function, read each time it is returned, and left out when there are none. -4. Under **Details**: each field of `input.schema` and `output.schema` with its name, its description, its type, bounds and default, or the shape `output.shape` names, and that the program is TypeScript, in the definition's document. The program itself is not described. - -The runtime composes the overview from the document when it is saved, and writes it at each read with the definitions that use the function, so the rest says only what the document says. Write the `description` in one or two sentences, and give each field of the schemas a `description` that begins with an article where the field names a thing, such as `The reporting period`, so the overview can name the field by it. Write the `about` for a business reader: what the function is for, who it serves, when it runs and what happens. Neither it nor the `description` takes a heading, since the overview writes its own, and a save refuses one at its line. Write it as a literal block scalar, `about: |` with its lines indented under it, so that a `#` in it is text and each of its lines stays a line: - -```yaml -about: | - The finance team reads it to see whether each campaign spends to plan. - A workflow runs it at the end of each reporting period, and gets back - the spend, the pace and the projection of every campaign. -``` - -The program may give a field a value of its own where the input leaves it out. Write that value as the field's `default` in `input.schema`, so the overview can say it. A function without them still has an overview, with the names of its fields alone. +An overview holds 1 character to 16,384 bytes in UTF-8. The version keeps it as written, and `get_definition` returns it so until an update gives another; an update that leaves it out keeps the overview saved. `list_definitions` leaves it out. [Reasoning functions](http.md#reasoning-functions) gives the rules every definition follows, with `used_by`, the definitions of the playbook that use this one. ## Availability diff --git a/docs/reference/http.md b/docs/reference/http.md index feb7b5374..facd08e8b 100644 --- a/docs/reference/http.md +++ b/docs/reference/http.md @@ -119,10 +119,10 @@ These routes are relative to `/v1/orgs/{org}/playbooks/{playbook}`: | Operation | Method and route | Permission | Input | | ------------------- | ------------------------------------------- | ---------------- | ---------------------------------------- | -| `create_definition` | `POST /definitions/reasoning` | `playbook:write` | `name`, `source` | +| `create_definition` | `POST /definitions/reasoning` | `playbook:write` | `name`, `source`, optional `overview` | | `list_definitions` | `GET /definitions/reasoning` | `playbook:read` | Optional `include_retired` | | `get_definition` | `GET /definitions/reasoning/{name}` | `playbook:read` | Name in path | -| `update_definition` | `PUT /definitions/reasoning/{name}` | `playbook:write` | `source` | +| `update_definition` | `PUT /definitions/reasoning/{name}` | `playbook:write` | `source`, optional `overview` | | `retire_definition` | `POST /definitions/reasoning/{name}/retire` | `playbook:write` | Name in path | | `run_definition` | `POST /definitions/reasoning/{name}/run` | `playbook:write` | Optional `input`, optional UUID `run_id` | | `get_run` | `GET /runs/{run_id}` | `playbook:read` | Run id in path | @@ -132,18 +132,36 @@ The `source` is a [reasoning function document](reasoning-format.md). These rule - A name follows the playbook id rule: 3 to 48 lowercase letters, digits and hyphens, beginning with a letter. - A name is unique within its type and playbook, and cannot be reused after retirement. - A source document may be at most 65,536 UTF-8 bytes. -- Changing a document creates a version. Updating it with identical source records no change. +- Changing a document or its overview creates a version. Updating it with identical source, and the same overview or none, records no change. - A run uses the latest active version. - A retired definition can be read, but cannot be edited or run. - A definition carries its `input_schema` and `output_schema` where its document declares them. A reasoning function, a computation function or a workflow whose `output.shape` names an [output shape](output-shapes.md) has the shape's schema as its `output_schema`, and the shape's name as `details.shape`, such as `"details": { "shape": "series" }`; `details` is left out otherwise. A prediction function has the `series` shape so, since its questions answer a series. `list_definitions` shows both too. -`create_definition`, `update_definition`, `get_definition` and `retire_definition` return the definition with its `overview`; `list_definitions` leaves it out. The overview describes the definition in plain words, for a person who does not read its document: +`create_definition`, `update_definition`, `get_definition` and `retire_definition` return the definition with its `overview` and its `used_by`; `list_definitions` leaves both out. -- It is Markdown with headings, lists, tables of its fields and, where one helps, a diagram. It says what the definition needs, what it produces and what it does, as its type has it. For a reasoning function, it also gives the model, the tools and what the model is told. -- The runtime composes it from the document when it saves the version, and writes it each time the definition is returned. -- A workflow's overview then says **What it calls**: each definition its `run_definition` steps name, with the first paragraph of its description, or that it is retired or not in the playbook yet. -- **Used by** follows, a line for each active definition of the playbook that uses this one: a workflow whose `run_definition` calls it, with the number and the name of each step that does, a prediction function that sends it builds to train or asks it to approve a version, or a recall function with a filter on its runs (by their `subject`) or on its changes (by their `source`). -- Both are read each time the definition is returned, so they are as current as the playbook, and each is left out when it has nothing to say. Their exact names, each with its type, close the overview under **Details**. +- `overview` is Markdown its author gives beside `source` for the person who runs the business: why the definition exists, what it does and how it fits, in one to three short paragraphs, with a Mermaid diagram in a fenced block where a flow is worth a picture. It holds 1 character to 16,384 UTF-8 bytes. The version keeps it as written, and an update that leaves it out keeps the overview saved. A definition saved without one has none. +- `used_by` lists the active definitions of the playbook that use this one, read from the playbook each time the definition is returned, and is left out when none does. Each gives its `type`, its `name` and its `relation`: `calls` for a workflow whose `run_definition` steps call it, with those `steps`, each its `number`, its place among the workflow's tasks in the order of its document, and its `name`; `sends-builds` or `asks-approval` for a prediction function that sends it builds to train or asks it to approve a version; `remembers-runs` or `remembers-changes` for a recall function with a filter on its runs (by their `subject`) or on its changes (by their `source`). A definition that uses this one in two ways is listed for each. + +The interaction function `send-build` that a prediction function trains through, that a workflow calls in two steps and that two recall functions follow is returned with this `used_by`, shown here without its other fields: + +```json +{ + "used_by": [ + { "type": "prediction", "name": "weekly-orders", "relation": "sends-builds" }, + { "type": "recall", "name": "build-answers", "relation": "remembers-runs" }, + { "type": "recall", "name": "build-changes", "relation": "remembers-changes" }, + { + "type": "workflow", + "name": "weekly-build", + "relation": "calls", + "steps": [ + { "number": 1, "name": "send" }, + { "number": 3, "name": "again" } + ] + } + ] +} +``` ## Interaction functions diff --git a/docs/reference/interaction-format.md b/docs/reference/interaction-format.md index 9cc4315a2..6d3349ee0 100644 --- a/docs/reference/interaction-format.md +++ b/docs/reference/interaction-format.md @@ -201,7 +201,6 @@ A request sent through a tool is attempted at once. After a failed attempt it is | Field | Purpose | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `description` | Optional explanation, 1 to 1,000 characters | -| `about` | Optional Markdown of 1 to 4,000 characters, without headings, for a business reader; the [overview](#the-overview) shows it after the `description` | | `call` | The tool the function asks: `server`, `tool`, `with` and `read`; see [`call`](#call). With it, `output.schema` is required and `to`, `expires`, `deliver`, `replies`, `from`, `reply` and the body are refused | | `to` | Required without `call`, refused with it. A template that renders the party the request goes to: a name the playbook's readers filter by, which a delivery's templates may use as the tool's address | | `from` | Optional. A template, as `to` is, that renders the party whose reply counts, the answerer; `to` when left out. A request sent to a room names the person here | @@ -216,16 +215,16 @@ A request sent through a tool is attempted at once. After a failed attempt it is ### Fields by shape -| Field | Asks a system | Asks through the inbox | Asks through a tool | -| -------------------------------------- | ------------- | ------------------------------------------------ | ---------------------------------------------------------------- | -| `call` | Required | Refused | Refused | -| `to`, `expires`, the message | Refused | Required | Required | -| `from` | Refused | Optional | Optional | -| `deliver` | Refused | Left out | Required | -| `replies` | Refused | Refused | Optional; needs `deliver.sent`, `output.schema` and a reply rule | -| `output.schema` | Required | Optional; without it, a notification | Optional; without it, a notification | -| `reply` | Refused | Optional, with `output.schema`; nothing reads it | Optional, with `output.schema`; read only with `replies` | -| `description`, `about`, `input.schema` | Optional | Optional | Optional | +| Field | Asks a system | Asks through the inbox | Asks through a tool | +| ----------------------------- | ------------- | ------------------------------------------------ | ---------------------------------------------------------------- | +| `call` | Required | Refused | Refused | +| `to`, `expires`, the message | Refused | Required | Required | +| `from` | Refused | Optional | Optional | +| `deliver` | Refused | Left out | Required | +| `replies` | Refused | Refused | Optional; needs `deliver.sent`, `output.schema` and a reply rule | +| `output.schema` | Required | Optional; without it, a notification | Optional; without it, a notification | +| `reply` | Refused | Optional, with `output.schema`; nothing reads it | Optional, with `output.schema`; read only with `replies` | +| `description`, `input.schema` | Optional | Optional | Optional | A function that reads replies needs a reply rule: the one `reply` writes, or one made from the answer schema. Without either, it is refused when saved. @@ -549,32 +548,9 @@ The run ends `succeeded` with the answer, such as `{"verdict": "approve"}`, as i ## The overview -`get_definition` returns a saved function with its `overview`: the function in plain words, for a person who does not read Liquid or YAML. Auto Studio shows it before the document. It opens with the `description`, as a sentence, or says the function has none yet, then the `about` as it is written. It then tells what happens, and names each field by its meaning: the first clause of the field's `description`, such as "the month" for `The month, such as April 2026`, where that clause begins with an article or a question word and is short, or else the field's name in words. The technical facts follow under **Details**. +`create_definition` and `update_definition` take an `overview` beside the document: Markdown for the person who runs the business, written from everything known of them and of the playbook. It says why the function exists, what it does and how it fits, in one to three short paragraphs, with a Mermaid diagram in a fenced block where a flow is worth a picture; less technical, more value. Write it after the document, and write it again with every update. -For a function that asks a system, the overview says in one paragraph what it is given, the server and the tool it asks once per run, and what it answers with: the part of the tool's answer that `read` points at, and its fields. Under Details: each field of `input.schema` and `output.schema` with its type and its bounds, and the call: the tool, its server, `read` and each argument as written. - -For a function that asks a person, it says: - -1. For a function that reads replies by their first word, a diagram: the message, the wait, the first word of a reply deciding each value, and the request that nobody answers. -2. What it is given and whom it sends the message: the meaning of the value `to` reads where `to` is one value, such as `{{ input.owner }}`, `to` itself where it is text, and the party named under Details otherwise. -3. The message, quoted as the person receives it. Each value it reads is in brackets, by its meaning, such as `[the draft report]` for `{{ input.draft }}`, so the author's own bold and italics stay as written. -4. How long it waits and how far apart it reads the replies; how a reply gives each property of the answer, such as "a reply whose first word is approve gives the verdict approve"; that a reply that begins with another word is not taken, and the person told how to answer where `tell` says how; and that a request nobody answers by its expiry ends unanswered. A request in the inbox says that whoever may write to the playbook answers it, and a notification that it waits for nothing. -5. What it answers with: each field of `output.schema`, a field the answer may leave out "when there is one". - -Under Details: each field of `input.schema` and `output.schema` with its type and its bounds, or that a notification takes no answer, and where the request goes: `to` and `from`, the tools and servers of `deliver` and `replies`, and `wait` and `expires`, each as written. - -Either kind ends with **Used by**: the active definitions of the playbook that use this function, read each time it is returned, and left out when there are none. - -The runtime composes the overview from the document when it is saved, and writes it at each read with the definitions that use the function, so the rest says only what the document says. Write the `description` in one or two sentences, and give each field of the schemas a `description` that begins with an article where the field names a thing, such as `The campaign to approve`, so the overview can name the field by it. Write the `about` for a business reader: what the function is for, who it serves, when it runs and what happens. Neither it nor the `description` takes a heading, since the overview writes its own, and a save refuses one at its line. Write it as a literal block scalar, `about: |` with its lines indented under it, so that a `#` in it is text and each of its lines stays a line: - -```yaml -about: | - The campaign owner approves each brief before it goes to a client. - It runs when a brief is ready, waits two days for the owner's answer, - and answers with their decision and their note. -``` - -A function without them still has an overview, with the names of its fields alone. +An overview holds 1 character to 16,384 bytes in UTF-8. The version keeps it as written, and `get_definition` returns it so until an update gives another; an update that leaves it out keeps the overview saved. `list_definitions` leaves it out. [Reasoning functions](http.md#reasoning-functions) gives the rules every definition follows, with `used_by`, the definitions of the playbook that use this one. ## Availability diff --git a/docs/reference/mcp.md b/docs/reference/mcp.md index f1f05f2e5..a93bcd5c3 100644 --- a/docs/reference/mcp.md +++ b/docs/reference/mcp.md @@ -35,7 +35,7 @@ An agent that connects receives instructions written for the tools its key or to - what a playbook is, in the words of the [terminology](../concepts/terminology.md), with a sentence for each function type the runtime runs and one for workflows; - that what the person wants kept, for anyone or a workflow to ask later, goes in a recall function of the playbook and not in the agent's own notes; - what the connection acts on: the caller's own org on `/mcp`, one org's playbooks on `/orgs/{org}/mcp`, or one playbook on `/orgs/{org}/playbooks/{playbook}/mcp`; -- that `get_guide` returns the format of each definition; +- that `get_guide` returns the format of each definition, and that each definition is given an overview; - how a reasoning function names its model and its tools; - where `search` and `add_connection` are both listed, that a person's accounts at services are connected through Auto Runtime: asked to connect, link, sign in to or use a service, the agent calls `search` with the service's words, which lists the providers that match, then `add_connection` with a provider's name, which the person approves on the page it links. The agent says that a service cannot be connected only when no provider or server matches; - that `get_run` shows whether a run that finishes later has ended or is still waiting; @@ -112,15 +112,15 @@ Each playbook `list_playbooks` returns holds every field of the playbook, a reti These tools are listed on `/mcp` and a playbook endpoint. Their routes are relative to `/v1/orgs/{org}/playbooks/{playbook}`, where `{type}` is the tool's `type` argument. -| Tool | HTTP operation | What it is for | -| ------------------- | ---------------------------------------- | -------------------------------------------------------------------- | -| `create_definition` | `POST /definitions/{type}` | Save a new function or workflow from its document, given as `source` | -| `list_definitions` | `GET /definitions/{type}` | List the definitions of one type, without their `overview` | -| `get_definition` | `GET /definitions/{type}/{name}` | Read a definition, with its document and its `overview` | -| `update_definition` | `PUT /definitions/{type}/{name}` | Save a new version from a changed document | -| `retire_definition` | `POST /definitions/{type}/{name}/retire` | Retire a definition for good | +| Tool | HTTP operation | What it is for | +| ------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------- | +| `create_definition` | `POST /definitions/{type}` | Save a new function or workflow from its document, given as `source`, and its `overview` | +| `list_definitions` | `GET /definitions/{type}` | List the definitions of one type, without their `overview` and `used_by` | +| `get_definition` | `GET /definitions/{type}/{name}` | Read a definition, with its document, its `overview` and `used_by` | +| `update_definition` | `PUT /definitions/{type}/{name}` | Save a new version from a changed document or overview | +| `retire_definition` | `POST /definitions/{type}/{name}/retire` | Retire a definition for good | -The `overview` describes a definition in plain words, so an agent can show a person what it does without its source. `get_definition`, `create_definition`, `update_definition` and `retire_definition` return it. It is written each time it is returned, from what the version's document says and what the playbook holds then: for a workflow, **What it calls**, each definition it names with its description; then **Used by**, the active definitions of the playbook that use this one, a workflow with the steps that call it; and last, under **Details**, their exact names with their types. [Reasoning functions](http.md#reasoning-functions) gives the rules every type of definition follows, and the section of each type adds its own. +`create_definition` and `update_definition` take an `overview` beside `source`: Markdown for the person who runs the business, written from everything the agent knows of them and of the playbook, on why the definition exists, what it does and how it fits, in one to three short paragraphs, with a Mermaid diagram in a fenced block where a flow is worth a picture. The agent writes it after the document and again with every update; one left out of an update stays as saved. `get_definition`, `create_definition`, `update_definition` and `retire_definition` return it as written, and with it `used_by`, the active definitions of the playbook that use this one, each with its `type`, its `name` and its `relation`, a workflow with the `steps` that call it, such as `{ "type": "workflow", "name": "weekly-build", "relation": "calls", "steps": [{ "number": 1, "name": "send" }] }`. [Reasoning functions](http.md#reasoning-functions) gives the rules every type of definition follows and each relation of `used_by`, and the section of each type adds its own. ### Runs diff --git a/docs/reference/output-shapes.md b/docs/reference/output-shapes.md index 6a7357e90..acb95a779 100644 --- a/docs/reference/output-shapes.md +++ b/docs/reference/output-shapes.md @@ -41,7 +41,6 @@ A reasoning function's shape is sent to the provider as an output schema of the - `output_schema` is the shape's schema. Its `$id` is `https://on.auto/shapes/` followed by the shape's name, such as `https://on.auto/shapes/series`, and its `title` is the name. - `details.shape` is the shape's name: what the definition answers. -- The [overview](http.md#reasoning-functions) says the shape in place of a table of its fields: in **What it produces** for a reasoning function and a workflow, and for a computation function in what it answers with, such as "a series over time", and whole under **Details**. The overview of a prediction function says its series in **What it predicts**, in the same words for what it holds: "It answers the last 8 observed weeks and 1 and 4 weeks ahead, each with a likely value, a low and a high." ## What a run shows diff --git a/docs/reference/prediction-format.md b/docs/reference/prediction-format.md index 37a440577..b15081d37 100644 --- a/docs/reference/prediction-format.md +++ b/docs/reference/prediction-format.md @@ -83,7 +83,6 @@ Before you write one, settle these with the person: | Field | Meaning | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `description` | Optional; what the function predicts, 1 to 1,000 characters | -| `about` | Optional; Markdown of 1 to 4,000 characters, without headings, for a business reader; see [The overview](#the-overview) | | `predicts.target` | Required; the name of the quantity, which names its column in the dataset | | `predicts.period` | Required; `day`, `week`, `month`, `quarter` or `year` | | `predicts.season` | Optional; the length of a season in periods, 2 to 366, such as 52 for weeks over a year | @@ -691,17 +690,9 @@ A workflow on a [schedule](workflow-format.md#triggers) can ask for a build on t ## The overview -`get_definition` returns a saved function with its `overview`: the function in plain words, for a person who does not read its document. Auto Studio shows it before the document. The overview says, in order: +`create_definition` and `update_definition` take an `overview` beside the document: Markdown for the person who runs the business, written from everything known of them and of the playbook. It says why the function exists, what it does and how it fits, in one to three short paragraphs, with a Mermaid diagram in a fenced block where a flow is worth a picture; less technical, more value. Write it after the document, and write it again with every update. -1. The `description`, or that the function has none yet, then the `about` as it is written. -2. What the function predicts: the target, its unit, its period and its horizons, or the period it sums to with `accumulates`. Then its season, what a question names, with the given features among it, and its answer in the words of the series shape, such as "It answers the last 8 observed weeks and 1 and 4 weeks ahead, each with a likely value, a low and a high." -3. What it learns from. For a projected dataset: each filter with the attributes and the `data` test it names, where the time and the series are read, and each column of a row with how it is aggregated. For an uploaded dataset: its digest and its columns. For either: the window a build reads. -4. Who trains it: the party of `source.to` in the inbox, or the interaction function of `source.through`, in bold. Then how long a build waits for its candidates, how long each is scored, and the brief as a person reads it, read as [the overview of a reasoning function](reasoning-format.md#the-overview) reads its message. -5. How a version goes live: the holdout, the metric, the coverage floor, the baselines and the confidence a candidate must pass. Then the shadow a later version runs over a projected dataset, and whether a version activates on its own or once it is approved through the interaction function that `evaluation.activate.approved` names. -6. The rules, when there are any. -7. **Used by**: the active definitions of the playbook that use this function, read each time it is returned, and left out when there are none. - -The runtime composes the overview from the document when it is saved, and writes it at each read with the definitions that use the function. Write the `description` in one or two sentences. Write the `about` for a business reader: what the function is for, who it serves, when it runs and what happens. Neither it nor the `description` takes a heading, since the overview writes its own. Write it as a literal block scalar, `about: |`, so that a `#` in it is text. +An overview holds 1 character to 16,384 bytes in UTF-8. The version keeps it as written, and `get_definition` returns it so until an update gives another; an update that leaves it out keeps the overview saved. `list_definitions` leaves it out. [Reasoning functions](http.md#reasoning-functions) gives the rules every definition follows, with `used_by`, the definitions of the playbook that use this one. ## Availability diff --git a/docs/reference/reasoning-format.md b/docs/reference/reasoning-format.md index 6b1c222b6..b5064f4d5 100644 --- a/docs/reference/reasoning-format.md +++ b/docs/reference/reasoning-format.md @@ -36,8 +36,7 @@ A function document contains instructions, never model credentials. Each change | Field | Required | Value | | -------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `description` | No | Text of 1 to 1,000 characters. `list_definitions`, `get_definition` and the [overview](#the-overview) show it | -| `about` | No | Markdown of 1 to 4,000 characters, without headings, for a business reader; the [overview](#the-overview) shows it after the `description` | +| `description` | No | Text of 1 to 1,000 characters. `list_definitions` and `get_definition` show it | | `model` | Yes | A model reference in `provider/model` form, such as `anthropic/claude-sonnet-4-5` | | `config.max_output_tokens` | No | A whole number from 1 to 64,000; 1,024 when left out. The most tokens the answer may use | | `config.temperature` | No | A finite number: the sampling temperature. Each provider accepts its own range | @@ -359,32 +358,9 @@ More rules: ## The overview -`get_definition` returns a saved function with its `overview`: a plain-words description for a person who does not read Liquid or YAML. Auto Studio shows it before the document. The overview says, in order: +`create_definition` and `update_definition` take an `overview` beside the document: Markdown for the person who runs the business, written from everything known of them and of the playbook. It says why the function exists, what it does and how it fits, in one to three short paragraphs, with a Mermaid diagram in a fenced block where a flow is worth a picture; less technical, more value. Write it after the document, and write it again with every update. -1. The `description`, or that the function has none yet, then the `about` as it is written. -2. What the function needs: each field of `input.schema`, with its type, its bounds and the value `input.default` gives it. -3. What it produces: text, each field of `output.schema`, or the shape `output.shape` names, in a sentence. -4. The model, and each tool it may call. When the function names any tools, a diagram draws the function, its model and those tools. -5. What the model is told: the instructions and the message, as a person reads them. -6. **Used by**: the active definitions of the playbook that use this function, read each time it is returned, and left out when there are none. - -In the instructions and the message: - -- Each value the template reads, such as `{{ input.brief }}`, appears as its name in bold, **brief**, without its filters. -- A condition or a loop that ends on a later line than it opens reads as lines. `{% if input.vip %}` becomes an italic line, _When input.vip:_. `{% unless %}` becomes _Unless …:_, `{% elsif %}` _Or when …:_ and `{% else %}` _Otherwise:_. `{% for item in input.items %}` becomes _For each item of input.items:_. -- A condition or a loop that opens and ends within one line reads as a phrase of that line. `{% if input.vip %}Dear {% else %}Hello {% endif %}` reads as Dear when input.vip, otherwise Hello. `{% for item in input.items %}{{ item.name }}{% unless forloop.last %}, {% endunless %}{% endfor %}` reads as each item of input.items as **name**, separated by commas. -- Comments are left out, a `raw` block keeps its text, and any other tag stays as it is written. -- The instructions and the message are each cut past 8,192 characters, with the line of the document where they continue. - -The runtime composes the overview from the document when it is saved, and writes it at each read with the definitions that use the function, so the rest says only what the document says. Write the `description` in one or two sentences. Give each field of the schemas a `description` that begins with an article where the field names a thing, such as `The brief to review`, and put each field's default value in `input.default`, so the overview can show it. Write the `about` for a business reader: what the function is for, who it serves, when it runs and what happens. Neither it nor the `description` takes a heading, since the overview writes its own, and a save refuses one at its line. Write it as a literal block scalar, `about: |` with its lines indented under it, so that a `#` in it is text and each of its lines stays a line: - -```yaml -about: | - The campaign team uses it to check each brief before it goes to a client. - It runs when someone asks for a review, and answers with the evidence the brief lacks. -``` - -A function without them still has an overview, with only the types. +An overview holds 1 character to 16,384 bytes in UTF-8. The version keeps it as written, and `get_definition` returns it so until an update gives another; an update that leaves it out keeps the overview saved. `list_definitions` leaves it out. [Reasoning functions](http.md#reasoning-functions) gives the rules every definition follows, with `used_by`, the definitions of the playbook that use this one. ## Provider options diff --git a/docs/reference/recall-format.md b/docs/reference/recall-format.md index b3fa5cba4..c54241774 100644 --- a/docs/reference/recall-format.md +++ b/docs/reference/recall-format.md @@ -201,7 +201,6 @@ export function fold(view: View, event: Event): View { | Field | Purpose | | --------------- | -------------------------------------------------------------------------------------------------------- | | `description` | Optional; 1 to 1,000 characters | -| `about` | Optional; Markdown of 1 to 4,000 characters, without headings; see [The overview](#the-overview) | | `language` | Required; `typescript`, the playbook's one language | | `source.events` | Required; 1 to 8 filters, each naming events the fold takes; see [The events](#the-events) | | `view.initial` | Optional; the view before any event, any JSON value within the bound on a view; `null` when left out | @@ -549,16 +548,9 @@ do: ## The overview -`get_definition` returns a saved function with its `overview`: the function in plain words, for a person who does not read TypeScript or YAML. Auto Studio shows it before the document. The overview says, in order: +`create_definition` and `update_definition` take an `overview` beside the document: Markdown for the person who runs the business, written from everything known of them and of the playbook. It says why the function exists, what it does and how it fits, in one to three short paragraphs, with a Mermaid diagram in a fenced block where a flow is worth a picture; less technical, more value. Write it after the document, and write it again with every update. -1. The `description`, or that the function has none yet, then the `about` as it is written. -2. What the function remembers: each filter as a sentence, such as "each time a run of the reasoning function **review-brief** succeeds" for the example at the top of this page. A filter without a `subject` names every run, and one for a definition's event without a `source` names every definition. Each other attribute and the `data` a filter names are said with it, an expression as it is written. -3. What it keeps: the view's schema in words, with its bounds, such as `maxProperties` and `maxItems`, and `view.initial` unless it is empty. -4. What it answers: each field of `input.schema` and `output.schema`, or the view itself for a module without `answer`. -5. How the view is kept: a diagram from each filter to the view and from the view to the answer, with one line that the program is TypeScript and is shown under Source. The module itself is not described. -6. **Used by**: the active definitions of the playbook that use this function, read each time it is returned, and left out when there are none. - -The runtime composes the overview from the document when it is saved, and writes it at each read with the definitions that use the function, so the rest says only what the document says. Write the `description` in one or two sentences, and give each field of the schemas a `description` that begins with an article where the field names a thing, such as `The campaign to look up`, so the overview can say it. Write the `about` for a business reader: what the function is for, who it serves, when it runs and what happens. Neither it nor the `description` takes a heading, since the overview writes its own, and a save refuses one at its line. Write it as a literal block scalar, `about: |` with its lines indented under it, so that a `#` in it is text and each of its lines stays a line. A function without them still has an overview, with the types alone. +An overview holds 1 character to 16,384 bytes in UTF-8. The version keeps it as written, and `get_definition` returns it so until an update gives another; an update that leaves it out keeps the overview saved. `list_definitions` leaves it out. [Reasoning functions](http.md#reasoning-functions) gives the rules every definition follows, with `used_by`, the definitions of the playbook that use this one. ## Availability diff --git a/docs/reference/workflow-format.md b/docs/reference/workflow-format.md index 0e541e36e..f403ae039 100644 --- a/docs/reference/workflow-format.md +++ b/docs/reference/workflow-format.md @@ -139,7 +139,6 @@ An approval, or any other question for a person or a system, is an interaction f | `document.namespace`, `document.name` | Required names of the document | | `document.version` | Required semantic version of the document, such as `1.0.0` | | `document.title`, `document.summary` | Optional; the summary, or else the title, is the definition's `description` | -| `document.metadata.about` | Optional Markdown of 1 to 4,000 characters, without headings, for the overview | | `input.schema.document` | Optional inline JSON Schema, published as the definition's `input_schema` | | `input.from` | Optional expression or template shaping the run's input before the first task | | `evaluate.language`, `evaluate.mode` | Optional; only `typescript` and `strict` ([Expressions](#expressions)) | @@ -591,33 +590,9 @@ These are refused when a document is saved, each for the reason given: ## The overview -`get_definition` returns a saved workflow with its `overview`: the workflow in plain words, for a person who does not read YAML. Auto Studio shows it before the document. The overview says, in order: +`create_definition` and `update_definition` take an `overview` beside the document: Markdown for the person who runs the business, written from everything known of them and of the playbook. It says why the workflow exists, what it does and how it fits, in one to three short paragraphs, with a Mermaid diagram in a fenced block where a flow is worth a picture; less technical, more value. Write it after the document, and write it again with every update. -1. The `summary`, or else the `title`, or that the workflow has no description yet, then `document.metadata.about` as it is written. -2. What starts it: each trigger of its `schedule` in words, and a call of `run_definition`, which can always start it. A `cron` reads as a time in UTC and its days where it reads plainly, and as written otherwise. -3. What it needs and produces: each field of `input.schema.document` and `output.schema.document`, with its type and its `examples` unless its `description` gives one, or the shape `output.shape` names, in a sentence. -4. What it does: a flowchart of its tasks, with the same steps beneath it as a numbered list in words. -5. What it calls: each definition a `call: run_definition` names, with its type and its description, read each time it is returned, and left out when the workflow names no definition. -6. **Used by**: the active definitions of the playbook that use this workflow, read each time it is returned, and left out when there are none. - -In the flowchart: - -- Each trigger is an entry node. Each task is a step, numbered in document order as in the list. -- A `call` is a node named for the definition it runs, such as `Run review-campaign-brief`. A `switch` is a decision with an edge for each case, and a `raise` an end node. A `set`, `listen`, `wait`, `emit` or `run` is a node in words. -- A `for`, `do`, `fork` or `try` is a group around its tasks. A `for` draws an edge back for the next item. A `fork` starts each branch at once, and its group says when the first to finish wins. A `try` draws its `catch` as a dashed edge from each of its steps to the steps of `catch.do`, or to the step after it when `catch.do` names none. -- Edges follow document order and each `then`, and pass around a task whose `if` may not hold. - -The runtime composes the overview from the document when it is saved, and writes it at each read with the definitions the workflow calls and those that use it. Write the `summary` in one or two sentences, and each schema field's `description` to begin with an article where the field names a thing. Write `document.metadata.about` for a business reader: what the workflow is for, who it serves, when it runs and what happens. Neither it nor the `summary` takes a heading, nor the `title` where it stands in for the summary, which a save refuses at its line. Write it as a literal block scalar, `about: |`, so that a `#` in it is text: - -```yaml -document: - metadata: - about: | - The campaign team uses it to have each brief reviewed and approved. - It runs when a brief is submitted, and ends with the owner's decision. -``` - -Name each task for what it does, since the steps carry those names. +An overview holds 1 character to 16,384 bytes in UTF-8. The version keeps it as written, and `get_definition` returns it so until an update gives another; an update that leaves it out keeps the overview saved. `list_definitions` leaves it out. [Reasoning functions](http.md#reasoning-functions) gives the rules every definition follows, with `used_by`, the definitions of the playbook that use this one. A definition the workflow calls names the workflow's steps there by the names of their tasks, so name each task for what it does. ## How a run ends diff --git a/packages/api/README.md b/packages/api/README.md index c6a6b8ff1..ff9a6299c 100644 --- a/packages/api/README.md +++ b/packages/api/README.md @@ -148,7 +148,7 @@ So invalid arguments are a tool result with `isError` and an `invalid_input` pro **Instructions.** Every endpoint answers `initialize` with instructions built by `instructionsFor` in `src/mcp/instructions.ts`. They establish that Auto Runtime executes playbooks, Studio manages them, and the agent is a separate client. Each playbook contains one brain of reusable functions and its workflows; functions run directly or in workflows, and runs belong to the playbook. -The instructions are composed from the tools the caller may use and the registered definition types. They explain connection scope, put what the person wants kept in a recall function rather than the agent's own notes where the server runs recall functions, require reading the format before writing a definition, name model and tool discovery where offered, and direct an answer to an outstanding interaction back to the same run. Where `search` and `add_connection` are both listed, they say that a person's accounts at services are connected through this server: asked to connect, link, sign in to or use a service, the agent calls `search` with its words, which lists the providers that match, then `add_connection` with a provider's name, which the person approves on the page it links, and says a service cannot be connected only when no provider or server matches. Scoped connections and read-only callers receive only applicable instructions. `src/mcp/instructions.test.ts` checks these combinations and the 2,000-character limit, about what Claude Code shows of a server's instructions; startup refuses instructions that exceed the bound. Tool descriptions and format guides carry the details that do not fit in initialization: `get_guide`'s description names the recipes the connection's tools can follow, `execute`'s and `dry_run`'s say what each does, and those of `list_runs`, `list_playbook_events` and `list_interactions` say that their pages are read on only as the person needs. +The instructions are composed from the tools the caller may use and the registered definition types. They explain connection scope, put what the person wants kept in a recall function rather than the agent's own notes where the server runs recall functions, require reading the format before writing a definition and ask for its overview, name model and tool discovery where offered, and direct an answer to an outstanding interaction back to the same run. Where `search` and `add_connection` are both listed, they say that a person's accounts at services are connected through this server: asked to connect, link, sign in to or use a service, the agent calls `search` with its words, which lists the providers that match, then `add_connection` with a provider's name, which the person approves on the page it links, and says a service cannot be connected only when no provider or server matches. Scoped connections and read-only callers receive only applicable instructions. `src/mcp/instructions.test.ts` checks these combinations and the 2,000-character limit, about what Claude Code shows of a server's instructions; startup refuses instructions that exceed the bound. Tool descriptions and format guides carry the details that do not fit in initialization: `get_guide`'s description names the recipes the connection's tools can follow, `execute`'s and `dry_run`'s say what each does, and those of `list_runs`, `list_playbook_events` and `list_interactions` say that their pages are read on only as the person needs. **A link to open.** An operation whose definition gives `linkToOpen`, as `add_connection` of `@on.auto/connections` does with Studio's page that approves a connection, puts the page in its output, and its tool also asks the client to open it, with the message the definition gives, through the `url` form of elicitation, when the client declared that form in the capabilities its request carries (`src/tools/linked-answers.ts`). Only a request of the `2026-07-28` revision carries them: the handler serves an earlier revision statelessly, each request "by a fresh instance from the same factory", as the SDK documents its `legacy: 'stateless'`, so the capabilities a `2025-11-25` client declared in its `initialize` never reach the instance that answers its call, nor would the client's answer to the SDK's `elicitInput`, which takes the `url` form, reach the instance that asked; a client of that revision is answered at once, the page in the output. A tool that asks answers `input_required`, with one `url` elicitation and a `requestState` that `createRequestStateCodec` of the SDK signs with the current key of `retryStateKeys`, the HMAC keys `mcpRoutes` is given, and binds with the codec's `bind` to the caller and the org of the request: the tool's answer, with the tool, the caller and the org it was made for. The client opens the page, or the person declines, and retries the call with the state; the SDK verifies the state, and the tool answers what it kept for the same tool, caller and org without running the operation again, so a retry adds nothing a second time. The retry state is signed under a key the runtime keeps for itself in its secret store: 32 random bytes the first server of a ledger makes and names by a fact of the runtime's own, `own_key_kept` in `runtime/own-keys`, as the server's README says, which the server gives `mcpRoutes` as the `current` of `retryStateKeys`, with no `previous`; so every server that shares the ledger signs and verifies under the same key, whichever of them asked, across a restart and across a rotation of the secrets key, which wraps that key again and leaves it as it is, as the server's `src/connections/connections-over-mcp.test.ts` shows. A state is verified under `current` and then under `previous`, which is kept for a later rotation of that key, as `src/tools/linked-answers.test.ts` shows; a state of another key, made for another caller or org, or older than its ten minutes, is refused by the SDK as `-32602`, `Invalid or expired requestState`, and what the first call did stands. @@ -175,9 +175,9 @@ The bound on a listing is bytes because a listing is what an agent's client load | Endpoint | Tools | Bytes on the wire | Bound | | ------------------------------------------- | ----- | ----------------- | ------ | -| `POST /mcp` | 28 | 43,778 | 48,000 | +| `POST /mcp` | 28 | 44,771 | 48,000 | | `POST /orgs/{org}/mcp` | 10 | 13,019 | 13,100 | -| `POST /orgs/{org}/playbooks/{playbook}/mcp` | 20 | 30,932 | 40,000 | +| `POST /orgs/{org}/playbooks/{playbook}/mcp` | 20 | 31,925 | 40,000 | The bound of the org endpoint moved from 12,000 bytes to 13,000 with the tools of connections, the least that held them, since its listing took 12,767 with every tool at its shortest; `add_connection`'s way in by a provider's name took it to 12,869, and the sentence of `search` that names its `providers`, with `add_connection`'s words that `search` lists them, to 12,954, and the kind `authorization` among the kinds of its credential, which a caller names to connect a provider by consent, to 12,970. The bound moved again, to 13,100, the least hundred that holds the listing, when `search` came to name the providers that match a service and `add_connection` to open with a person's account at a service that `search` lists, which took the listing to 13,019. A listing takes 3.9 to 4.2 bytes a token, as two open byte-pair vocabularies of 100,000 and 200,000 count it, so the 48,000 bytes of `/mcp` come to about 11,400 to 12,300 tokens (the rate counted on 2026-10-13 over the `tools/list` answer as it is sent, the server-sent event's framing included). A model's own tokenizer counts differently, by a few percent either way. diff --git a/packages/api/src/mcp/instructions.test.ts b/packages/api/src/mcp/instructions.test.ts index 2e9af5d04..8f1a94a19 100644 --- a/packages/api/src/mcp/instructions.test.ts +++ b/packages/api/src/mcp/instructions.test.ts @@ -15,13 +15,13 @@ import { import { instructionsFor, type ServedTools } from './instructions.ts'; const servedOnMcp = - "Auto Runtime executes playbooks; Studio manages them; your agent is a separate client. A playbook contains one brain (its reusable functions) and workflows. The brain shares its lifecycle. Functions run directly or in workflows; runs belong to the playbook. A reasoning function has a prompt and calls a language model. An interaction function asks a system and answers at once, or a person and answers started. A computation function runs a program whose output is the same for the same input. Recall retrieves run results, definitions and events, never run inputs or tool calls. What the person wants kept goes in a recall function, for anyone or a workflow to ask later, not your notes. A prediction function answers from a model trained on the playbook's history; a build, input { build: {} }, answers started. A workflow runs functions in steps, waits for input and can start on a schedule or an event. This connection acts in the caller's own org: list_playbooks shows its playbooks and create_playbook makes one. Before writing a definition, read its format with get_guide. A reasoning function names a model that list_models lists and may name tools that search finds. Accounts at services are connected through this server: asked to connect, link, sign in to or use a service, call search with its words, which lists the providers that match, then add_connection with a provider's name, which the person approves on the page it links. Say a service cannot be connected only when no provider or server matches. A run of a workflow answers started; get_run shows whether it ended or still waits. When the person approves, rejects or otherwise answers what a run waits on, answer its request with answer_interaction, not a new run. Say what was done and what the person can do next, in the words of playbooks, functions, workflows and runs, not the tools' names, fields or rules; an id or status only where needed to act. A tool that cannot do what was asked says why and what to change."; + "Auto Runtime executes playbooks; Studio manages them; your agent is a separate client. A playbook contains one brain (its reusable functions) and workflows. The brain shares its lifecycle. Functions run directly or in workflows; runs belong to the playbook. A reasoning function has a prompt and calls a language model. An interaction function asks a system and answers at once, or a person and answers started. A computation function runs a program whose output is the same for the same input. Recall retrieves run results, definitions and events, never run inputs or tool calls. What the person wants kept goes in a recall function, for anyone or a workflow to ask later, not your notes. A prediction function answers from a model trained on the playbook's history; a build, input { build: {} }, answers started. A workflow runs functions in steps, waits for input and can start on a schedule or an event. This connection acts in the caller's own org: list_playbooks shows its playbooks and create_playbook makes one. Read a definition's format with get_guide; give it an overview. A reasoning function names a model that list_models lists and may name tools that search finds. Accounts at services are connected through this server: asked to connect, link, sign in to or use a service, call search with its words, which lists the providers that match, then add_connection with a provider's name, which the person approves on the page it links. Say a service cannot be connected only when no provider or server matches. A run of a workflow answers started; get_run shows whether it ended or still waits. When the person approves, rejects or otherwise answers what a run waits on, answer its request with answer_interaction, not a new run. Say what was done and what the person can do next, in the words of playbooks, functions, workflows and runs, not the tools' names, fields or rules; an id or status only where needed to act. A tool that cannot do what was asked says why and what to change."; const servedOnAnOrg = "Auto Runtime executes playbooks; Studio manages them; your agent is a separate client. A playbook contains one brain (its reusable functions) and workflows. The brain shares its lifecycle. Functions run directly or in workflows; runs belong to the playbook. A reasoning function has a prompt and calls a language model. An interaction function asks a system and answers at once, or a person and answers started. A computation function runs a program whose output is the same for the same input. Recall retrieves run results, definitions and events, never run inputs or tool calls. What the person wants kept goes in a recall function, for anyone or a workflow to ask later, not your notes. A prediction function answers from a model trained on the playbook's history; a build, input { build: {} }, answers started. A workflow runs functions in steps, waits for input and can start on a schedule or an event. This connection manages the playbooks of one org: list_playbooks shows them and create_playbook makes one, and a playbook's functions and workflows are made on the playbook's own connection, /orgs/{org}/playbooks/{playbook}/mcp. get_guide holds what these words mean and how each kind of definition is written. A reasoning function names a model that list_models lists and may name tools that search finds. Accounts at services are connected through this server: asked to connect, link, sign in to or use a service, call search with its words, which lists the providers that match, then add_connection with a provider's name, which the person approves on the page it links. Say a service cannot be connected only when no provider or server matches. Say what was done and what the person can do next, in the words of playbooks, functions, workflows and runs, not the tools' names, fields or rules; an id or status only where needed to act. A tool that cannot do what was asked says why and what to change."; const servedOnAPlaybook = - "Auto Runtime executes playbooks; Studio manages them; your agent is a separate client. A playbook contains one brain (its reusable functions) and workflows. The brain shares its lifecycle. Functions run directly or in workflows; runs belong to the playbook. A reasoning function has a prompt and calls a language model. An interaction function asks a system and answers at once, or a person and answers started. A computation function runs a program whose output is the same for the same input. Recall retrieves run results, definitions and events, never run inputs or tool calls. What the person wants kept goes in a recall function, for anyone or a workflow to ask later, not your notes. A prediction function answers from a model trained on the playbook's history; a build, input { build: {} }, answers started. A workflow runs functions in steps, waits for input and can start on a schedule or an event. This connection acts inside one playbook. Before writing a definition, read its format with get_guide. A reasoning function names a model this server can call, as the reasoning-function guide says, and may name tools that search finds. A run of a workflow answers started; get_run shows whether it ended or still waits. When the person approves, rejects or otherwise answers what a run waits on, answer its request with answer_interaction, not a new run. Say what was done and what the person can do next, in the words of playbooks, functions, workflows and runs, not the tools' names, fields or rules; an id or status only where needed to act. A tool that cannot do what was asked says why and what to change."; + "Auto Runtime executes playbooks; Studio manages them; your agent is a separate client. A playbook contains one brain (its reusable functions) and workflows. The brain shares its lifecycle. Functions run directly or in workflows; runs belong to the playbook. A reasoning function has a prompt and calls a language model. An interaction function asks a system and answers at once, or a person and answers started. A computation function runs a program whose output is the same for the same input. Recall retrieves run results, definitions and events, never run inputs or tool calls. What the person wants kept goes in a recall function, for anyone or a workflow to ask later, not your notes. A prediction function answers from a model trained on the playbook's history; a build, input { build: {} }, answers started. A workflow runs functions in steps, waits for input and can start on a schedule or an event. This connection acts inside one playbook. Read a definition's format with get_guide; give it an overview. A reasoning function names a model this server can call, as the reasoning-function guide says, and may name tools that search finds. A run of a workflow answers started; get_run shows whether it ended or still waits. When the person approves, rejects or otherwise answers what a run waits on, answer its request with answer_interaction, not a new run. Say what was done and what the person can do next, in the words of playbooks, functions, workflows and runs, not the tools' names, fields or rules; an id or status only where needed to act. A tool that cannot do what was asked says why and what to change."; const interactionSentence = 'An interaction function asks a system and answers at once, or a person and answers started.'; @@ -188,14 +188,14 @@ describe('what the instructions say of the definition types', () => { expect( ['computation function', 'recall function', 'A workflow runs'].filter((words) => instructions.includes(words)), ).toEqual([]); - expect(instructions).toContain('This connection acts inside one playbook. Before writing a definition'); + expect(instructions).toContain("This connection acts inside one playbook. Read a definition's format"); }); it('give no sentence to a type they have no words for', () => { const drafting = [{ type: 'drafting', noun: 'draft', guide: 'drafting' }]; expect(instructionsFor('playbook', playbookEndpoint, drafting)).toContain( - 'Auto Runtime executes playbooks; Studio manages them; your agent is a separate client. A playbook contains one brain (its reusable functions) and workflows. The brain shares its lifecycle. Functions run directly or in workflows; runs belong to the playbook. This connection acts inside one playbook. Before writing a definition', + "Auto Runtime executes playbooks; Studio manages them; your agent is a separate client. A playbook contains one brain (its reusable functions) and workflows. The brain shares its lifecycle. Functions run directly or in workflows; runs belong to the playbook. This connection acts inside one playbook. Read a definition's format", ); }); @@ -214,14 +214,14 @@ describe('what the instructions say of the definition types', () => { }); describe('what the instructions say of the guides', () => { - it("point to get_guide for a definition's format on every endpoint that writes one, and name no recipe, which get_guide names", () => { + it("point to get_guide for a definition's format and ask for its overview on every endpoint that writes one, and name no recipe, which get_guide names", () => { const writing = everyEndpoint.filter(([endpoint]) => endpoint !== 'org'); expect( writing.filter( ([endpoint, served]) => !instructionsFor(endpoint, served, definitionTypes).includes( - 'Before writing a definition, read its format with get_guide. A reasoning function', + "Read a definition's format with get_guide; give it an overview. A reasoning function", ), ), ).toEqual([]); diff --git a/packages/api/src/mcp/instructions.ts b/packages/api/src/mcp/instructions.ts index dfded2309..8106134ca 100644 --- a/packages/api/src/mcp/instructions.ts +++ b/packages/api/src/mcp/instructions.ts @@ -86,7 +86,7 @@ const modelsBeforeWriting: Sentences = ({ reasoning, listed }) => const guides: Sentences = ({ listed }) => [ listed('create_definition') - ? 'Before writing a definition, read its format with get_guide.' + ? "Read a definition's format with get_guide; give it an overview." : 'get_guide holds what these words mean and how each kind of definition is written.', ]; diff --git a/packages/api/src/mcp/tool-calling-instructions.test.ts b/packages/api/src/mcp/tool-calling-instructions.test.ts index 6bb089d8f..79265de86 100644 --- a/packages/api/src/mcp/tool-calling-instructions.test.ts +++ b/packages/api/src/mcp/tool-calling-instructions.test.ts @@ -12,10 +12,10 @@ import { import { instructionsFor } from './instructions.ts'; describe('the length of the instructions with every tool', () => { - it('take 1,993 characters on /mcp, 1,912 on the org endpoint and 1,618 on the playbook endpoint', () => { + it('take 1,996 characters on /mcp, 1,912 on the org endpoint and 1,621 on the playbook endpoint', () => { expect( everyEndpoint.map(([endpoint, served]) => instructionsFor(endpoint, served, definitionTypes).length), - ).toEqual([1912, 1618, 1993]); + ).toEqual([1912, 1621, 1996]); }); }); diff --git a/packages/definitions/README.md b/packages/definitions/README.md index 1c631fe1f..bc375f86f 100644 --- a/packages/definitions/README.md +++ b/packages/definitions/README.md @@ -65,8 +65,7 @@ A capability has: - `noun`, its definitions' noun for one and for several, and `describeOutput(output, record?)`, the sentence the operations say of a run's output, given the run's record beside it, which tells an interaction function's call that answered `{}` from a notification. `outputInWords('answer' | 'result')` is the sentence of every capability but that call's: the output in words within 300 characters, counted as code points, the records of a list each in parentheses, or that it is too long to repeat. - `mediaType`: the media type of its definition documents, such as `text/markdown`. - `parse(source)`: turns the document into the capability's own value. Parsing is validation: everything that can be checked without running is checked here. It fails with `InvalidInput`, whose issues each say in `detail` where in the document and what is wrong (line and problem). An issue's `pointer` addresses the document as a whole, so it is `''`; the operations answer it under `/source`. -- `summarize(parsed)`: what the operations show about a definition without knowing the type: an optional `description`, an optional `about`, the author's own Markdown for a business reader, which its overview writes after the description, optional JSON Schemas of the input a run takes (`inputSchema`) and the output it gives (`outputSchema`), `triggers`, the parsed triggers of a definition that starts runs on its own, as a workflow with a schedule does, in the order its document names them, each with its `kind`, `event`, `cron` or `every`, its `reference` in the document, and its rule: an event trigger's `filters`, each a `reference`, a `type` and the `attributes` it matches, a cron's `expression` or an every's `milliseconds` (`TriggerSchema`), and optional `warnings`: what `parse` found that does not stop the definition from being accepted but may not work everywhere, each a line of text that says where in the document (for reasoning: a schema some providers reject or do not enforce); and optional `details`, a JSON object the registry stores with the definition's record and keeps in its state, but never in a definition an operation answers, so that code which reads the record without the capability's parser can act on it (for recall: the fold, `answer`, the filters, `initial` and the view's schema, which the workflow host folds a view by; for a workflow and a prediction function: `calls`, the definitions it calls, which [Used by](#used-by) reads with the recall function's filters). -- `overview(parsed, name, context)`, optional (`OverviewOf`): the parts of the definition's overview, its `body`, its `sections` and its `appendix`, an `OverviewParts`, or, for an overview told as what happens, its `body` and its `appendix` alone, `ToldParts` (see [Told as what happens](#told-as-what-happens)), which the capability composes from its parsed document alone, since each parsed document keeps the texts its overview reads, each with the line of the document it starts on: a reasoning function's prompt in its compiled template, and an interaction function's `to`, `from` and message and a prediction function's brief in their parsed templates; it is given too the name the overview is written for, by which reasoning draws the function, and a `context` that holds the `summary` `prepare` computed, from which a workflow's overview says its triggers and its schemas without computing the summary again (see [The overview of a definition](#the-overview-of-a-definition)). `prepare` answers `overview(name)`, the overview of the definition of that name as data, an `Overview`, which nothing writes as Markdown before a read: its name as the title, its `description`, as a sentence where its parts are told, or the line `This has no description yet.` in the capability's `noun`, its `about`, when the summary has one, as the first block of its body, and these parts, or, for a capability that declares none, **What it needs** and **What it produces** from the summary's schemas, as two sections. +- `summarize(parsed)`: what the operations show about a definition without knowing the type: an optional `description`, optional JSON Schemas of the input a run takes (`inputSchema`) and the output it gives (`outputSchema`), `triggers`, the parsed triggers of a definition that starts runs on its own, as a workflow with a schedule does, in the order its document names them, each with its `kind`, `event`, `cron` or `every`, its `reference` in the document, and its rule: an event trigger's `filters`, each a `reference`, a `type` and the `attributes` it matches, a cron's `expression` or an every's `milliseconds` (`TriggerSchema`), and optional `warnings`: what `parse` found that does not stop the definition from being accepted but may not work everywhere, each a line of text that says where in the document (for reasoning: a schema some providers reject or do not enforce); and optional `details`, a JSON object the registry stores with the definition's record and keeps in its state, but never in a definition an operation answers, so that code which reads the record without the capability's parser can act on it (for recall: the fold, `answer`, the filters, `initial` and the view's schema, which the workflow host folds a view by; for a workflow and a prediction function: `calls`, the definitions it calls, which [Used by](#used-by) reads with the recall function's filters). - `run(parsed, input, context, stripped)`: runs the definition, given the stripped forms its version was saved with, or none. `input` is the caller's JSON value; `context` carries the run's `id`, the `org`, the `playbook`, the `caller` who started it (the identity the call was authorized for), the `definition` that runs, by `name` and `version`, the `journal` through which it records the tool calls it makes (see [Tool calls](#tool-calls)), its `lineage`: `startId`, the id of the `run_started` that began this attempt, and `correlationId`, the id of the run at the top of the tree the run belongs to (see [The lineage of a run](#the-lineage-of-a-run)), its `depth`, the reaction depth of the run (see [The reaction depth of a run](#the-reaction-depth-of-a-run)), its `callDepth`, the number of calls above it (see [The call depth of a run](#the-call-depth-of-a-run)), and `longestRunOf(type, name)` and `longestRunFor(type, name, input)`, the longest a run of the active latest version of that definition may take for any input and for the input given, or nothing for a definition the playbook does not have, which a workflow reads at its start. It answers with the `output`, a JSON value returned to the caller, and a `record`, a JSON object of what happened, stored with the run (for reasoning: the rendered prompt, the model, token usage). It fails with `InvalidInput`, with pointers into the input (`/name` above; the operations answer them under `/input`); with `Unavailable` when something the capability depends on cannot serve now and retrying may work; or with `Conflict` when the definition cannot run as written, which only running it can tell (for reasoning: a model the provider does not have; for computation: a program that raised an error on the input), so the definition must be updated before it can run, with the kind `unworkable` when it says so. A rejection may carry a `record`, a JSON object of what the capability did before it (for reasoning: the tokens and the duration of a model call whose answer it could not use), which the run keeps on its `run_rejected`, held to the same size limit as any record. A capability that starts work which finishes after the call returns, such as a workflow, answers `{ finishesLater: true, record }` instead, the record saying what it started (for a workflow: its run reference); see [Runs that finish later](#runs-that-finish-later). - `check(parsed, source, saved)`, optional: checks what parsing does not, at a cost a run must not pay, such as the programs and expressions of a document with a compiler, or against the other definitions of the playbook, which `saved`, a `SavedDefinitions`, reads: its `active(type, name)` answers the active version of that definition with its `name`, `version` and schemas, or nothing for one the playbook does not have active (for prediction: the interaction functions a version is approved and a build is sent through), and its `content(sha256)` answers the content the playbook keeps under that digest, through `PlaybookReader.readContent`, or nothing for a digest it does not keep (for prediction: the uploaded dataset a function names); the check answers the forms a run takes in place of the document's own, `StrippedForms`: the `module` and the `expressions` the check stripped of their types, an expression only where stripping changed it. It runs when a definition is created or updated, after `parse` and before anything is recorded, and never when a definition is read or run, since `parse` serves a run too; the save keeps what it answers as the version's `stripped`, beside its `source`, which no read shows. It fails with `InvalidInput`, as `parse` does, or with `Unavailable` when the check could not answer in time, which `create_definition` and `update_definition` answer as `unavailable`, 503, without a kind; see [The check at save](#the-check-at-save). @@ -84,10 +83,10 @@ A call cancelled while `run` runs, because its client went away or the server is | Operation | Kind | Route | Input | Answer | Rejections of the handler | | ------------------------ | ------- | ---------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------ | ------------------------------------------------------- | -| `create_definition` | command | `POST /definitions/{type}` | `type`, `name`, `source` | the definition, `201` | `not_found`, `invalid_input`, `conflict`, `unavailable` | +| `create_definition` | command | `POST /definitions/{type}` | `type`, `name`, `source`, `overview` (optional) | the definition, `201` | `not_found`, `invalid_input`, `conflict`, `unavailable` | | `list_definitions` | query | `GET /definitions/{type}` | `type`, `include_retired` (default `false`) | `{ definitions }`, sorted by name, no documents | `not_found` | | `get_definition` | query | `GET /definitions/{type}/{name}` | `type`, `name` | the definition, active or retired, with document | `not_found` | -| `update_definition` | command | `PUT /definitions/{type}/{name}` | `type`, `name`, `source` | the definition at its new version | `not_found`, `invalid_input`, `conflict`, `unavailable` | +| `update_definition` | command | `PUT /definitions/{type}/{name}` | `type`, `name`, `source`, `overview` (optional) | the definition at its new version | `not_found`, `invalid_input`, `conflict`, `unavailable` | | `retire_definition` | command | `POST /definitions/{type}/{name}/retire` | `type`, `name` | the definition | `not_found`, `conflict` | | `run_definition` | command | `POST /definitions/{type}/{name}/run` | `type`, `name`, `input` (any JSON, default `{}`), `run_id` (optional UUID) | the run | `not_found`, `conflict`, `invalid_input`, `unavailable` | | `get_run` | query | `GET /runs/{run_id}` | `run_id` | the run, with its record | `not_found` | @@ -96,15 +95,16 @@ A call cancelled while `run` runs, because its client went away or the server is | `get_run_history` | query | `GET /runs/{run_id}/history` | `run_id`, `order` (default `asc`), `limit`, `cursor` | `{ events, has_more, next_cursor }` | `not_found`, `invalid_input` | | `get_playbook_analytics` | query | `GET /analytics` | `days` (7, 14 or 30, default 7), or `from` and `to`; `type`, `name`, all optional | the analytics of the playbook | `invalid_input` | -A definition carries `type`, `name`, `version`, `status` (`active` or `retired`), `media_type`, the `description`, `input_schema`, `output_schema` and `warnings` its capability gives when it gives them, `created_at`, `created_by`, `updated_at`, `retired_at` on a retired definition, its document as `source` and its `overview`, the Markdown the read writes from the overview its version was saved with and from what the playbook holds when it reads (see [Used by](#used-by)). `get_definition` adds `standing` when its capability's `standing` answers one (below). Of the `details` its capability gives, which the registry keeps, a definition answers only `details.shape`, the name of its [output shape](#output-shapes), when the details name one, and leaves the rest out (`src/operations/definition-views.ts`). A listed definition is the same without `source`, `overview` and `standing`. Times are ISO 8601 UTC strings read from Effect's `Clock`. `DefinitionSchema`, `ListedDefinitionSchema`, `RunSchema` and `RunDetailSchema` are the schemas. +A definition carries `type`, `name`, `version`, `status` (`active` or `retired`), `media_type`, the `description`, `input_schema`, `output_schema` and `warnings` its capability gives when it gives them, `created_at`, `created_by`, `updated_at`, `retired_at` on a retired definition, its document as `source`, the `overview` its author gave with the version, when one was given (see [The overview of a definition](#the-overview-of-a-definition)), and `used_by`, the active definitions of the playbook that use it, when any does (see [Used by](#used-by)). `get_definition` adds `standing` when its capability's `standing` answers one (below). Of the `details` its capability gives, which the registry keeps, a definition answers only `details.shape`, the name of its [output shape](#output-shapes), when the details name one, and leaves the rest out (`src/operations/definition-views.ts`). A listed definition is the same without `source`, `overview`, `used_by` and `standing`. Times are ISO 8601 UTC strings read from Effect's `Clock`. `DefinitionSchema`, `ListedDefinitionSchema`, `RunSchema` and `RunDetailSchema` are the schemas. - `type` is the type a capability states. The published JSON Schema of the field is a plain `{ "type": "string", "enum": [...], "description": ... }` of the known types. Decoding checks the shape and then that a capability of the server serves the type, so a malformed type and one the server does not run are both `invalid_input` at `/type` on every operation that takes one, `list_runs` and `get_playbook_analytics` among them; the second names the types the server runs. Effect would publish the types under `allOf`, because it inlines no `enum` from a check, so each operation replaces that one property of its input's JSON Schema. - `name` is 3 to 48 lowercase letters, digits and hyphens, starting with a letter, `definitionNamePattern`, which a capability that names another definition in its document checks it by: unique among the definitions of its type in the playbook, and never reused. - `source` is at most 65536 bytes in UTF-8, checked at decoding. The JSON Schema says `maxLength: 65536`, which every such document meets. +- `overview` is 1 character to 16384 bytes in UTF-8, checked at decoding the same way, with `minLength: 1` and `maxLength: 16384` in the JSON Schema. - A document the capability's `parse` rejects is `invalid_input` with the capability's issues under `/source`, and nothing is stored, and so is one its `check` refuses; a check that does not answer in time is `unavailable`, and nothing is stored either. - `create_definition` meets `conflict` when an active or a retired definition of the type holds the name. - A definition with triggers is recorded with its `triggers` in the content of `definition_created` and `definition_updated`, which `get_definition` and `list_definitions` show, and a playbook holds at most 1,024 active definitions of one type with triggers, however many each has, `mostReactingDefinitions`: a `create_definition` or `update_definition` that would make one more is `conflict`, counted after the version it replaces; `get_definition` of an active definition with triggers shows `triggers_since`, the id of the record that made its current version, from which what its triggers start is a run of that version. It is one id for the definition, while the workflow host keeps, for each trigger, the record that activated it as it is. -- `update_definition` replaces the document. Every update that changes the document makes a new version, one more than the last; an update with the same document succeeds and records nothing. It meets `not_found` for a missing definition and `conflict` for a retired one. +- `update_definition` replaces the document. Every update that changes the document or the overview makes a new version, one more than the last; an update that leaves the overview out keeps the one saved, and one with the same document and the same overview, or none, succeeds and records nothing. It meets `not_found` for a missing definition and `conflict` for a retired one. - `retire_definition` is permanent: a retired definition can be read and listed, but not updated, executed or recreated. Retiring a retired definition succeeds and records nothing. - Every command also meets `conflict` when the definitions of the type, or the run, changed while it decided. - `run_definition` runs the active latest version of the definition. It meets `not_found` for a missing definition, and `conflict` for a retired definition, a definition whose stored document its capability no longer parses, or a definition its capability finds cannot run as written. @@ -215,73 +215,50 @@ The compiler's control-flow analysis grows with the square of the assignments in ## Output shapes -A shape is a JSON Schema the runtime owns for what a definition produces, which Auto Studio draws: `series`, `categories`, `table` and `text` (`src/shapes`). Each is data, a schema of draft 2020-12 whose `$id` is `https://on.auto/shapes/` and `title` its name, with `additionalProperties: false` on every record, its bounds beside it as constants, and `further`, the check of what the schema cannot say, run by `@on.auto/json-schema` with the schema's own check so that its issues are worded and bounded as the schema's are: the form of each `at` of a series under its `period`, `YYYY-MM-DD`, `YYYY-Www`, `YYYY-MM`, `YYYY-Qn` or `YYYY`, or an RFC 3339 date and time without one, with the day a day of its month and the week a week of its ISO year, and of its `origin`; each `at` later than the one before it in its line; at most 2,000 points across the lines; a `low` at most and a `high` at least its `value`; a name no line, value or column before it has; one cell in each row for each column; and a text of at most 65,536 bytes in UTF-8. The further check reads any JSON and says nothing of what the schema refuses, so its issues follow the schema's. `outputShapeNamed(name)` answers a shape and `outputShapeNames()` the names, each `{ name, schema, further, check }`, `check` the schema's check of an output, nested at most 512 levels, compiled the first time it is asked; `shapeMismatchOf(shape, output)` words the issues of an output that does not match, at most three; and `declaredShape(shape, besideSchema)` reads the `output.shape` of a document, refusing it at `schema` beside a schema of its own and at `shape` for anything but a shape's name, with the words each capability puts at its line. `rangeHolds({ low, value, high })` is the rule of a point's range that the check applies, exported so a capability that scores numbers before they become a series holds them to the same rule. `seriesShape`, `seriesBounds` and `seriesMarksSchema`, the schema of a series' marks, are exported for a capability whose output is always a series, as a prediction function's is. `shapeNamed(name)` of `@on.auto/definitions/testing` throws for a name that is no shape. A capability whose output names a shape publishes the shape's schema as its `outputSchema` and the name as `details.shape` through `publishedOutputOf({ schema, shape, details })` (`src/shapes/published-output.ts`), the one way a definition says its shape: with a shape it answers the shape's schema and adds `shape` to the capability's own `details`, and without one the definition's own schema and details, each left out when there is none; reasoning, computation, coordination and prediction use it, prediction with the series shape it always answers. +A shape is a JSON Schema the runtime owns for what a definition produces, which Auto Studio draws: `series`, `categories`, `table` and `text` (`src/shapes`). Each is data, a schema of draft 2020-12 whose `$id` is `https://on.auto/shapes/` and `title` its name, with `additionalProperties: false` on every record, its bounds beside it as constants, and `further`, the check of what the schema cannot say, run by `@on.auto/json-schema` with the schema's own check so that its issues are worded and bounded as the schema's are: the form of each `at` of a series under its `period`, `YYYY-MM-DD`, `YYYY-Www`, `YYYY-MM`, `YYYY-Qn` or `YYYY`, or an RFC 3339 date and time without one, with the day a day of its month and the week a week of its ISO year, and of its `origin`; each `at` later than the one before it in its line; at most 2,000 points across the lines; a `low` at most and a `high` at least its `value`; a name no line, value or column before it has; one cell in each row for each column; and a text of at most 65,536 bytes in UTF-8. The further check reads any JSON and says nothing of what the schema refuses, so its issues follow the schema's. `outputShapeNamed(name)` answers a shape and `outputShapeNames()` the names, each `{ name, schema, further, check }`, `check` the schema's check of an output, nested at most 512 levels, compiled the first time it is asked; `shapeMismatchOf(shape, output)` words the issues of an output that does not match, at most three; and `declaredShape(shape, besideSchema)` reads the `output.shape` of a document, refusing it at `schema` beside a schema of its own and at `shape` for anything but a shape's name, with the words each capability puts at its line. `rangeHolds({ low, value, high })` is the rule of a point's range that the check applies, exported so a capability that scores numbers before they become a series holds them to the same rule. `seriesShape` and `seriesMarksSchema`, the schema of a series' marks, are exported for a capability whose output is always a series, as a prediction function's is. `shapeNamed(name)` of `@on.auto/definitions/testing` throws for a name that is no shape. A capability whose output names a shape publishes the shape's schema as its `outputSchema` and the name as `details.shape` through `publishedOutputOf({ schema, shape, details })` (`src/shapes/published-output.ts`), the one way a definition says its shape: with a shape it answers the shape's schema and adds `shape` to the capability's own `details`, and without one the definition's own schema and details, each left out when there is none; reasoning, computation, coordination and prediction use it, prediction with the series shape it always answers. ## Reading function documents `@on.auto/definitions/document` is the reader that every capability whose document is YAML front matter and a body shares, so each capability supplies only its own keys and what its body means: - `splitDocument(source, definition)` splits the front matter between two lines of three dashes from the body, with the line each starts on; `definition` names the document in the message for a source that does not start with the dashes. -- `frontMatterIn(text, firstLine, shape)` reads the front matter with the YAML rules every capability keeps (YAML 1.2's core schema, strictly, with no anchor, alias or tag and at most 72 levels of nesting) and decodes it with the capability's `FrontMatterShape`: its `sections`, the keys allowed at the top and under each section, of which any other is an issue at its line; its `decode`; and `required`, which the message for empty front matter names, such as `the model` for reasoning and `the language` for computation. It refuses, in the `description` and the `about`, what `writtenTextIssues` refuses, keeping the line and the column of only those two values, where each opens. -- `DescriptionSchema` is a `description` of a function document's front matter, 1 to 1,000 characters, which the front matter of each of the five function types takes, and `AboutSchema` an `about`, Markdown its author writes for a business reader, 1 to 4,000 characters, each counted in UTF-16 units by the schema's own check, which a workflow's `document.metadata.about` takes too. `writtenTextIssues(text, key, { lines, firstLine }, { line, column })`, given the text a key holds, the lines of its source and the line of the first of them, and the line and the column its value opens at, refuses what the overview cannot hold, in words that name the key: a heading, which an overview writes alone; a line `## Details`, in a fence too, the overview's own, since the page that shows an overview splits it at that text; and a fence or an HTML block, but an element or a lone tag, which the blank line after the text ends, left open at its end, which would read the rest of the overview into it. In a literal block scalar, `|`, whose lines are the source's own, each is refused at the line and the column its text is at; in any other style, a folded block, a plain or a quoted scalar or an escaped line break, whose lines YAML joins or makes, at the line and the column the value opens at, the words naming the line by its first four words, `the line that begins “## When it runs”`. The reader of a front matter and a workflow's reader both refuse with it. A text that a run reads, such as a prompt, a message or a brief, is not refused these: the overview writes it as its author wrote it, and keeps itself whole, as [`liquid-reading.ts`](#the-overview-of-a-definition) says. `ValueContract`, `{ schema? }`, the compiled schema of an input or an output a parsed document may hold, which the parsed documents of interaction, recall and computation take. +- `frontMatterIn(text, firstLine, shape)` reads the front matter with the YAML rules every capability keeps (YAML 1.2's core schema, strictly, with no anchor, alias or tag and at most 72 levels of nesting) and decodes it with the capability's `FrontMatterShape`: its `sections`, the keys allowed at the top and under each section, of which any other is an issue at its line; its `decode`; and `required`, which the message for empty front matter names, such as `the model` for reasoning and `the language` for computation. +- `DescriptionSchema` is a `description` of a function document's front matter, one or two sentences for lists of 1 to 1,000 characters, counted in UTF-16 units by the schema's own check, which the front matter of each of the five function types takes. `ValueContract`, `{ schema? }`, the compiled schema of an input or an output a parsed document may hold, which the parsed documents of interaction, recall and computation take. - `issueAt` places an issue at the line of the deepest part of its JSON pointer that the front matter has, `issueText` writes it as `Line 7, /input/schema: ...`, or `Line 7: ...` for the document as a whole, and `reportedIssues` sorts a document's issues by line and bounds how many it reports. Reasoning reads its documents with it and keeps its own keys, messages and tests; interaction, prediction, recall and computation read their own with the same reader. The JSON Schemas a document holds are compiled by [`@on.auto/json-schema`](../json-schema/README.md), which this reader and every capability import. The subpath is separate from the main entry because it imports the YAML parser, which nothing else in this package needs. ## Templates of a capability -`@on.auto/definitions/template` is the Liquid engine every capability whose documents hold templates shares, as a factory that gives each capability an instance of its own, so what one registers no other sees. `templateEngine(registrations?)` makes one, with the filters and tags every instance keeps, strict filters and variables, own properties only, and the bounds of `templateLimits`, 64 KiB of template, 1,000 names, 200 ms and 5 MB of a render, and with the filters and tags the capability registers, given as a function that answers them: the reasoning function adapter registers its system block and its prompt filters, `money`, `clip` and `words`, and the interaction and prediction capabilities nothing. `parsedTemplate(engine, body, firstLine)` parses a template, refusing one that does not parse with the line of its issue, and answers it with the variables it reads, each with its line in the document, an issue beside it when it uses more names than a template may, and the `text` it was parsed from with its `firstLine`, so that a parsed template is a `TemplateText` the reader of an overview takes as it is. `renderedTemplate(engine, parsed, { variables, emitter, refusalOf, clock })` renders it with the variables it is given into an emitter of the capability's own, which decides what a written value becomes and may refuse one, and answers a missing variable, a bound of the render or a failed filter, with its line, or what `refusalOf` makes of what the emitter threw. The 200 ms of a render are counted from its start on `clock`, a function that answers milliseconds, which every render is given: `renderClockOf(clock)` reads an Effect `Clock` as one, by its monotonic time, and `renderClock` is the one of the clock of the effect that renders, so each capability counts a render on the clock of its run and a test on `steppingClock` of the test support. The engine reaches into liquidjs at two places, each a function of its own. `tagsRemovedFrom` deletes the tags that load or capture templates from the instance's `tags`, since liquidjs unregisters a filter but offers no way to unregister a tag. `contextLimitedBy` writes the limit counted on `clock` into the context's `renderLimit`, since liquidjs counts a render's time on `performance.now()` and takes a limiter only through that field and its constructor, while it exports neither the class of its limiter, whose private members no object of ours can satisfy, nor an option for its clock. `inputVariableIssues(variables, inputSchema, what)` refuses a template that reads anything but `input`, `today` and `now`, or a property a closed input schema does not have, in the words of `what` the template is. `outputText` writes a value as Liquid writes it, an object as `[object Object]`; a rendered value is text, never parsed again as a template. +`@on.auto/definitions/template` is the Liquid engine every capability whose documents hold templates shares, as a factory that gives each capability an instance of its own, so what one registers no other sees. `templateEngine(registrations?)` makes one, with the filters and tags every instance keeps, strict filters and variables, own properties only, and the bounds of `templateLimits`, 64 KiB of template, 1,000 names, 200 ms and 5 MB of a render, and with the filters and tags the capability registers, given as a function that answers them: the reasoning function adapter registers its system block and its prompt filters, `money`, `clip` and `words`, and the interaction and prediction capabilities nothing. `parsedTemplate(engine, body, firstLine)` parses a template, refusing one that does not parse with the line of its issue, and answers it with the variables it reads, each with its line in the document, an issue beside it when it uses more names than a template may, and the `text` it was parsed from with its `firstLine`. `renderedTemplate(engine, parsed, { variables, emitter, refusalOf, clock })` renders it with the variables it is given into an emitter of the capability's own, which decides what a written value becomes and may refuse one, and answers a missing variable, a bound of the render or a failed filter, with its line, or what `refusalOf` makes of what the emitter threw. The 200 ms of a render are counted from its start on `clock`, a function that answers milliseconds, which every render is given: `renderClockOf(clock)` reads an Effect `Clock` as one, by its monotonic time, and `renderClock` is the one of the clock of the effect that renders, so each capability counts a render on the clock of its run and a test on `steppingClock` of the test support. The engine reaches into liquidjs at two places, each a function of its own. `tagsRemovedFrom` deletes the tags that load or capture templates from the instance's `tags`, since liquidjs unregisters a filter but offers no way to unregister a tag. `contextLimitedBy` writes the limit counted on `clock` into the context's `renderLimit`, since liquidjs counts a render's time on `performance.now()` and takes a limiter only through that field and its constructor, while it exports neither the class of its limiter, whose private members no object of ours can satisfy, nor an option for its clock. `inputVariableIssues(variables, inputSchema, what)` refuses a template that reads anything but `input`, `today` and `now`, or a property a closed input schema does not have, in the words of `what` the template is. `outputText` writes a value as Liquid writes it, an object as `[object Object]`; a rendered value is text, never parsed again as a template. -It serves too the reader of a template for an overview, `readableTemplate` and `readableParts` (see [The overview of a definition](#the-overview-of-a-definition)). The subpath is separate from the main entry because it imports `liquidjs`, which nothing else in this package needs. +The subpath is separate from the main entry because it imports `liquidjs`, which nothing else in this package needs. ## The overview of a definition -Each definition has an overview, the definition in plain words for a person who does not read its document, which Auto Studio shows before the source. It is composed once, when a version is saved, and written as Markdown at each read: `contentOf` asks `prepare(source).overview(name)` for it, an `Overview` with its blocks and its phrases, and the save records it in the version's `content` beside the `source` in its encoded form, through `encodedOverview`, so an older version keeps the overview it was saved with, and a phrase the read resolves is still a phrase when the read comes. The schema of the fact's content takes the overview as any value and decodes nothing of it, so the registry and the fold of a definition's stream carry it encoded: a load of a type's definitions, which every `get_definition`, `create_definition`, `update_definition`, `retire_definition`, `run_definition`, check at save and `list_definitions` makes, as do the workflow host's projector, its reader of triggers and the playbook's facts as events, folds every version of each name and keeps the latest, without decoding an overview. The SQLite and PostgreSQL stores keep each event as JSON text, which a load parses whole, overview and all, so what a fact keeps of an overview is bounded as what is written: a table of fields keeps its first `mostTableRows` rows where it is composed (see `overview.ts` below). `decodedOverview` decodes it, through the one `OverviewSchema`, in two places alone: where a read writes it, in `answeredDefinition`, and where the definition presenter measures it. A change to what a capability composes shows in a definition once a new version of it is saved, and a change to the writer at its next read. What the stored overview cannot hold, since another definition's save would make it stale, the read adds: who uses the definition and what it calls (see [Used by](#used-by)). It then writes the Markdown, so `get_definition`, `create_definition`, `update_definition` and `retire_definition` answer one `overview` text, as they did when the save wrote it. Measured on 2026-10-19 with Node 26.10.0 on a machine of 16 cores, by `pnpm --filter @on.auto/definitions measure:loads` (`measure/measure-loads.ts`), on SQLite: 50 versions of a document of 65,524 bytes, as many fields as one document holds, keep events of 180,841 bytes each, of which the overview takes 5,297, its table cut at 50 rows, beside the source and the input schema the summary gives; a load of their type, the parse of each event included, takes 36 ms of processor time at the median of 15, as does a read of the definition, which decodes and writes its one overview, and 47 to 112 ms of wall time at a load average of 180 to 260 from other work. `src/overview` holds it: +A definition's `overview` is Markdown its author gives beside the document, for the person who runs the business: why the definition exists, what it does and how it fits. `create_definition` and `update_definition` take it as `overview`, `OverviewField` of `src/operations/definition-fields.ts`, bounded at decoding as `source` is. Its description, which an agent reads as the argument of the two tools, is the sentence of the ask that fits the 300 characters an argument's description may hold on MCP; the descriptions of the two commands add that it is written again with every update and that one left out stays. `contentOf` keeps it in the version's `content` as it is given, and every operation that answers a definition answers it as stored. An update without one keeps the saved one: `decideUpdate` of `src/registry/registry-decisions.ts` takes the saved version's overview where the update's content has none, and records nothing when the document and the overview are the saved version's, so a new overview alone makes a new version. A definition saved without an overview has none. -- `overview.ts`, the `Overview`, data and not Markdown, and `OverviewSchema`, the one schema from which the `Overview` and the type of each of its parts derive, through which `encodedOverview` encodes it for the fact and `decodedOverview` decodes it where it is written: a `title`, an `opening` and its parts: a `body`, the blocks a person reads first, with no sections of its own; `sections`, each a `heading` with blocks, which each capability composes and which are written between the body and the appendix; and an `appendix`, sections of the facts the body leaves out. A block is one of seven kinds, an `authored` text, the author's own Markdown, which a document's `about` alone gives, a `paragraph` of phrases, a `list` of them, a `numbered` list of them, a table of `fields`, each a `name`, `required` and `words`, which `fieldTable(rows)` composes with its first `mostTableRows`, 50, rows and, past them, the count of the rest as `more`, a `quote` of `lines`, each a line of phrases, a message, an instruction or a brief shown as its reader receives it, and a `diagram` of `nodes`, each with a label and a `shape`, a `step` when left out, a `decision`, a `terminal` or a `cylinder` for what is kept, `edges`, each with an optional label and `dashed` or not, and optional `groups` of nodes with a label, each `within` an earlier group it names or at the top. A diagram draws a workflow's steps, how a recall function's view is kept and, for a reasoning function that names tools, the function, its model and each tool the model may call. A phrase is text as it is written, the author's own Markdown among it, a `name`, an `emphasis`, `code`, a `definition`, a `PhrasedDefinition`, its `type`, `name` and an optional `title`, which `definitionPhrase({ type, name, title? })` makes, or a `server` by its name; `listedPhrases(items, 'and' | 'or', separation?)` joins phrases as English lists them, as `listed` of `@on.auto/operations` joins words, with a serial comma before the last of three or more, and with `'serial-when-items-hold-commas'` writes a comma before the last only where an item holds one, `a, b and c` but `the verdict, one of approve and reject, and the note, when there is one`, as the steps of a caller and the lists of an overview told as what happens do. -- `markdown-writer.ts`, `markdownOf(overview)`, and `markdownOfKept(kept)`, the same of an overview in the encoded form its fact keeps, which the definition presenter measures: the title as the one heading `#`, the opening, the blocks of the body, each section under `##`, and the appendix last, under the one heading `## Details` with each of its sections under `###`; a name in bold and an emphasis in italics with what would end them escaped, a definition, in the body and the sections, in bold by its `title`, or else by its name in words with its first letter raised, `Month window` for `month-window`, through `readableName` of `src/plain-language/in-words.ts`, and in the appendix by its name as written, in code, and a server in bold by its name, code in a span of backticks longer than any it holds, a list an item a line, a numbered list an item a line from `1.`, a table of fields in GFM with each `|`, backtick and backslash of a cell escaped and its line breaks made spaces, the rows it is given and, for a table whose composition kept the count of the rest, a last row `and n more`, an author's text as it is, between blank lines, since its document's save refused a heading, a line `## Details` and a block left open in it, and a diagram as a fenced `mermaid` block, `flowchart LR`, in `mermaid-diagram.ts`, whose node ids are `n1`, `n2` and group ids `g1`, `g2` by position, never the author's words, with each label quoted and its `#`, `"`, `&`, `<`, `>`, backtick and `|` written as Mermaid's entity codes, a step in brackets, a decision in braces, a terminal in a stadium, `([…])`, and a cylinder as `[(…)]`, a dashed edge as `-.->`, and each group a subgraph, nested in the subgraph of the group it is within. A quote is a Markdown block quote, through `blockQuoted(lines)` of `markdown-text.ts`, `> ` before each of its lines and `>` alone for a blank one, with a hard break, a backslash at the end of a line, between two lines of one paragraph, and none on a paragraph's or an item's last line, since a trailing backslash there is text: none before a line that opens a list item, a quote, a heading, a fence, an HTML block, a thematic break or a setext underline, however far it is indented, as in an item nested in another; none after a heading, a thematic break or a setext underline; and none within a fence, indented code, a table or an HTML block. `src/markdown/markdown-lines.ts` reads the lines, `markdownLinesOf(lines)`, as CommonMark reads its blocks, within the list items and quotes that hold them: each line in the item it is indented into, after the markers of the items and quotes it opens, so a line indented less than an item's content ends it, unless it lazily goes on with a paragraph; a heading line, a fence, indented code and an HTML block, each at most three spaces into its container, indented code at four, where a line indented four spaces after a line of a paragraph goes on with it; a fence or an HTML block opened in an item ends with the item; an empty item, or an ordered one that does not start at 1, does not interrupt a paragraph. Indented code starts after a blank line, a heading, a closing fence, a thematic break or the marker of an item. A line is indented by its spaces and tabs alone, so a no-break or an ideographic space before `#` or a fence is text; a fence of backticks takes no backtick after its marker, so `` ``` a`b `` is a line of a paragraph; and it closes on a line of its marker with only spaces or tabs after it. An item whose marker line is empty ends at a blank line right after it, since an item may start with one blank line at most. A table runs from a row with a `|` followed by its delimiter row, such as `| --- | :-: |`, to the next blank line, as GFM reads one, and a line with a `|` that heads no table is a line of its paragraph. An HTML block, in `src/markdown/html-blocks.ts`, opens with one of the six kinds of the CommonMark reference that may interrupt a paragraph, ``, or, where it does not interrupt a paragraph or a table, the seventh, a line that is one whole open or closing tag of any name, and runs to the end its kind names, a comment to `-->` or to `--!>`, at which a browser ends one too, or, for an element and a lone tag, to the next blank line. An open block keeps the line that closes it: a fence its marker, a raw block the closing tag of its name, `` for `
`, a comment `-->`, an instruction `?>`, a declaration `>` and a `CDATA` section `]]>`. An overview takes at most `mostOverviewBytes`, 65,536 bytes in UTF-8, its body, its sections and its appendix bounded together, in that order: past the bound, it is cut before the first block that does not fit, so its appendix loses its blocks before its body does, with one last line that says so; a heading of the appendix is written only with at least one of its own blocks, and `## Details` only when a block of the appendix is kept.
-- `schema-words.ts`, `schemaBlocks(schema, unknown)`: a JSON Schema in words, through `schema-reading.ts`, which follows a `$ref` within the document, to `#`, `#/$defs/` or `#/definitions/`. An object with `properties` is a table of its fields in its order, each `required` or not, an undocumented one too; any other schema is a sentence; no schema at all is the words `unknown`. A field's words are its `description`, prose written as one line, its line breaks spaces, through `descriptionOf`, where every sentence of a schema's description is made, so that no line of one, in a cell or in a paragraph, is a line of the overview's own such as `## Details`, cut at `mostFieldDescriptionCharacters`, 300 characters, then its type: `text`, the form of a format the runtime checks, `a date`, `a date and time`, `a time`, `an email address`, `a URI` or `a UUID`, from `forms` of `schema-nouns.ts`, keyed by the format names of [`@on.auto/json-schema`](../json-schema/README.md#formats), and `text in the form the schema calls phone` for any other format, here `phone`, which a workflow's output schema may name, since the runtime does not compile it, `a whole number`, `a number`, `yes or no`, `nothing`, `one of: a, b, c` for an `enum` of up to 12 values named, `exactly x` for a `const`, a few alternatives of `anyOf`, `oneOf` or a list of types joined with `or`, `a list of` the plural of its items, `a record with:` its fields, each in bold with its words, one level down, `a record whose every entry is` the words of its `additionalProperties` for one without `properties`, and `a record` beneath that, and `any value` for a schema that says nothing. A list or a record says its own bounds before what it holds, in `schema-bounds.ts`, so a reader can tell which bound is whose: `a list of 1 to 10 whole numbers`, `a list of at most 20 records with: **at** (text)`, `a list of at most 50 items` for one that says nothing of its items, `a record of at most 50 entries, each a list of at least 1 record`, and `a record of 2 to 4 entries with:` its fields; the items after a bound are counted, in `schema-nouns.ts`, `whole number` after 1 and `whole numbers` after any other number, `text values` for text and `values` for any value. Any other bound follows its type, a number's `from 1 to 50`, `at least 1`, `more than 0` or `a multiple of 5`, a text's `at most 200 characters` and a list's `no item twice`, as do the bounds of a list or a record that a choice or alternatives say, and then `5 when left out` for a `default`. A field's `examples` come last, `such as “April 2026” or 7`, in the words of a value, unless its description says “such as” or holds one of them as a whole word. A lower bound equal to its upper says `exactly`, `a list of exactly 1 record` or `a whole number, exactly 5`, never `1 to 1`. Within a list, what follows the type of its items is each item's and begins with `each`: `a list of 1 to 10 text values, each at most 200 characters`, `a list of at most 3 whole numbers, each from 1 to 50`.
-- `duration-words.ts`, `durationInWords(milliseconds)`: a duration in days, hours, minutes and seconds, each unit it holds, such as `1 hour and 30 minutes`, to the nearest second from a minute on and with a fraction of a second below it, such as `30.5 seconds`, which the interaction, prediction and workflow overviews say an expiry, a build's wait, a wait and a schedule with.
-- `event-filter-words.ts`, `eventFilterPhrases(filter)`: an `EventFilter`, the `type` of the events it takes and each other attribute as written, in words, which a recall function's filters, the events a prediction function's dataset folds and a workflow's event trigger each say with it, after words of their own, `each time` or `On an event, each time`. A run event, `run_started`, `run_succeeded`, `run_failed` or `run_rejected`, is a run of the definition its `subject` names, `a run of the reasoning function **review-brief** succeeds`, or of any function or workflow without one; a definition event, `definition_created`, `definition_updated` or `definition_retired`, is the definition its `source` names, `the workflow **greet** is retired`; and any other type is `an event` of that type `is published to the playbook`. Each other attribute follows, `from the source`, `with the subject`, `when the caller is`, or for the caller `playbook:` `when the caller is the playbook` of that id, as for a run a trigger started and the runs it calls, `when its data is exactly` a value or `when` an expression `holds for its data`, and `when its` any other attribute `is` its value.
-- `definition-overview.ts`, `inputSection` and `outputSection`, **What it needs** and **What it produces** of a schema or of none, or of a shape in place of its schema's fields, in the one paragraph of `src/shapes/shape-words.ts`, in the nouns of `schema-nouns.ts`: `It answers a series over time, in **unit**: 1 to 20 lines, one for each **name**, …`, `It answers 1 to 200 **values**, …`, `It answers a table of 1 to 50 **columns**, …` and `It answers a **text**: Markdown of at most 65,536 bytes.`, which a capability's own overview reuses; `seriesPathBlocks({ period, observed, ahead })` says, in the same module and the same `periodNouns`, a series whose content a capability knows, its last observed periods and its points ahead, such as `It answers the last 8 observed weeks and 1 and 4 weeks ahead, each with a likely value, a low and a high.`, or one total to the end of an enclosing period, which the overview of a prediction function says; `composedOverview`, the overview of a definition as data, its name as the title and its description as the opening, then, as the first block of its body, the summary's `about` without the blank lines around it, unless it is blank, with the generic parts of every type unless its capability composes its own `OverviewParts`, a `Pick` of the `body`, the `sections` and the `appendix` of an `Overview`, or `ToldParts`, a `Pick` of the `body` and the `appendix` alone, whose opening is the description through `asSentence` when it is one paragraph that ends in a line of prose, as `firstParagraphOf` reads it, and as written otherwise; and `inputDetails(schema)` and `outputDetails(schema, shape?)`, the same words as **What it needs** and **What it produces**, under **Input** and **Output**, the headings of the tables of an appendix.
-- `liquid-reading.ts`, `liquid-phrases.ts` and `inline-constructs.ts`, the reader of a Liquid template as a person reads it, served from `@on.auto/definitions/template`, since it imports `liquidjs`: `readableTemplate({ text, firstLine })` reads a template, `readableParts({ text, firstLine }, block)` reads apart what stands inside one top-level block, as reasoning reads its `{% system %}` block, and `quotedTemplate({ text, firstLine }, inputSchema)` reads a template as a quote, with each value it reads in brackets (see [Told as what happens](#told-as-what-happens)). An output that reads a value, `{{ input.x }}` or `{{ x }}`, is the last name of its path in bold, its filters dropped, and any other output stays as it is in a code span; a condition or a loop whose end tag is on a later line than its opening tag reads as lines and paragraphs: `{% if %}` is an italic line _When …:_ with its condition as written, `{% unless %}` _Unless …:_, `{% elsif %}` _Or when …:_, `{% else %}` _Otherwise:_ and `{% for item in list %}` _For each item of list:_, each a paragraph of its own, and the end of a condition, `{% endif %}` or `{% endunless %}`, or of a loop ends the paragraph it opened, so what follows it is not read as part of it; the ends of other blocks are nothing; `{% comment %}…{% endcomment %}` and `{% # %}` are left out; `{% raw %}` keeps its text; and any other tag stays as it is in a code span. A condition or a loop whose end tag is on the line of its opening tag is instead a phrase of that line, in `inline-constructs.ts`, read inside out when one holds another, with what each case writes read as the rest of the line is and `nothing` for a case that writes nothing: `{% if c %}A{% endif %}` is `A when c`, with the condition as the italic line writes it, `{% unless c %}A{% endunless %}` `A unless c`, each `{% elsif d %}B` after it `, B when d` and `{% else %}E` `, otherwise E`; `{% for item in list %}A{% endfor %}` is `each item of list as A`, and its `{% else %}E` `, otherwise E`. The full stop, question mark or exclamation mark that ends what a case of a condition writes, before any closing quote or bracket, moves to the end of the whole phrase, the last such mark when more than one case ends with one, so `{% if c %}Be kind.{% else %}be firm.{% endif %}` is `Be kind when c, otherwise be firm.`; a case that writes only the mark keeps it. A loop that ends with a condition that holds for all but its last item, `{% unless forloop.last %}`, `{% if forloop.last == false %}` or `{% if forloop.index < forloop.length %}`, and that writes only text and outputs, reads it as what separates its items: `, separated by commas` for `, `, `semicolons` for `; `, `and` for `and`, `slashes` for `/` and `a line each` for a line break, the one line break a loop may hold and still be a phrase of its line; any other separator is quoted, trimmed, in a code span, and one that is only space adds nothing. So `{% for f in fs %}{{ f.name }}{% unless forloop.last %}, {% endunless %}{% endfor %}` is `each f of fs as **name**, separated by commas`. Inside a phrase, a tag the reader does not know stays in a code span, and the `{% else %}` of a `{% case %}` is its case's. The author's paragraphs and lists come through as they are. Each part past `mostReadCharacters`, 8,192 characters of the template, is cut at the last line or word that fits, with a line that says the line of the document where it goes on. A part is written as its author wrote it, through `paragraphsAsWritten(paragraphs)` of `markdown-text.ts`, with two changes, so that it neither splits the overview nor reads the rest of it into a block: each line `## Details`, at which the page that shows an overview splits it, is written with its first character escaped, `\## Details`; and a fence or an HTML block that the part, or its cut, leaves open after its last line is closed there with the line that closes it, before the line that says where the part goes on. A save refuses none of these in a template.
-
-The overview counts every character bound it has in code points, through `charactersIn(text)` and `firstCharacters(text, count)` of `src/plain-language/characters.ts`, so a character outside the Basic Multilingual Plane, an emoji among them, counts once, as a person reads it, and is never cut apart: a field's description, each part of a template, and a value in words (`inWords`, at most 300 characters, of `src/plain-language/in-words.ts`), which the sentence of a run's output says too, and the index exports `charactersIn`. A text of more than twice the bound in UTF-16 units holds more characters than the bound, so `inWords` does not count it. The description a definition's event shows is cut by the same `firstCharacters`.
-
-`plainTextOf(phrases)` of `markdown-text.ts` writes phrases as plain text, each kind as it reads, a definition by its `title` or its readable name and a server by its name, as the words of a refused value say what a field expects. Through the same reading, `markdown-text.ts` answers what a written text may not hold, which `writtenTextIssues` refuses: `headingLinesIn(text)`, the lines that make a heading, an ATX line, one to six `#` and a space, a tab or the end of the line, at most three spaces into its container, so `> # Words`, `- # Words` and `> - # Words` are headings and a line indented four spaces after a line of a paragraph is not, or a setext underline, a line of `=` or `-`, at most three spaces in, directly under a line of a paragraph in its own container, so `> ===` under `> Title` makes one and `---` under `- item` or `===` under `> quote` does not; `unclosedBlockIn(text)`, the line that opens a fence or an HTML block still open after the last line, with the line that closes it, but an element or a lone tag, which the blank line after it ends, and one in a list item, which the next line of the overview, at the left margin, ends; `detailsLinesIn(text)`, each line `## Details`; and `firstParagraphOf(text)`, the lines of a text up to the first blank line outside a fence or an HTML block, read as the words after a colon read it, and whether its last line is a line of a paragraph. The reading was held against the CommonMark parser that the documentation site bundles, over generated texts of two to six lines drawn from a set of edge cases, which the repository does not keep, and differs inside quotes: where a list item opens inside a quote, a later line of the quote indented to the item's content is not read inside it, so an underline there is not refused as a heading, though CommonMark reads one; a fence or an HTML block inside a quote is read as the quote's text, so a heading line in one is refused, which CommonMark reads as code; a tab after a quote marker is read as four columns, where CommonMark counts the columns from the marker, so `>` and a tab before `# Words` is not refused as a heading, which CommonMark reads as one; and a line indented four spaces that would open a list item, right after a paragraph of a quote, is read as that paragraph going on, where that parser reads it as code after the quote. `schemaBlocks(schema, unknown)` says the sentence `unknown` for a schema a document leaves out, and `describedSchemaBlocks(schema)` the words of a schema that is always there, which takes none.
-
-`src/overview-words` holds, a file to each, the sentences of an overview that more than one capability writes, that the read writes or that an overview told as what happens writes (see [Told as what happens](#told-as-what-happens)), each one function with its variants: `theToolOf({ server, tool })`, `the tool **t** of **s**`, and `theToolAsWritten({ server, tool })`, the tool in code and the server a `server` phrase, ``the tool `t` of the server **s**``, as Details names them, in `tool-words.ts`; `templateLead(lead, 'run' | 'build')`, the lead of a template read as a person reads it, such as `Its message, with the values of each run in place of the names in bold:`, in `template-words.ts`; `undescribedSentence('input' | 'output')`, `It takes any input, which its document does not describe.` or `It produces any output, which its document does not describe.`, which **What it needs** and **What it produces** say of a value without a schema, and `undescribedSentence('input' | 'output' | 'value', 'alone')`, `Any input, which its document does not describe.` and the like, in `value-words.ts`; `shownUnderSource('program' | 'value', ...said)`, the words that name a program followed by `is TypeScript and is shown under Source.`, or those that name a value followed by `, shown under Source.`, in `source-words.ts`; `callerSentence(user)`, the sentence of a definition that uses another, in the words of its relation, `The workflow **Monthly website report** calls it in step 3, **Review**, and in step 6, **Review again**.`, its steps listed by `listedPhrases` with a serial comma, since each holds one, in `caller-words.ts`, given a `DefinitionUser` of `src/used-by`; `calledSentence(called)`, the sentence of a definition another calls, given a `DefinitionCalled` of `src/used-by`, `It calls the reasoning function **Write report**: Writes the report.`, with the first paragraph of its description after a colon, through `firstParagraphOf`, so a blank line inside a fence does not end it and no fence that its place after the colon would leave open is in it, as written and never lowered, ended as a sentence when its last line is a line of a paragraph, or with nothing after its name when it has no description, `, which is retired.` for a retired one and `, which the playbook does not have yet.` for one the playbook does not hold, in `called-words.ts`; and `exactNameWithType(definition)`, its name as written, in code, and its type after an article, `` `weekly-review`, a workflow ``, in `exact-name-words.ts`. The overviews of the capabilities and the read write these sentences through these functions, so each is written in one place, and the words take what they are given as types of `src/overview` and of `src/used-by`, whose `called-definitions.ts` holds the format of a call, its `CallingStep` and the `UserRelation`, and whose `user-relations.ts` holds `DefinitionUser` and `DefinitionCalled`: `src/used-by` runs the words, and the words import only types from it.
-
-### Told as what happens
-
-An overview told as what happens is one whose capability's `overview` gives `ToldParts`, a body of blocks without a heading and an appendix, under Details, of the technical facts. These rules write it, each sentence pattern in one function of `src/overview-words` that the capabilities compose, so that no capability writes a sentence of its own:
-
-- **A field's meaning**, `fieldMeaning(name, description, listing)` of `field-meanings.ts`: the clause of the field's description up to the first `. `, `;` or `:` or its end, read as one line and without its last full stop, and, for a field listed among others, up to the first `, ` too, so `The month, such as April 2026` is `the month` beside other fields and a field alone keeps its commas. The clause is the meaning, its first word lowered, only when that word is one of the, a, an, its, their, each, every, one, how, what, which, who, where and whether and the clause holds at most `mostMeaningCharacters`, 60, characters, counted in code points; otherwise the meaning is the field's name in words with `the`, `the approver` for a field described as `Chat user id of the approver` and `the prev label` for an undescribed `prev_label`. The body names a field by its meaning; the table of fields under Details holds its name and its whole description.
-- **The body** is the opening, the description as a sentence where it is one paragraph (see `composedOverview` above), the `about`, a diagram where the flow branches, and then what happens, a paragraph to each step. `givenClause(schema)` of `field-sentences.ts`, `It is given the approver, the month and the draft report`, says what a definition is given, and `answeredWords(schema, shape?)` what it answers with, each as `listedPhrases` lists them with `'serial-when-items-hold-commas'`: the meanings of at most `mostToldFields`, 8, fields and, past them, the required ones, at most 8, and `the rest, under Details`, or `what Details lists` where none is required; a field with an `enum` followed by `, one of approve, revise and skip`, `enumerationWords(values)`, which names twelve values and how many more there are; a field of the answer that may be left out by `, when there is one`; and a field with a `default` by `, else its default`. An input of no fields is `any input`, or the meaning of its schema's description where that is a noun phrase, and an output of no fields is the shape it declares, `a series over time`, `values by category`, `rows under named columns` or `a text of Markdown` (`shapeNouns` of `src/shapes/shape-words.ts`), `one of …` of its `enum`, or what kind of value it is without its bounds, such as `text`, `a whole number` or `a list of records`. No bound, type with its bound, `yes`, `no` or name of a described field is written in the body.
-- **A computation function**: `computedSentence({ input, output, shape })` of `computation-words.ts`, `It is given the month to report on, as 2026-04 or April 2026, and a program works out the answer, the same every time for the same input: the month, the label, …`, and no diagram; under Details, the tables of its input and output and **Program**, `programSection(language)`, `TypeScript, in the definition's document.`
-- **An interaction function that asks a person**, in `request-words.ts`: `messageLead({ input, party, server? })`, `It is given the approver, the month and the draft report. It sends the approver this message through **gas**, with the run's values in place of the words in brackets:`, then the message as a quote; then, in one paragraph, `replyWaitSentence(expiresMs, readEveryMs)`, `It waits up to 7 days for a reply to that message, reading the replies at least 15 seconds apart.`, where it reads replies, or `answerWaitSentence(expiresMs)`, `It waits up to 2 days for the answer, which whoever may write to the playbook gives.`, where it delivers through a tool and reads none; `replyRuleSentence(rule, answerSchema)`, a clause to each value of a part read from the first word, `A reply whose first word is approve, approved or yes gives the verdict approve; one whose first word is reject gives reject`, and `whatever follows the first word is the note` and `the whole reply is the note` for the parts read from the rest and from the whole; `notTakenSentence(told)`, `A reply that begins with any other word is not taken`, with `, and the person is told how to answer` where `tell` is written, after a rule that reads a first word; and `unansweredSentence(expiresMs)`, `If nobody answers within 7 days, the request ends unanswered.` A request left in the inbox says `inboxSentence('answer')`, `It leaves the message in the playbook's inbox, where whoever may write to the playbook answers it.`, before its expiry; a notification sent through a tool says `notificationSentence`, `It sends the message and waits for nothing.`, and one left in the inbox `inboxSentence('nothing')`, `… where whoever may write to the playbook reads it, and waits for nothing.` Last, `answerSentence(schema)`, `It answers with the verdict, one of approve and reject, and the note, when there is one.` The party is the meaning of the value that `to` reads where `to` is one output, `valueMeaning`, `to` as written where it is one line of text, and `partyNamedUnderDetails`, `the party named under Details`, otherwise. A function that reads replies by their first word draws `replyDiagram` of `reply-diagram-words.ts`: the fields given, `A message to the approver through gas`, `Waits up to 7 days for a reply`, a decision, `The first word of the reply`, with an edge of the words of each value to a terminal of the value, a dashed edge back to the wait, `any other word`, with `, told how to answer` where `tell` is written, and a dashed edge `no reply` to `unanswered`. Under Details: the tables of the input and of the answer, `answerSection(schema)`, which says of a notification that it takes none, and **Where**, `whereSection`: `to` and `from` as written, the tools and servers it is sent and read through, the `wait` and the `expires` as written.
-- **An interaction function that asks a system**, in `call-words.ts`: `callSentence`, `It is given the month and asks **ads** with its tool campaign-costs, once per run. It answers with what it answered under rows: a list of records.`, or `with the whole of what the tool answered` without `read`, and no diagram; under Details, the tables of its input and output and **Call**, `callSection`: the tool and its server, where `read` points, and each argument as written.
-- **A quote**, `quotedTemplate(template, inputSchema)`: each paragraph of the template a paragraph of a block quote, its lines kept as `blockQuoted` keeps them, after the lead `quoteLead('run' | 'build', ...lead)` of `template-words.ts`, `…, with the run's values in place of the words in brackets:`. Each value the template reads is its meaning in brackets, `inBrackets`, escaped so that Markdown keeps them and reads no link, `\[the month\]`: an `input.` by the meaning of the field the input schema has there, `valueMeaning(path, inputSchema)` of `field-meanings.ts`, read among the fields of the record that holds it, or by the last name of its path in words with `the` where the schema has no such field; `today` as `today's date` and `now` as `the time now`; the variable of a loop by its last name in words; and any other value by its last name in words with `the`. Filters are dropped, a value the reader cannot read and a tag it does not read stay in code spans, the author's `*…*` and `**…**` stay around the brackets, and a template cut at `mostReadCharacters` says where it goes on after the quote.
-- **A label of a diagram** holds at most `mostDiagramLabelCharacters`, 60, characters, and is cut past them with an ellipsis, by `diagramLabel(text)` of `diagram-labels.ts`.
-- **Details** holds the tables of the fields with their names, whole descriptions, types and bounds, the exact names of what is called and of who calls, which the read writes, the tools and servers by name, and durations and expressions as written, so every fact the sections of these overviews said is in the body or in Details. A server is a `server` phrase wherever these words name one, but in the label of a diagram, which is text.
+Measured on 2026-10-22 with Node 26.10.0 on a machine of 16 cores, by `pnpm --filter @on.auto/definitions measure:loads` (`measure/measure-loads.ts`), on SQLite: 50 versions of a document of 65,524 bytes, as many fields as one document holds, each saved with an overview at its bound, keep events of 191,931 bytes each, of which the overview takes 16,386, beside the source and the input schema the summary gives; a load of their type, the parse of each event included, takes 25.3 ms of processor time at the median of 15, and a read of the definition 25.7 ms, at a load average that fell from 65 to 10 from other work.
 
 ### Used by
 
-No fact records who uses a definition; the registries of the playbook are the index. A capability names what a definition uses in the `details` its `summarize` gives, which the registry keeps: a workflow and a prediction function as `calls`, each a `CalledDefinitionSchema`, a call with its role, `{ type, name, role }`, or a call with the steps that make it, `{ type, name, steps }`, a workflow the definitions its `run_definition` tasks name with a written-out `type` and `name`, in the order of its document and each once, and a prediction function its trainer, `source.through`, with the role `trainer`, then the function that approves a version, `evaluation.activate.approved`, with the role `approver`, both interaction functions; and a recall function as its `filters`. Each step of a call is a `CallingStep`, `{ number, name }`, the number its step has in the definition's diagram and the name of its task, and the details may carry a `title`, the definition's own readable name. `src/used-by` holds the reader: `usersOf(registries, { type, name })`, given every type's registry of the playbook, lists the active definitions but the one named whose details name it, each `{ type, name, title?, relation, steps? }`, its `title` the one its details carry, read only once a use of the definition is found, ordered by type, the five function types in their order, then workflows, then any other type, and then by name. A call with a role is the relation of its role, a trainer's `sends-builds` and an approver's `asks-approval`, and the calls with steps the relation `calls`, once, with every step they carry, so a call has a role or steps and nothing else names a user; a filter on a run event, `run_started`, `run_succeeded`, `run_rejected` or `run_failed`, whose `subject` is `/`, is `remembers-runs`, and a filter on a definition event, `definition_created`, `definition_updated` or `definition_retired`, whose `source` is `/definitions//`, is `remembers-changes`, since those events carry no subject. A user with two relations is listed once for each. `definitionsCalled(registries, details)` lists the definitions a definition calls, those its `calls` name with their `steps`, in their order, each as the playbook holds it, a `DefinitionCalled`: `active` with its `description` and its `title`, `retired`, or `missing`; a call with a role is not one of them, so a prediction function's trainer is said by its own overview. Every operation that answers a definition with its overview, `get_definition`, `create_definition`, `update_definition` and `retire_definition`, does it through `answeredDefinition` of `src/operations/definition-reading.ts`: given the registry of the definition's own type, which the read loaded or the save left, it loads the registry of every other type it was given, once each, and writes the stored overview with what they hold through `overviewReadFrom(registries, type, stored)` of `src/used-by/overview-at-read.ts`, which decodes it and gives it `usersOf` and `definitionsCalled` through `overviewAtRead(overview, { users, called })`, so a definition saved after a workflow that calls it is answered with that workflow. `overviewAtRead` adds two sections after those the capability composed, each only when it has something to say: **What it calls**, a paragraph for each definition called, in the words of `calledSentence`, then **Used by**, a line for each user, in the words of `callerSentence`; and, after the capability's own appendix, the same two headings, which list the exact names of what is called and of who uses it, each once, in the words of `exactNameWithType`. It writes the whole through `markdownOf`, the body first and the appendix last, so an overview near its bound loses its appendix before its body and keeps its cut line, and `## Details` is written only when a block of the appendix is kept; a definition nothing uses that calls nothing is its stored overview as written. A phrase of the stored overview is written as the writer writes it: a definition by its title, or its name in words, in the body and by its name in the appendix, and a server by its name. `list_definitions` answers no overview. The words of a relation are `calls it`, followed by ` in step , ****` for each step that calls it, `sends it builds to train`, `asks it to approve a version`, `remembers its runs` and `remembers its changes`, each after `The  ****`.
+No fact records who uses a definition; the registries of the playbook are the index. A capability names what a definition uses in the `details` its `summarize` gives, which the registry keeps: a workflow and a prediction function as `calls`, each a `CalledDefinitionSchema`, a call with its role, `{ type, name, role }`, or a call with the steps that make it, `{ type, name, steps }`, a workflow the definitions its `run_definition` tasks name with a written-out `type` and `name`, in the order of its document and each once, and a prediction function its trainer, `source.through`, with the role `trainer`, then the function that approves a version, `evaluation.activate.approved`, with the role `approver`, both interaction functions; and a recall function as its `filters`. Each step of a call is a `CallingStep`, `{ number, name }`, the place of its task among the workflow's tasks in the order of its document, nested ones included, and the name of its task. `src/used-by` holds the reader: `usersOf(registries, { type, name })`, given every type's registry of the playbook, lists the active definitions but the one named whose details name it, each a `DefinitionUser`, `{ type, name, relation, steps? }`, ordered by type, the five function types in their order, then workflows, then any other type, and then by name. A call with a role is the relation of its role, a trainer's `sends-builds` and an approver's `asks-approval`, and the calls with steps the relation `calls`, once, with every step they carry, so a call has a role or steps and nothing else names a user; a filter on a run event, `run_started`, `run_succeeded`, `run_rejected` or `run_failed`, whose `subject` is `/`, is `remembers-runs`, and a filter on a definition event, `definition_created`, `definition_updated` or `definition_retired`, whose `source` is `/definitions//`, is `remembers-changes`, since those events carry no subject. A user with two relations is listed once for each. `DefinitionUserSchema` of `src/used-by/user-relations.ts` is the one shape of a user, a union of a call with its `steps` and of the other relations, from which `DefinitionUser` derives and which `DefinitionSchema` answers as `used_by`. Every operation that answers a definition, `get_definition`, `create_definition`, `update_definition` and `retire_definition`, does it through `answeredDefinition` of `src/operations/definition-reading.ts`: given the registry of the definition's own type, which the read loaded or the save left, it loads the registry of every other type it was given, once each, and answers the users `usersOf` finds as `used_by`, left out when there are none, so a definition saved after a workflow that calls it is answered with that workflow. `list_definitions` answers no users.
 
 ## The words of a refused value
 
 `wordedIssuesOf(schema, value)` (`src/checking/schema-refusals.ts`) is the one wording of the issues of a value its compiled schema refuses, for every refusal of an input or an answer a person or an agent gives: the input of a reasoning, interaction, recall, computation or prediction function or of a workflow, and the answer of a request. It answers each issue `issuesOf` of `@on.auto/json-schema` found at its pointer, unchanged, with one sentence for its `detail`, built from the schema and the pointer alone, so it is the same code for every field and every domain; `refusedInput(issues)` is the `InvalidInput` whose `detail` is the sentences in order, and `checkedInput(input, schema, noun)` the one check of the input of a run that the reasoning, interaction, prediction, recall and computation capabilities and the workflow capability share: an input nested past 512 levels is refused for the noun, `The input nests more than the 512 levels a computation function takes`, with no kind, as the operation's own bound of 512 levels refuses it first at `/input`, and one its schema refuses in those words, of the kind `input_schema` (`InvalidInputKindSchema` of `@on.auto/operations`), which says the definition refused the input it was given before it ran, so a caller knows to correct the input. `refusedByTheInputSchema({ detail, issues })` is the one place that sets the kind, which `checkedInput` calls and the reasoning capability calls for an input that is not the object its input schema takes. The kind travels with the rejection: `rejectedForInput` records it on the run's `invalid_input` rejection, which `get_run`, `list_runs`, the history of the run and its words show, a retry under the run id answers again, and a workflow's step reads as `$error.kind`. A sentence names the field, then says the rule it broke:
 
 - the field is its name, the path of a nested one in words, `address, then city`, with an item of a list by its place, `lines, then item 2`, then the field's `description` as its author wrote it, its first letter included, without a final full stop, and its `examples`, `such as “ACME-001”`, unless the description says “such as” or holds one of them as a whole word; the value as a whole is `The input`, or `The answer`, as the schema's `what` names it;
-- a bound it broke is said as the overview says it, from the keyword and its value alone: `at least 6 characters`, `at most 100`, `more than 0`, `at least 1 item`, `a multiple of 0.5`, `no item twice`;
-- a value of another kind is told what the field takes, in the overview's words of the field: `a whole number, from 1 to 100`, `one of: draft, sent, approved`, `a date`;
-- a string out of its format is told the form and an example of it, as a bound is said: `a date such as 2026-04-01`, `a date and time such as 2026-04-01T09:30:00Z`, `a time such as 09:30:00Z`, `an email address such as name@example.com`, `a URI such as https://example.com`, `a UUID such as 0f8fad5b-d9cb-469f-a165-70867728950e`, the form from `forms` and the example from `stringFormats` of `@on.auto/json-schema`; the example is left out where the field's description or its examples already give one, by the test the overview's `examplesWordsOf` makes, `field, such as “2026-05-01”: expected a date`, and kept for the value as a whole, whose description and examples the sentence does not say;
+- a bound it broke is said from the keyword and its value alone: `at least 6 characters`, `at most 100`, `more than 0`, `at least 1 item`, `a multiple of 0.5`, `no item twice`;
+- a value of another kind is told what the field takes, in the words of its schema: `a whole number, from 1 to 100`, `one of: draft, sent, approved`, `a date`;
+- a string out of its format is told the form and an example of it, as a bound is said: `a date such as 2026-04-01`, `a date and time such as 2026-04-01T09:30:00Z`, `a time such as 09:30:00Z`, `an email address such as name@example.com`, `a URI such as https://example.com`, `a UUID such as 0f8fad5b-d9cb-469f-a165-70867728950e`, the form from `forms` and the example from `stringFormats` of `@on.auto/json-schema`; the example is left out where the field's description or its examples already give one, by the test `examplesWordsOf` makes, `field, such as “2026-05-01”: expected a date`, and kept for the value as a whole, whose description and examples the sentence does not say;
 - a missing field is said at the field that holds it, `The input: expected the field month`, with the description of the field that is missing, and one the schema does not allow as `expected no field extra`, or `no item 3` in a list;
 - a name that `propertyNames` refuses is said as the name and the field that has it, `the name long of names: expected at most 2 characters`, and a name where the names allow none as a field the schema does not allow is, `names: expected no name long`;
 - an issue no keyword names keeps the words of the validator, `expected exactly one member to match`, after the field.
 
-There is no `pattern` to word, nor a `format` other than the six, since the dialect refuses them. The issues of a refusal reach a workflow that called the function unchanged, under `/input`, and the workflow's rejection keeps them.
+There is no `pattern` to word, nor a `format` other than the six, since the dialect refuses them.
+
+The words are those of `src/schema-words`, as plain text. `expectedWordsOf(schema, root)` says what a schema takes, following a `$ref` within the document, to `#`, `#/$defs/` or `#/definitions/`, through `schema-reading.ts`: `text`, or the form of a format the runtime checks, `a date`, `a date and time`, `a time`, `an email address`, `a URI` or `a UUID`, from `forms` of `schema-nouns.ts`, keyed by the format names of [`@on.auto/json-schema`](../json-schema/README.md#formats), `a whole number`, `a number`, `yes or no`, `nothing`, `one of: a, b, c` for an `enum` of up to 12 values named, `exactly x` for a `const`, up to four alternatives of `anyOf`, `oneOf` or a list of types joined with `or`, `a list of` its items in the plural, and `a record with:` its first twelve fields one level down, each with its description and its words; then its bounds and its default, from `schema-bounds.ts`. `descriptionOf(schema)` is a field's description as one line, cut at 300 characters, and `examplesWordsOf` and `givesAnExample` its examples in words unless its description gives one. Each bound of these words is counted in code points, through `firstCharacters(text, count)` and `charactersIn(text)` of `src/plain-language/characters.ts`, so a character outside the Basic Multilingual Plane, an emoji among them, counts once and is never cut apart, as a value in words is (`inWords`, at most 300 characters, of `src/plain-language/in-words.ts`), which the sentence of a run's output says too. A text of more than twice the bound in UTF-16 units holds more characters than the bound, so `inWords` does not count it. The description a definition's event shows is cut by the same `firstCharacters`. The issues of a refusal reach a workflow that called the function unchanged, under `/input`, and the workflow's rejection keeps them.
 
 ## The checked worker
 
@@ -299,25 +276,25 @@ There is no `pattern` to word, nor a `format` other than the six, since the dial
 
 Each event is a `PublicEvent` of `@on.auto/operations`, `{ id, type, summary, data, metadata }`: `id` is the id of the message the event presents, or for a step event its record's message id, an underscore and its number in the record, `type` a public name, `summary` plain words with no ids, `data` the fact, at most 4 KiB as JSON, under one rule for a field that may be large, `input`, `output`, `record`, `answer`, `arguments`, `result` and a published event's `data`: whole within 2 KiB as JSON and otherwise its size under its name and `_bytes`, the largest replaced first while the data passes 4 KiB, and `metadata` the record's context under wire names, with `causation_id` the id of the message or step event that directly caused it, or null, and `at` the fact's own time. A tool's `result` is shown on `get_event` alone, beside `answer`, the document read from it. `makeDefinitionPresenters(capabilities)` gives the presenters of the four stream kinds this package owns and of the stream kinds the capabilities composed declare they own, which the server also passes to the feed of the playbook, `list_playbook_events` of `@on.auto/playbooks`, and to `get_event`, which reads one record by its id and presents it with every field whole:
 
-| Stream kind   | Stored type and public name                | `data`                                                                                                                                                                                                                                                                                       |
-| ------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `runs`        | `run_started`                              | `input`, `calls_tools` and `finishes_later` when recorded                                                                                                                                                                                                                                    |
-| `runs`        | `run_succeeded`                            | `output`, `record`                                                                                                                                                                                                                                                                           |
-| `runs`        | `run_rejected`                             | `reason`, `detail` cut at 1024 bytes, `kind` and `because` when given, for `invalid_input` `issue_count` and the first five `issues`, and `record` when the rejection kept a record                                                                                                          |
-| `runs`        | `run_failed`                               | `incident` when recorded                                                                                                                                                                                                                                                                     |
-| `runs`        | `run_cancel_requested`                     | `kind`, `reason` cut at 1024 bytes                                                                                                                                                                                                                                                           |
-| `runs`        | `tool_call_started`                        | `number`, `call_id`, `server`, `tool`, `read_only` when marked, `arguments_bytes`, and `arguments_sha256` and `arguments` where kept                                                                                                                                                         |
-| `runs`        | `tool_call_answered`                       | `number`, `is_error`, `result_bytes`, `shown_bytes` when the model read less, `duration_ms`, `jsonrpc_id`, `server_request_id`, and `result_sha256` and `answer` where kept                                                                                                                  |
-| `runs`        | `tool_call_failed`                         | `number`, `because`, `detail` cut at 1024 bytes, `duration_ms`, `jsonrpc_id` and `server_request_id` where there is one                                                                                                                                                                      |
-| `runs`        | `run_deferred`                             | `record`, and what the run's capability shows of the record, when it gives words for it                                                                                                                                                                                                      |
-| `runs`        | `request_answered`                         | `answer`, `claimed_for` cut at 256 bytes when given, and the `reply`'s `id` and `sender` when a reply brought it, never the reply's words                                                                                                                                                    |
-| `runs`        | a fact a capability declares               | what that capability shows of it, in its words, through the `present` it declares beside the fact's schema                                                                                                                                                                                   |
-| `tool-calls`  | `tool_call_started`                        | a call made by hand: `call_id`, `server`, `tool`, `read_only` when marked, `arguments_bytes`, and `arguments_sha256` and `arguments` where kept; `handCallPresenter` of `@on.auto/tool-servers` presents the three                                                                           |
-| `tool-calls`  | `tool_call_answered`, `tool_call_failed`   | `is_error` or `because`, `result_bytes`, `duration_ms`, `jsonrpc_id`, `server_request_id`, and `result_sha256` and `answer` where kept, with no `number`, since the stream holds one call                                                                                                    |
-| `definitions` | `definition_created`, `definition_updated` | `source_bytes`, `overview_bytes`, the bytes in UTF-8 of the Markdown the overview its content keeps writes, without what a read adds, which is not the size of what the fact stores, the first 300 characters of `description`, `input_schema_bytes`, `output_schema_bytes`, `warning_count` |
-| `definitions` | `definition_retired`                       | none                                                                                                                                                                                                                                                                                         |
-| `events`      | `event_published`                          | `event_id` and `event_type` cut at 256 bytes, `source` and `subject` cut at 1024, `time`, `data` when it has data, the attributes the playbook `filled`                                                                                                                                      |
-| `reactions`   | `reaction_refused`                         | `count`, the last `reason` cut at 1024, and the `minute` it counts; the workflow host records it through `reactionRefusalDecider`, and its context names the workflow and its version, with the playbook as its actor                                                                        |
+| Stream kind   | Stored type and public name                | `data`                                                                                                                                                                                                                |
+| ------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `runs`        | `run_started`                              | `input`, `calls_tools` and `finishes_later` when recorded                                                                                                                                                             |
+| `runs`        | `run_succeeded`                            | `output`, `record`                                                                                                                                                                                                    |
+| `runs`        | `run_rejected`                             | `reason`, `detail` cut at 1024 bytes, `kind` and `because` when given, for `invalid_input` `issue_count` and the first five `issues`, and `record` when the rejection kept a record                                   |
+| `runs`        | `run_failed`                               | `incident` when recorded                                                                                                                                                                                              |
+| `runs`        | `run_cancel_requested`                     | `kind`, `reason` cut at 1024 bytes                                                                                                                                                                                    |
+| `runs`        | `tool_call_started`                        | `number`, `call_id`, `server`, `tool`, `read_only` when marked, `arguments_bytes`, and `arguments_sha256` and `arguments` where kept                                                                                  |
+| `runs`        | `tool_call_answered`                       | `number`, `is_error`, `result_bytes`, `shown_bytes` when the model read less, `duration_ms`, `jsonrpc_id`, `server_request_id`, and `result_sha256` and `answer` where kept                                           |
+| `runs`        | `tool_call_failed`                         | `number`, `because`, `detail` cut at 1024 bytes, `duration_ms`, `jsonrpc_id` and `server_request_id` where there is one                                                                                               |
+| `runs`        | `run_deferred`                             | `record`, and what the run's capability shows of the record, when it gives words for it                                                                                                                               |
+| `runs`        | `request_answered`                         | `answer`, `claimed_for` cut at 256 bytes when given, and the `reply`'s `id` and `sender` when a reply brought it, never the reply's words                                                                             |
+| `runs`        | a fact a capability declares               | what that capability shows of it, in its words, through the `present` it declares beside the fact's schema                                                                                                            |
+| `tool-calls`  | `tool_call_started`                        | a call made by hand: `call_id`, `server`, `tool`, `read_only` when marked, `arguments_bytes`, and `arguments_sha256` and `arguments` where kept; `handCallPresenter` of `@on.auto/tool-servers` presents the three    |
+| `tool-calls`  | `tool_call_answered`, `tool_call_failed`   | `is_error` or `because`, `result_bytes`, `duration_ms`, `jsonrpc_id`, `server_request_id`, and `result_sha256` and `answer` where kept, with no `number`, since the stream holds one call                             |
+| `definitions` | `definition_created`, `definition_updated` | `source_bytes`, `overview_bytes`, the bytes in UTF-8 of the overview, when the content has one, the first 300 characters of `description`, `input_schema_bytes`, `output_schema_bytes`, `warning_count`               |
+| `definitions` | `definition_retired`                       | none                                                                                                                                                                                                                  |
+| `events`      | `event_published`                          | `event_id` and `event_type` cut at 256 bytes, `source` and `subject` cut at 1024, `time`, `data` when it has data, the attributes the playbook `filled`                                                               |
+| `reactions`   | `reaction_refused`                         | `count`, the last `reason` cut at 1024, and the `minute` it counts; the workflow host records it through `reactionRefusalDecider`, and its context names the workflow and its version, with the playbook as its actor |
 
 The run presenter asks the run's capability, by the definition type in the record's context, for the words of its deferral, `runWords`: `deferral(record)` answers the summary and the fields of the record it shows, or nothing, under the public type `deferralType`, `run_deferred` unless the capability names its own, as an interaction function names `interaction_requested`. A fact a capability declares is shown by that capability, whichever run it is on, and a fact no capability composed declares shows as no event. A capability that gives no words for its deferral, as a workflow gives none, shows it as no event: it stays on the run's stream, where `status` and a retry read it, and nothing names it as its cause, so a history goes on from a start to what the run did next, the inputs and steps of a workflow directly. An interaction function's deferral is its request, shown as “waiting for an answer”. Sizes are of the value as JSON in UTF-8, the document's of its own text. A cut is made at a code point, measured as JSON so that escapes count; an issue's `detail` at 256 bytes and its `pointer` at 128; a tool call's `call_id`, `server`, `tool` and `server_request_id` at 256 bytes, its digests and a `jsonrpc_id` that is text at 128. The latest message of a running run may be a tool event, which `list_runs` shows as `started`. A test over the event schemas of both deciders holds every stored type to a decision of its presenter, and the largest record each type can hold to 4 KiB of `data`. Who acted, when, the run, the definition and the chain are never in `data`: they are the row's `metadata`, from the record's context.
 
@@ -325,7 +302,7 @@ The run presenter asks the run's capability, by the definition type in the recor
 
 The definitions of one type in a playbook are one stream, named `definitions/{type}` relative to the playbook. Its events carry a `type` and the fact; the definition's type, name and version, who recorded it and when are the context of the message, a `DefinitionContext` of `@on.auto/operations` that each fact's schema names beside its data, so the decider's `context` gives every event of a decision one and a record without its definition or its version does not decode:
 
-- `definition_created`, version 1 in its context, with the `content`: the `source`, the `overview` composed from it, in its encoded form, which the fold carries without decoding it, and the `description`, `input_schema`, `output_schema`, `warnings` and `details` its capability gave
+- `definition_created`, version 1 in its context, with the `content`: the `source`, the `overview` its author gave, when one was given, and the `description`, `input_schema`, `output_schema`, `warnings` and `details` its capability gave
 - `definition_updated`, the new version in its context, with the whole new `content`
 - `definition_retired`, the version it retires in its context
 
@@ -458,8 +435,8 @@ A run that finishes later records facts after the call that started it returned,
 
 ## Testing
 
-`@on.auto/definitions/testing` exports `echo`, a small real capability for the tests of this and other packages. Its definition document is a JSON object with a string `greeting`, an optional string `description`, optional `warnings`, a list of strings, and optional `triggers`, which its summary gives back; a run takes a JSON object and answers `{ greeting, input }`. `nothingSaved` is the `SavedDefinitions` of a playbook with nothing active and no content kept, for a check a test runs outside a save. `steppingClock(stepMs, wall)` is the Effect `Clock` of a test that renders: its monotonic time, which a render counts on, starts at nothing and moves by `stepMs` each time it is read, so it stands still with a step of nothing, while it tells the time and sleeps as `wall` does, the real clock unless given. `standingClock` is it with a step of nothing, `standingRenderClock` the render clock that clock reads as, and `onTheStandingClock(effect)` runs an effect on it, so the tests of every capability render on one clock that stands still. `checkingWithin(pool, deadlineMs)` is the pool whose checks at save may take `deadlineMs`, `checksAwaitedAtMostMs`, 30 s, unless given, for the tests that check a document with the compiler in a real worker, which a busy machine can slow past the 2 s a save allows; the refusal at that deadline is held by a pool that answers it, in `src/program-check/save-check.test.ts`. Those tests keep the 10 s a checker's worker may take to start, `mostCheckStartMs` of `@on.auto/workflow-engine`, after which a check ends `busy`: the first check of a pool starts its worker within it on purpose, since the pool's own tests prove that bound with a worker slow to start. `sectionIn(overview, heading)` is the text an overview writes under one of its `##` headings, up to the next, or nothing for a heading it does not write, `overviewTitled(title, ...opening)` an `Overview` of a title and an opening alone, for a test that records a definition's content itself, and `overviewAsRead(definition, held)` the overview of a definition as a read writes it, through the read's own composition, in a playbook that holds it and each `HeldDefinition` given, with its status, description and details.
+`@on.auto/definitions/testing` exports `echo`, a small real capability for the tests of this and other packages. Its definition document is a JSON object with a string `greeting`, an optional string `description`, optional `warnings`, a list of strings, and optional `triggers`, which its summary gives back; a run takes a JSON object and answers `{ greeting, input }`. `nothingSaved` is the `SavedDefinitions` of a playbook with nothing active and no content kept, for a check a test runs outside a save. `steppingClock(stepMs, wall)` is the Effect `Clock` of a test that renders: its monotonic time, which a render counts on, starts at nothing and moves by `stepMs` each time it is read, so it stands still with a step of nothing, while it tells the time and sleeps as `wall` does, the real clock unless given. `standingClock` is it with a step of nothing, `standingRenderClock` the render clock that clock reads as, and `onTheStandingClock(effect)` runs an effect on it, so the tests of every capability render on one clock that stands still. `checkingWithin(pool, deadlineMs)` is the pool whose checks at save may take `deadlineMs`, `checksAwaitedAtMostMs`, 30 s, unless given, for the tests that check a document with the compiler in a real worker, which a busy machine can slow past the 2 s a save allows; the refusal at that deadline is held by a pool that answers it, in `src/program-check/save-check.test.ts`. Those tests keep the 10 s a checker's worker may take to start, `mostCheckStartMs` of `@on.auto/workflow-engine`, after which a check ends `busy`: the first check of a pool starts its worker within it on purpose, since the pool's own tests prove that bound with a worker slow to start.
 
 ## Source
 
-`src/shapes` holds the output shapes, their checks and their words. `src/overview` holds the overview of a definition and its writer, the reader of a template among them, which `src/template.ts` serves, `src/overview-words` the sentences of an overview that more than one capability writes, and `src/used-by` the shape of what a definition calls, the reader of who uses a definition and of what it calls, and the overview as a read writes it. `src/index.ts` is the main entry point, `src/document.ts` the entry point of the reader of function documents, held in `src/document`, the checked worker and the words of a refused value in `src/checking`, `src/template.ts` the entry point of the template engine of the capabilities, held in `src/template`, and `src/testing/index.ts` the entry point of the test support. `src/capability` holds the declaration of a capability and the list of known capabilities. `src/registry` holds the definitions of a capability in a playbook: a definition, the events and commands of its stream, and the decider and its rules. `src/runs` holds a run: its events, commands, state, decider and rules, its size limits, and `runSettler`. `src/operations` holds the eight operations that change and read definitions and runs, `start_definition_version` among them, and how they load and record them. `src/reading` holds `list_runs` and `get_run_history`, `src/analytics` `get_playbook_analytics` and `runOutcomeMapping`, and `src/presenting` the presenters of the four stream kinds. `src/events` holds the events of a playbook: the shape of a CloudEvent, `publish_event` and the stream of a published event, and the playbook's own facts as events. `src/tool-calls` holds the journal of a run's tool calls, and `src/run-work` the work a run records after its start: the decisions on its calls, the recorder of its outbound calls, the words and accounts of its deliveries and deferral, and a run as it was recorded. `src/testing` holds what the tests share, with the capabilities of the tests in `src/testing/capabilities` and their clocks and patient checks in `src/testing/time`. `operations` depends on `capability`, `overview`, `plain-language`, `reading`, `registry`, `run-work`, `runs`, `shapes`, `tool-calls` and `used-by`, and not on `events`; `reading` on `analytics`, `cancellation`, `capability`, `plain-language`, `presenting`, `run-work`, `runs` and the fields and `get_run` of `operations`; `analytics` on `capability` and the fields of `operations`; `events` on `capability`, `registry`, `run-work`, `runs` and the command metadata of `operations`; `presenting` on `capability`, `events`, `overview`, `plain-language`, `registry`, `run-work` and `runs`; `used-by` on `events`, `overview`, `overview-words`, `registry` and the words of `capability`; `overview-words` on `overview`, `plain-language` and the words of `capability`, and on the types alone of `used-by`, an edge that type stripping erases, so nothing that runs goes from the words to `used-by`; `overview` on `capability`, `overview-words`, `plain-language`, `shapes` and `template`; `registry` on the words and the stripped forms of `capability`; and `runs` on `registry`, `shapes` and the work decisions of `run-work`.
+`src/shapes` holds the output shapes and their checks. `src/used-by` holds the shape of what a definition calls and of who uses it, and the reader of who uses a definition, and `src/schema-words` the words of a schema that a refused value is told in. `src/index.ts` is the main entry point, `src/document.ts` the entry point of the reader of function documents, held in `src/document`, the checked worker and the words of a refused value in `src/checking`, `src/template.ts` the entry point of the template engine of the capabilities, held in `src/template`, and `src/testing/index.ts` the entry point of the test support. `src/capability` holds the declaration of a capability and the list of known capabilities. `src/registry` holds the definitions of a capability in a playbook: a definition, the events and commands of its stream, and the decider and its rules. `src/runs` holds a run: its events, commands, state, decider and rules, its size limits, and `runSettler`. `src/operations` holds the eight operations that change and read definitions and runs, `start_definition_version` among them, and how they load and record them. `src/reading` holds `list_runs` and `get_run_history`, `src/analytics` `get_playbook_analytics` and `runOutcomeMapping`, and `src/presenting` the presenters of the four stream kinds. `src/events` holds the events of a playbook: the shape of a CloudEvent, `publish_event` and the stream of a published event, and the playbook's own facts as events. `src/tool-calls` holds the journal of a run's tool calls, and `src/run-work` the work a run records after its start: the decisions on its calls, the recorder of its outbound calls, the words and accounts of its deliveries and deferral, and a run as it was recorded. `src/testing` holds what the tests share, with the capabilities of the tests in `src/testing/capabilities` and their clocks and patient checks in `src/testing/time`. `operations` depends on `capability`, `plain-language`, `reading`, `registry`, `run-work`, `runs`, `shapes`, `tool-calls` and `used-by`, and not on `events`; `reading` on `analytics`, `cancellation`, `capability`, `plain-language`, `presenting`, `run-work`, `runs` and the fields and `get_run` of `operations`; `analytics` on `capability` and the fields of `operations`; `events` on `capability`, `registry`, `run-work`, `runs` and the command metadata of `operations`; `presenting` on `capability`, `events`, `plain-language`, `registry`, `run-work` and `runs`; `used-by` on `events`, `registry` and the words of `capability`; `checking` on `schema-words`, and `schema-words` on `plain-language`; `registry` on the words and the stripped forms of `capability` and the shape of a user, `user-relations.ts` of `used-by`; and `runs` on `registry`, `shapes` and the work decisions of `run-work`.
diff --git a/packages/definitions/measure/measure-loads.ts b/packages/definitions/measure/measure-loads.ts
index 0692a9335..b79e822ff 100644
--- a/packages/definitions/measure/measure-loads.ts
+++ b/packages/definitions/measure/measure-loads.ts
@@ -17,6 +17,8 @@ const samples = Number(process.env['DEFINITIONS_MEASURE_SAMPLES'] ?? 15);
 
 const mostSourceBytes = 65_536;
 
+const mostOverviewBytes = 16_384;
+
 const utf8 = new TextEncoder();
 
 const decodeFields = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Record(Schema.String, Schema.String)));
@@ -64,6 +66,13 @@ function documentOf(version: number): string {
   return `{${entries.join(',')}}`;
 }
 
+function overviewOf(version: number): string {
+  return `Version ${version} of the overview. `.padEnd(
+    mostOverviewBytes,
+    'What the person who runs the business reads. ',
+  );
+}
+
 function median(values: readonly number[]): number {
   const sorted = values.toSorted((a, b) => a - b);
   return sorted[Math.floor(sorted.length / 2)] ?? 0;
@@ -138,10 +147,21 @@ const runtime = ManagedRuntime.make(
 try {
   const { load, read } = await runtime.runPromise(
     Effect.gen(function* () {
-      yield* dispatched(createDefinition, { type: 'widest', name: 'widest', source: documentOf(1) });
+      yield* dispatched(createDefinition, {
+        type: 'widest',
+        name: 'widest',
+        source: documentOf(1),
+        overview: overviewOf(1),
+      });
       yield* Effect.forEach(
         Array.from({ length: versions - 1 }, (_, index) => index + 2),
-        (version) => dispatched(updateDefinition, { type: 'widest', name: 'widest', source: documentOf(version) }),
+        (version) =>
+          dispatched(updateDefinition, {
+            type: 'widest',
+            name: 'widest',
+            source: documentOf(version),
+            overview: overviewOf(version),
+          }),
       );
       return {
         load: yield* sampled(dispatched(listDefinitions, { type: 'widest' })),
diff --git a/packages/definitions/src/capability/capability.ts b/packages/definitions/src/capability/capability.ts
index 96819ea47..3b1c8770c 100644
--- a/packages/definitions/src/capability/capability.ts
+++ b/packages/definitions/src/capability/capability.ts
@@ -12,7 +12,6 @@ import type {
 import type { CallEnded, CallStarted } from '@on.auto/tool-servers';
 import { Effect, type Schema } from 'effect';
 
-import { composedOverview, type OverviewParts, type ToldParts } from '../overview/definition-overview.ts';
 import type { Trigger } from '../registry/definition-triggers.ts';
 import { noFacts, type CapabilityFacts } from '../run-work/declared-facts.ts';
 import type { CancelRequestKind } from '../runs/run-events.ts';
@@ -22,7 +21,6 @@ import { noStrippedForms, type StrippedForms } from './stripped-forms.ts';
 
 export interface DefinitionSummary {
   readonly description?: string;
-  readonly about?: string;
   readonly inputSchema?: Schema.JsonObject;
   readonly outputSchema?: Schema.JsonObject;
   readonly warnings?: readonly string[];
@@ -115,10 +113,6 @@ export interface AnsweringRun {
 
 export type AnswerIssues = (answer: Schema.Json, run: AnsweringRun) => readonly Issue[];
 
-export type OverviewContext = Readonly<{ summary: DefinitionSummary }>;
-
-export type OverviewOf = (parsed: Parsed, name: string, context: OverviewContext) => OverviewParts | ToldParts;
-
 const defaultLongestAnyRunMs = 600_000;
 
 export interface CapabilityDeclaration {
@@ -135,7 +129,6 @@ export interface CapabilityDeclaration {
     saved: SavedDefinitions,
   ) => Effect.Effect;
   readonly summarize: (parsed: NoInfer) => DefinitionSummary;
-  readonly overview?: OverviewOf>;
   readonly run: (
     parsed: NoInfer,
     input: Schema.Json,
@@ -162,7 +155,6 @@ export interface CapabilityDeclaration {
 
 export interface PreparedDefinition {
   readonly summary: DefinitionSummary;
-  readonly overview: (name: string) => ReturnType;
   readonly check: (saved: SavedDefinitions) => Effect.Effect;
   readonly run: (input: Schema.Json, context: RunContext) => Effect.Effect;
   readonly whenCancelled: WhenCancelled;
@@ -263,7 +255,7 @@ function declaredRuns(definition: CapabilityDeclaration, longest
 }
 
 export function defineCapability(definition: CapabilityDeclaration): Capability {
-  const { type, title, guide, noun, describeOutput, mediaType, parse, summarize, overview, run } = definition;
+  const { type, title, guide, noun, describeOutput, mediaType, parse, summarize, run } = definition;
   const bounds = declaredBounds(definition);
   const { check, whenCancelled, callsTools, answersItsShapeFor, finishesLaterFor, longestRunOf, longestRunFor } =
     declaredRuns(definition, bounds.longestAnyRunMs);
@@ -280,21 +272,17 @@ export function defineCapability(definition: CapabilityDeclaration
       parse(source).pipe(
-        Effect.map((parsed) => {
-          const summary = summarize(parsed);
-          return {
-            summary,
-            overview: (name) => composedOverview({ name, noun, summary, parts: overview?.(parsed, name, { summary }) }),
-            check: (saved) => Effect.suspend(() => check(parsed, source, saved)),
-            run: (input, context) => run(parsed, input, context, stripped),
-            whenCancelled,
-            callsTools: callsTools(parsed),
-            finishesLaterFor: (input) => finishesLaterFor(parsed, input),
-            answersItsShapeFor: (input) => answersItsShapeFor(parsed, input),
-            longestRunMs: longestRunOf(parsed),
-            longestRunMsFor: (input) => longestRunFor(parsed, input),
-          };
-        }),
+        Effect.map((parsed) => ({
+          summary: summarize(parsed),
+          check: (saved) => Effect.suspend(() => check(parsed, source, saved)),
+          run: (input, context) => run(parsed, input, context, stripped),
+          whenCancelled,
+          callsTools: callsTools(parsed),
+          finishesLaterFor: (input) => finishesLaterFor(parsed, input),
+          answersItsShapeFor: (input) => answersItsShapeFor(parsed, input),
+          longestRunMs: longestRunOf(parsed),
+          longestRunMsFor: (input) => longestRunFor(parsed, input),
+        })),
       ),
   };
 }
diff --git a/packages/definitions/src/checking/schema-refusals.test.ts b/packages/definitions/src/checking/schema-refusals.test.ts
index ed814ffcd..5cf299986 100644
--- a/packages/definitions/src/checking/schema-refusals.test.ts
+++ b/packages/definitions/src/checking/schema-refusals.test.ts
@@ -63,7 +63,7 @@ describe('the words of the rule a value broke', () => {
 });
 
 describe('the words of a value of another kind', () => {
-  it('says what a field of another kind takes, as the overview words the field', () => {
+  it('says what a field of another kind takes, in the words of its schema', () => {
     expect([
       fieldWords({ type: 'integer', minimum: 1, maximum: 100 }, 2.5),
       fieldWords({ type: 'number' }, 'many'),
diff --git a/packages/definitions/src/checking/schema-refusals.ts b/packages/definitions/src/checking/schema-refusals.ts
index f2c7ee758..8cd784428 100644
--- a/packages/definitions/src/checking/schema-refusals.ts
+++ b/packages/definitions/src/checking/schema-refusals.ts
@@ -9,9 +9,8 @@ import { articled, asSentence, InvalidInput, type Issue } from '@on.auto/operati
 import { measureOf, mostValueDepth } from '@on.auto/workflow-engine';
 import { Effect, JsonPointer, type Schema } from 'effect';
 
-import { plainTextOf } from '../overview/markdown-text.ts';
-import { trailingWords } from '../overview/schema-bounds.ts';
-import { forms } from '../overview/schema-nouns.ts';
+import { trailingWords } from '../schema-words/schema-bounds.ts';
+import { forms } from '../schema-words/schema-nouns.ts';
 import {
   isItemOf,
   listOf,
@@ -20,8 +19,8 @@ import {
   type JsonSchema,
   type PlaceStep,
   type SchemaPlace,
-} from '../overview/schema-reading.ts';
-import { descriptionOf, examplesWordsOf, expectedWordsOf, givesAnExample } from '../overview/schema-words.ts';
+} from '../schema-words/schema-reading.ts';
+import { descriptionOf, examplesWordsOf, expectedWordsOf, givesAnExample } from '../schema-words/schema-words.ts';
 
 const kindKeywords = ['type', 'enum', 'const', 'anyOf', 'oneOf', 'properties', 'items', 'prefixItems'];
 
@@ -97,7 +96,7 @@ function refusedWords(steps: readonly PlaceStep[]): string {
 
 function kindWords(schemas: readonly JsonSchema[], root: JsonSchema, detail: string): string {
   const said = schemas.find((schema) => kindKeywords.some((keyword) => schema[keyword] !== undefined));
-  return said === undefined ? asPhrase(detail) : `expected ${plainTextOf(expectedWordsOf(said, root))}`;
+  return said === undefined ? asPhrase(detail) : `expected ${expectedWordsOf(said, root)}`;
 }
 
 function holderSentence({ document, what }: CompiledSchema, tokens: readonly string[], breach: Breach): string {
diff --git a/packages/definitions/src/document.ts b/packages/definitions/src/document.ts
index c54bde657..b241cb070 100644
--- a/packages/definitions/src/document.ts
+++ b/packages/definitions/src/document.ts
@@ -6,5 +6,4 @@ export {
   type FrontMatterShape,
   type ReadFrontMatter,
 } from './document/front-matter-keys.ts';
-export { AboutSchema, DescriptionSchema, type ValueContract } from './document/function-fields.ts';
-export { writtenTextIssues, type TextPosition } from './document/written-texts.ts';
+export { DescriptionSchema, type ValueContract } from './document/function-fields.ts';
diff --git a/packages/definitions/src/document/front-matter-keys.ts b/packages/definitions/src/document/front-matter-keys.ts
index 16a7a2178..9e2ced21f 100644
--- a/packages/definitions/src/document/front-matter-keys.ts
+++ b/packages/definitions/src/document/front-matter-keys.ts
@@ -3,7 +3,6 @@ import { Predicate, Result, SchemaIssue, type Schema, type StandardSchema } from
 
 import { issueAt, type DocumentIssue, type SourceLines } from './document-issue.ts';
 import { readFrontMatter } from './front-matter-reading.ts';
-import { describingPointers, frontMatterTextIssues } from './written-texts.ts';
 
 export interface FrontMatterSection {
   readonly name: string | undefined;
@@ -75,16 +74,9 @@ export function frontMatterIn(
   firstLine: number,
   shape: FrontMatterShape,
 ): Result.Result {
-  return Result.map(
-    readFrontMatter(text, firstLine, shape.required, new Set(describingPointers)),
-    ({ value, lines, positions }) => ({
-      root: value,
-      lines,
-      issues: [
-        ...unknownKeyIssues(value, lines, shape.sections),
-        ...typeIssues(value, lines, shape),
-        ...frontMatterTextIssues({ root: value, positions }, { lines: text.split('\n'), firstLine }),
-      ],
-    }),
-  );
+  return Result.map(readFrontMatter(text, firstLine, shape.required), ({ value, lines }) => ({
+    root: value,
+    lines,
+    issues: [...unknownKeyIssues(value, lines, shape.sections), ...typeIssues(value, lines, shape)],
+  }));
 }
diff --git a/packages/definitions/src/document/front-matter-reading.ts b/packages/definitions/src/document/front-matter-reading.ts
index e354ff975..dd31ac341 100644
--- a/packages/definitions/src/document/front-matter-reading.ts
+++ b/packages/definitions/src/document/front-matter-reading.ts
@@ -13,12 +13,10 @@ import {
 } from 'yaml';
 
 import type { DocumentIssue, SourceLines } from './document-issue.ts';
-import type { TextPosition } from './written-texts.ts';
 
 export interface FrontMatterReading {
   readonly value: Schema.JsonObject;
   readonly lines: SourceLines;
-  readonly positions: ReadonlyMap;
 }
 
 const frontMatterNesting = 72;
@@ -36,7 +34,6 @@ interface Place {
 interface Reading {
   readonly report: (line: number, pointer: string, detail: string) => void;
   readonly locate: (pointer: string, line: number) => void;
-  readonly position: (pointer: string, node: unknown) => void;
   readonly lineOf: (node: unknown, fallback: number) => number;
 }
 
@@ -97,9 +94,6 @@ function valueOf(node: unknown, place: Place, reading: Reading): Schema.Json {
     reading.report(line, place.pointer, 'Expected text, a finite number, true, false or null');
     return null;
   }
-  if (typeof value === 'string') {
-    reading.position(place.pointer, node);
-  }
   return value;
 }
 
@@ -146,7 +140,6 @@ export function readFrontMatter(
   text: string,
   firstLine: number,
   required: string,
-  positioned: ReadonlySet,
 ): Result.Result {
   const lineCounter = new LineCounter();
   const document = parseDocument(text, { ...parseOptions, lineCounter });
@@ -167,7 +160,6 @@ export function readFrontMatter(
   }
   const issues: DocumentIssue[] = [];
   const lines = new Map([['', firstLine]]);
-  const positions = new Map();
   const value = objectOf(
     () => contents,
     { pointer: '', depth: 0, line: firstLine },
@@ -178,17 +170,11 @@ export function readFrontMatter(
       locate: (pointer, line) => {
         lines.set(pointer, line);
       },
-      position: (pointer, node) => {
-        const offset = offsetOf(node);
-        if (offset !== undefined && positioned.has(pointer)) {
-          positions.set(pointer, { line: lineAt(offset), column: lineCounter.linePos(offset).col });
-        }
-      },
       lineOf: (node, fallback) => {
         const offset = offsetOf(node);
         return offset === undefined ? fallback : lineAt(offset);
       },
     },
   );
-  return issues.length > 0 ? Result.fail(issues) : Result.succeed({ value, lines, positions });
+  return issues.length > 0 ? Result.fail(issues) : Result.succeed({ value, lines });
 }
diff --git a/packages/definitions/src/document/front-matter.test.ts b/packages/definitions/src/document/front-matter.test.ts
index 7c66e48e3..a9921c8d2 100644
--- a/packages/definitions/src/document/front-matter.test.ts
+++ b/packages/definitions/src/document/front-matter.test.ts
@@ -4,7 +4,6 @@ import { describe, expect, it } from 'vitest';
 import { issueText, reportedIssues } from './document-issue.ts';
 import { splitDocument } from './document-split.ts';
 import { frontMatterIn, type FrontMatterShape, type ReadFrontMatter } from './front-matter-keys.ts';
-import { AboutSchema, DescriptionSchema } from './function-fields.ts';
 
 const NoteSchema = Schema.Struct({
   title: Schema.String,
@@ -27,37 +26,22 @@ const note: FrontMatterShape = {
   required: 'the title',
 };
 
-const DescribedSchema = Schema.Struct({
-  description: Schema.optionalKey(DescriptionSchema),
-  about: Schema.optionalKey(AboutSchema),
-});
-
-const described: FrontMatterShape = {
-  sections: [{ name: undefined, keys: Object.keys(DescribedSchema.fields) }],
-  decode: Schema.decodeUnknownResult(DescribedSchema, { errors: 'all' }),
-  required: 'what it is for',
-};
-
-function read(source: string, shape = note): Result.Result {
+function read(source: string): Result.Result {
   return Result.mapError(
     Result.flatMap(splitDocument(source, 'A note'), ({ frontMatter, frontMatterLine }) =>
-      frontMatterIn(frontMatter, frontMatterLine, shape),
+      frontMatterIn(frontMatter, frontMatterLine, note),
     ),
     (issues) => reportedIssues(issues).map((issue) => issueText(issue)),
   );
 }
 
-function issuesIn(source: string, shape = note): readonly string[] {
-  return Result.match(read(source, shape), {
+function issuesIn(source: string): readonly string[] {
+  return Result.match(read(source), {
     onSuccess: ({ issues }) => reportedIssues(issues).map((issue) => issueText(issue)),
     onFailure: (issues) => issues,
   });
 }
 
-function headingRefusalAt(line: number, key: string, where = 'this line'): string {
-  return `Line ${line}, /${key}: ${key} takes no heading, since an overview writes its own: ${where} makes one, with # before its words or as a line of = or - under the words above it`;
-}
-
 function rootOf(source: string): Schema.JsonObject {
   return Result.getOrThrow(read(source)).root;
 }
@@ -169,43 +153,3 @@ describe('the keys of the front matter', () => {
     expect(issuesIn(documentOf(`title: A\n${many}`)).at(-1)).toBe('Line 23: 3 more issues are not shown');
   });
 });
-
-describe('the description and the about of the front matter', () => {
-  it('take no heading, each refused at its own line in a literal block', () => {
-    const source = documentOf(
-      ['description: |', '  Reviews a brief.', '  ## When', 'about: |-', '', '  Who it serves', '  ---'].join('\n'),
-    );
-
-    expect(issuesIn(source, described)).toEqual([headingRefusalAt(4, 'description'), headingRefusalAt(8, 'about')]);
-  });
-
-  it('take no heading in any other style, refused at the line their value opens, naming the line by its first words', () => {
-    expect(
-      issuesIn(documentOf("description: '# Reviews a brief'\nabout: >\n  Who it serves.\n\n  # When"), described),
-    ).toEqual([
-      headingRefusalAt(2, 'description', 'the line that begins “# Reviews a brief”'),
-      headingRefusalAt(3, 'about', 'the line that begins “# When”'),
-    ]);
-    expect(issuesIn(documentOf('title: "# A note"\nextra:\n  anything: "# kept"'))).toEqual([]);
-  });
-
-  it('leave no block open, and hold no line ## Details, each refused at its line', () => {
-    const fence = '`'.repeat(3);
-
-    expect(
-      issuesIn(documentOf(['about: |', '  Who it serves.', `  ${fence}`, '  ## Details'].join('\n')), described),
-    ).toEqual([
-      'Line 4, /about: about leaves open the fence that this line opens, so the rest of the overview would be read inside it: close it',
-      "Line 5, /about: about may not hold the line ## Details, which is the overview's own, since the page that shows an overview splits it at that text: this line is one",
-    ]);
-  });
-
-  it('take Markdown with paragraphs, lists and emphasis', () => {
-    const about = ['about: |', '  It is for the *team*.', '', '  - Who it serves', '  - When it runs'].join('\n');
-
-    expect(Result.getOrThrow(read(documentOf(about), described))).toMatchObject({
-      root: { about: 'It is for the *team*.\n\n- Who it serves\n- When it runs\n' },
-      issues: [],
-    });
-  });
-});
diff --git a/packages/definitions/src/document/function-fields.test.ts b/packages/definitions/src/document/function-fields.test.ts
index bd970c5df..6691385ed 100644
--- a/packages/definitions/src/document/function-fields.test.ts
+++ b/packages/definitions/src/document/function-fields.test.ts
@@ -1,12 +1,10 @@
 import { Schema } from 'effect';
 import { describe, expect, it } from 'vitest';
 
-import { AboutSchema, DescriptionSchema } from '../document.ts';
+import { DescriptionSchema } from '../document.ts';
 
 const holds = Schema.is(DescriptionSchema);
 
-const holdsAbout = Schema.is(AboutSchema);
-
 describe('the description of a function document', () => {
   it('takes 1 to 1,000 characters', () => {
     expect([holds(''), holds('x'), holds('x'.repeat(1000)), holds('x'.repeat(1001))]).toEqual([
@@ -17,15 +15,3 @@ describe('the description of a function document', () => {
     ]);
   });
 });
-
-describe('the about of a function document', () => {
-  it('takes 1 to 4,000 characters, counted as UTF-16 units', () => {
-    expect([holdsAbout(''), holdsAbout('x'), holdsAbout('x'.repeat(4000)), holdsAbout('x'.repeat(4001))]).toEqual([
-      false,
-      true,
-      true,
-      false,
-    ]);
-    expect([holdsAbout('😀'.repeat(2000)), holdsAbout(`${'😀'.repeat(2000)}x`)]).toEqual([true, false]);
-  });
-});
diff --git a/packages/definitions/src/document/function-fields.ts b/packages/definitions/src/document/function-fields.ts
index 95a8d43ea..e07371af2 100644
--- a/packages/definitions/src/document/function-fields.ts
+++ b/packages/definitions/src/document/function-fields.ts
@@ -3,12 +3,8 @@ import { Schema } from 'effect';
 
 const descriptionLength = 1000;
 
-const aboutLength = 4000;
-
 export const DescriptionSchema = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(descriptionLength));
 
-export const AboutSchema = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(aboutLength));
-
 export interface ValueContract {
   readonly schema?: CompiledSchema;
 }
diff --git a/packages/definitions/src/document/written-texts.test.ts b/packages/definitions/src/document/written-texts.test.ts
deleted file mode 100644
index 98d0dd036..000000000
--- a/packages/definitions/src/document/written-texts.test.ts
+++ /dev/null
@@ -1,192 +0,0 @@
-import { describe, expect, it } from 'vitest';
-
-import { writtenTextIssues } from '../document.ts';
-
-type Finding = readonly [number, 'heading' | 'details' | 'fence' | 'html'];
-
-const literalHeader = { line: 1, column: 8 };
-
-function kindOf(detail: string): Finding[1] {
-  if (detail.includes('takes no heading')) {
-    return 'heading';
-  }
-  if (detail.includes('leaves open the fence')) {
-    return 'fence';
-  }
-  return detail.includes('leaves open the HTML block') ? 'html' : 'details';
-}
-
-function findingsOf(...lines: readonly string[]): readonly Finding[] {
-  const source = ['about: |', ...lines.map((line) => `  ${line}`)];
-  return writtenTextIssues(lines.join('\n'), 'about', { lines: source, firstLine: 1 }, literalHeader).map(
-    ({ line, detail }): Finding => [line - 2, kindOf(detail)],
-  );
-}
-
-interface Placed {
-  readonly line: number;
-  readonly column: number;
-  readonly detail: string;
-}
-
-function placesOf(
-  issues: readonly Placed[],
-): readonly { readonly line: number; readonly column: number; readonly where: string | undefined }[] {
-  return issues.map(({ line, column, detail }) => ({
-    line,
-    column,
-    where: /: (?the line that begins “.+”) makes one/u.exec(detail)?.groups?.['where'],
-  }));
-}
-
-function aboutIssuesAt(line: number, text: string, lines: readonly string[]): readonly Placed[] {
-  return writtenTextIssues(text, 'about', { lines, firstLine: line }, { line, column: 8 });
-}
-
-function headingsOf(...lines: readonly string[]): readonly number[] {
-  return findingsOf(...lines)
-    .filter(([, kind]) => kind === 'heading')
-    .map(([index]) => index);
-}
-
-describe('a refusal of a text of a document', () => {
-  it('is at the line of its own in a literal block, and at the column its text starts at, in words that name its key', () => {
-    const source = ['---', 'description: |', '    Who it serves.', '', '    ## When it runs'];
-
-    expect(
-      writtenTextIssues(
-        'Who it serves.\n\n## When it runs\n',
-        'description',
-        { lines: source, firstLine: 1 },
-        { line: 2, column: 14 },
-      ),
-    ).toEqual([
-      {
-        line: 5,
-        column: 5,
-        detail:
-          'description takes no heading, since an overview writes its own: this line makes one, with # before its words or as a line of = or - under the words above it',
-      },
-    ]);
-  });
-
-  it('is at the line and the column its value opens at in any other style, naming the line by its first words', () => {
-    const folded = ['about: >', '  Intro.', '', '  ## Heading of the team'];
-    const quoted = ['about: "Intro.\\n  ## Heading\\tof the brief"'];
-
-    expect([
-      placesOf(aboutIssuesAt(3, 'Intro.\n## Heading of the team\n', folded)),
-      placesOf(aboutIssuesAt(7, 'Intro.\n  ## Heading\tof the brief', quoted)),
-    ]).toEqual([
-      [{ line: 3, column: 8, where: 'the line that begins “## Heading of the”' }],
-      [{ line: 7, column: 8, where: 'the line that begins “## Heading of the”' }],
-    ]);
-  });
-
-  it('reads a value on a line its source does not hold as one in no literal block', () => {
-    expect(
-      placesOf(writtenTextIssues('# Heading', 'about', { lines: [], firstLine: 1 }, { line: 4, column: 3 })),
-    ).toEqual([{ line: 4, column: 3, where: 'the line that begins “# Heading”' }]);
-  });
-});
-
-describe('a heading in a text of a document', () => {
-  it('is a line of one to six # and a space, a tab or nothing, or a line of = or - under a line of a paragraph', () => {
-    expect(headingsOf('# One', 'Text', '###### Six', '#\tTabbed', '#', 'Two', '===', 'Three', '---')).toEqual([
-      0, 2, 3, 4, 6, 8,
-    ]);
-  });
-
-  it('is indented at most three spaces after a line of a paragraph, as a heading line or as an underline', () => {
-    expect([
-      headingsOf('Text', '   # Three'),
-      headingsOf('Text', '    # Four'),
-      headingsOf('Text', '\t# Tabbed'),
-      headingsOf('Text', '   ==='),
-      headingsOf('Text', '    ==='),
-    ]).toEqual([[1], [], [], [1], []]);
-  });
-
-  it('is none of a tag, seven #, an escaped #, a thematic break or a line of = after a blank line', () => {
-    expect(findingsOf('#tag', '####### Seven', '\\# Escaped', '', '---', '', '===', '***', 'Text', '- - -')).toEqual(
-      [],
-    );
-  });
-});
-
-describe('a heading in a container of a text of a document', () => {
-  it('is in a quote, in a list item and in a list item of a quote, after the markers of each', () => {
-    expect([
-      headingsOf('> # Words'),
-      headingsOf('- # Words'),
-      headingsOf('1. ## Words'),
-      headingsOf('> - # Words'),
-    ]).toEqual([[0], [0], [0], [0]]);
-  });
-
-  it('is an underline in a quote under a line of the same quote, and one indented to the content of an item under it', () => {
-    expect([
-      headingsOf('> Title', '> ==='),
-      headingsOf('- item', '  ---'),
-      headingsOf('- a', '  - b', '    ==='),
-    ]).toEqual([[1], [1], [2]]);
-  });
-
-  it('is no underline under a line that opens or lazily goes on with an item or a quote', () => {
-    expect([
-      findingsOf('- item', '---'),
-      findingsOf('> quote', '==='),
-      findingsOf('- item', 'goes on', '==='),
-      findingsOf('> - item', '> ---'),
-      findingsOf('> > deep', '> goes on', '> ==='),
-    ]).toEqual([[], [], [], [], []]);
-  });
-
-  it('is in an item, indented to its content with spaces or a tab, and not in indented code of the item', () => {
-    expect([
-      headingsOf('- item', '', '    # In it'),
-      headingsOf('- a', '  - b', '', '      # In b'),
-      headingsOf('- item', '\t# In it'),
-      headingsOf('-\t# Words'),
-      headingsOf('-     # Code'),
-      headingsOf('-', '  # In the item'),
-      headingsOf('- item', '>', '    # Code'),
-    ]).toEqual([[2], [3], [1], [0], [], [1], []]);
-  });
-
-  it('is an underline under a paragraph that an empty item or an ordered item not from 1 cannot interrupt', () => {
-    expect([headingsOf('Text', '-'), headingsOf('Text', '2. two', '    ===')]).toEqual([[1], []]);
-  });
-});
-
-describe('a heading in a block of a text of a document', () => {
-  it('is none of a line in a fence, in indented code, in an HTML block or under a row of a table', () => {
-    expect(
-      findingsOf(
-        '```',
-        '# A comment of a shell',
-        '```',
-        '',
-        '    # Code',
-        '
', - '# Inside', - '
', - '', - '| a | b |', - '| - | - |', - '| 1 | 2 |', - '---', - ), - ).toEqual([]); - }); - - it('is a line after an HTML block that ends on its line, and after a tag that interrupts a paragraph, and in no block of a tag alone on its line', () => { - expect([ - headingsOf('', '# After it'), - headingsOf('
', '# Inside', '
', '# After it'), - headingsOf('', '# Inside', '', '# After it'), - headingsOf('Text', '', '# After it'), - headingsOf('', '# Inside'), - ]).toEqual([[1], [3], [3], [2], []]); - }); -}); diff --git a/packages/definitions/src/document/written-texts.ts b/packages/definitions/src/document/written-texts.ts deleted file mode 100644 index 23899b492..000000000 --- a/packages/definitions/src/document/written-texts.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { quoted } from '@on.auto/operations'; -import type { Schema } from 'effect'; - -import { - detailsHeading, - detailsLinesIn, - headingLinesIn, - unclosedBlockIn, - type UnclosedBlock, -} from '../overview/markdown-text.ts'; -import type { DocumentIssue } from './document-issue.ts'; - -interface TextSource { - readonly lines: readonly string[]; - readonly firstLine: number; -} - -export interface TextPosition { - readonly line: number; - readonly column: number; -} - -interface TextIssue extends TextPosition { - readonly detail: string; -} - -interface Finding { - readonly index: number; - readonly opening: string; - readonly detail: (where: string) => string; -} - -export const describingPointers: readonly string[] = ['/description', '/about']; - -const literalBlock = /^\|/u; - -const blanks = /[ \t]+/u; - -const mostOpeningWords = 4; - -function openingWordsOf(line: string): string { - return line - .split(blanks) - .filter((word) => word !== '') - .slice(0, mostOpeningWords) - .join(' '); -} - -function findingsAt( - lines: readonly string[], - indexes: readonly number[], - detail: (where: string) => string, -): readonly Finding[] { - return lines.flatMap((line, index) => - indexes.includes(index) ? [{ index, opening: openingWordsOf(line), detail }] : [], - ); -} - -function detailsWords(key: string, where: string): string { - return `${key} may not hold the line ${detailsHeading}, which is the overview's own, since the page that shows an overview splits it at that text: ${where} is one`; -} - -function headingWords(key: string, where: string): string { - return `${key} takes no heading, since an overview writes its own: ${where} makes one, with # before its words or as a line of = or - under the words above it`; -} - -function unclosedWords(key: string, unclosed: UnclosedBlock | undefined, where: string): string { - const block = unclosed?.kind === 'fence' ? 'fence' : 'HTML block'; - return `${key} leaves open the ${block} that ${where} opens, so the rest of the overview would be read inside it: close it`; -} - -function findingsIn(key: string, text: string): readonly Finding[] { - const lines = text.split('\n'); - const details = detailsLinesIn(text); - const headings = headingLinesIn(text).filter((index) => !details.includes(index)); - const unclosed = unclosedBlockIn(text); - return [ - ...findingsAt(lines, details, (where) => detailsWords(key, where)), - ...findingsAt(lines, headings, (where) => headingWords(key, where)), - ...findingsAt(lines, unclosed === undefined ? [] : [unclosed.line], (where) => unclosedWords(key, unclosed, where)), - ]; -} - -function lineAt({ lines, firstLine }: TextSource, line: number): string { - return lines[line - firstLine] ?? ''; -} - -function columnOfText(line: string): number { - return line.length - line.trimStart().length + 1; -} - -function placed(findings: readonly Finding[], source: TextSource, opens: TextPosition): readonly TextIssue[] { - const literal = literalBlock.test(lineAt(source, opens.line).slice(opens.column - 1)); - return findings - .toSorted((one, other) => one.index - other.index) - .map(({ index, opening, detail }) => { - const line = opens.line + 1 + index; - return literal - ? { line, column: columnOfText(lineAt(source, line)), detail: detail('this line') } - : { line: opens.line, column: opens.column, detail: detail(`the line that begins ${quoted(opening)}`) }; - }); -} - -export function writtenTextIssues( - text: string, - key: string, - source: TextSource, - opens: TextPosition, -): readonly TextIssue[] { - return placed(findingsIn(key, text), source, opens); -} - -export function frontMatterTextIssues( - { root, positions }: { readonly root: Schema.JsonObject; readonly positions: ReadonlyMap }, - source: TextSource, -): readonly DocumentIssue[] { - return describingPointers.flatMap((pointer) => { - const key = pointer.slice(1); - const text = root[key]; - const opens = positions.get(pointer); - return typeof text === 'string' && opens !== undefined - ? writtenTextIssues(text, key, source, opens).map(({ line, detail }) => ({ line, pointer, detail })) - : []; - }); -} diff --git a/packages/definitions/src/events/playbook-facts.test.ts b/packages/definitions/src/events/playbook-facts.test.ts index 257d70fbd..1ee9f3419 100644 --- a/packages/definitions/src/events/playbook-facts.test.ts +++ b/packages/definitions/src/events/playbook-facts.test.ts @@ -11,7 +11,6 @@ import { type RunEvent, } from '../index.ts'; import { runStreamNameOf } from '../runs/run-decider.ts'; -import { overviewTitled } from '../testing/overview-sections.ts'; import { testRunId as runId } from '../testing/run-facts.ts'; const recordId = '0b1c2d3e-4f50-5a6b-8c7d-8e9fa0b1c2d3'; @@ -127,7 +126,6 @@ describe('the records of a run that are no facts', () => { const content = { source: '---\nmodel: anthropic/claude-sonnet-4-5\n---\nSummarize.', - overview: overviewTitled('summary'), }; describe('the facts of a definition as events', () => { diff --git a/packages/definitions/src/index.ts b/packages/definitions/src/index.ts index 1b2be9b56..b9e09d3e9 100644 --- a/packages/definitions/src/index.ts +++ b/packages/definitions/src/index.ts @@ -11,8 +11,6 @@ export { defineCapability, type AnswerIssues, type AnsweringRun, - type OverviewContext, - type OverviewOf, defaultRunWords, type CancelDecision, type CancelledRun, @@ -176,7 +174,6 @@ export { } from './registry/definition-triggers.ts'; export { runStartedOf } from './runs/run-starts.ts'; export { inWords, nameInWords, outputInWords } from './plain-language/in-words.ts'; -export { charactersIn } from './plain-language/characters.ts'; export { triggerNamed } from './plain-language/event-words.ts'; export { mostInputBytes, mostInputDepth, mostResultBytes } from './runs/recorded-size.ts'; export { @@ -207,86 +204,11 @@ export { } from './capability/function-terminology.ts'; export { checkedInput, refusedByTheInputSchema, refusedInput, wordedIssuesOf } from './checking/schema-refusals.ts'; export { checkedWorker } from './checking/worker-url.ts'; -export { - bulletList, - definitionPhrase, - listedPhrases, - mostTableRows, - named, - numberedList, - paragraph, - sectionOf, - type BulletList, - type Diagram, - type DiagramEdge, - type DiagramGroup, - type DiagramNode, - type FieldRow, - type FieldTable, - type NodeShape, - type NumberedList, - type Overview, - type OverviewBlock, - type OverviewSection, - type Paragraph, - type Phrase, - type PhrasedDefinition, - type Phrases, - type Quote, -} from './overview/overview.ts'; -export { durationInWords } from './overview/duration-words.ts'; -export { eventFilterPhrases, type EventFilter } from './overview/event-filter-words.ts'; -export { markdownOf, mostOverviewBytes } from './overview/markdown-writer.ts'; -export { describedSchemaBlocks, mostFieldDescriptionCharacters, schemaBlocks } from './overview/schema-words.ts'; -export { plainTextOf } from './overview/markdown-text.ts'; -export { isJsonSchema } from './overview/schema-reading.ts'; -export { callSection, callSentence } from './overview-words/call-words.ts'; -export { computedSentence, programSection } from './overview-words/computation-words.ts'; -export { valueMeaning } from './overview-words/field-meanings.ts'; -export { - answerSection, - answerSentence, - answerWaitSentence, - inboxSentence, - messageLead, - notTakenSentence, - notificationSentence, - partyNamedUnderDetails, - replyRuleSentence, - replyWaitSentence, - unansweredSentence, - whereSection, - type ReplyParts, -} from './overview-words/request-words.ts'; -export { replyDiagram } from './overview-words/reply-diagram-words.ts'; -export { shownUnderSource } from './overview-words/source-words.ts'; -export { templateLead } from './overview-words/template-words.ts'; -export { theToolOf } from './overview-words/tool-words.ts'; -export { undescribedSentence } from './overview-words/value-words.ts'; export type { OutputShape } from './shapes/output-shape.ts'; export { OutputShapeNameSchema, type OutputShapeName } from './shapes/shape-names.ts'; export { outputShapeNamed, outputShapeNames, shapeMismatchOf } from './shapes/output-shapes.ts'; export { declaredShape, type ShapeRefusal } from './shapes/shape-declarations.ts'; export { publishedOutputOf } from './shapes/published-output.ts'; -export { seriesBounds, seriesMarksSchema, seriesShape } from './shapes/series-shape.ts'; -export { periodNouns, seriesPathBlocks } from './shapes/shape-words.ts'; +export { seriesMarksSchema, seriesShape } from './shapes/series-shape.ts'; export { rangeHolds } from './shapes/shape-values.ts'; -export { - composedOverview, - inputDetails, - inputSection, - outputDetails, - outputSection, - type OverviewParts, - type ToldParts, -} from './overview/definition-overview.ts'; -export { overviewAtRead, type ReadKnowledge } from './used-by/overview-at-read.ts'; -export type { DefinitionCalled, DefinitionUser } from './used-by/user-relations.ts'; -export { - CalledDefinitionSchema, - type CalledDefinition, - type CallingStep, - type CallRole, - type NamedDefinition, - type UserRelation, -} from './used-by/called-definitions.ts'; +export type { CalledDefinition, CallingStep, CallRole } from './used-by/called-definitions.ts'; diff --git a/packages/definitions/src/markdown/html-blocks.ts b/packages/definitions/src/markdown/html-blocks.ts deleted file mode 100644 index 8ad8713a5..000000000 --- a/packages/definitions/src/markdown/html-blocks.ts +++ /dev/null @@ -1,56 +0,0 @@ -export type HtmlBlock = 'raw' | 'comment' | 'instruction' | 'declaration' | 'data' | 'element' | 'tag'; - -const blankLine = /^[ \t]*$/u; - -const blockTags = [ - ['address', 'article', 'aside', 'base', 'basefont', 'blockquote', 'body', 'caption', 'center', 'col'], - ['colgroup', 'dd', 'details', 'dialog', 'dir', 'div', 'dl', 'dt', 'fieldset', 'figcaption', 'figure'], - ['footer', 'form', 'frame', 'frameset', 'h[1-6]', 'head', 'header', 'hr', 'html', 'iframe', 'legend'], - ['li', 'link', 'main', 'menu', 'menuitem', 'nav', 'noframes', 'ol', 'optgroup', 'option', 'p', 'param'], - ['search', 'section', 'summary', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'title', 'tr', 'track', 'ul'], -].flat(); - -const tagName = '[A-Za-z][A-Za-z0-9-]*'; - -const attribute = `[ \\t]+[A-Za-z_:][A-Za-z0-9_.:-]*(?:[ \\t]*=[ \\t]*(?:[^ \\t"'=<>\`]+|'[^']*'|"[^"]*"))?`; - -const htmlBlocks: readonly HtmlBlock[] = ['raw', 'comment', 'instruction', 'declaration', 'data', 'element', 'tag']; - -const htmlOpeners: Readonly> = { - raw: /^<(?:script|pre|style|textarea)(?:[ \t>]|$)/iu, - comment: /^', - instruction: '?>', - declaration: '>', - data: ']]>', - element: '', - tag: '', -}; - -const endOfTheTagName = /[ \t>]|$/u; - -export const htmlEnds: Readonly> = { - raw: /<\/(?:script|pre|style|textarea)>/iu, - comment: /--!?>/u, - instruction: /\?>/u, - declaration: />/u, - data: /\]\]>/u, - element: blankLine, - tag: blankLine, -}; - -export function htmlOpenedBy(content: string, interrupting: boolean): HtmlBlock | undefined { - return htmlBlocks.find((html) => htmlOpeners[html].test(content) && !(html === 'tag' && interrupting)); -} - -export function htmlCloserOf(html: HtmlBlock, opener: string): string { - return html === 'raw' ? `` : htmlClosers[html]; -} diff --git a/packages/definitions/src/markdown/markdown-lines.ts b/packages/definitions/src/markdown/markdown-lines.ts deleted file mode 100644 index bcb732735..000000000 --- a/packages/definitions/src/markdown/markdown-lines.ts +++ /dev/null @@ -1,298 +0,0 @@ -import { htmlCloserOf, htmlEnds, htmlOpenedBy, type HtmlBlock } from './html-blocks.ts'; -import { tableLinesOf } from './table-rows.ts'; - -export interface OpenBlock { - readonly kind: 'fence' | HtmlBlock; - readonly closer: string; - readonly line: number; - readonly within: number | undefined; -} - -type Form = 'atx' | 'underline' | 'break'; - -interface ReadLine { - readonly prose: boolean; - readonly text: boolean; - readonly form: Form | undefined; - readonly separates: boolean; - readonly container: string; - readonly quoted: number; -} - -interface Reading { - readonly open: OpenBlock | undefined; - readonly codeMayStart: boolean; - readonly inCode: boolean; - readonly items: readonly number[]; - readonly startedEmpty: boolean; - readonly last: ReadLine | undefined; -} - -interface Stripped { - readonly quoted: number; - readonly opens: readonly number[]; - readonly at: number; - readonly rest: string; - readonly container: string; -} - -interface Place { - readonly quoted: number; - readonly kept: readonly number[]; - readonly opens: readonly number[]; - readonly container: string; - readonly indent: number; - readonly content: string; -} - -interface Step { - readonly reading: Reading; - readonly read: ReadLine; -} - -interface BlockLine { - readonly block: boolean; - readonly open: OpenBlock | undefined; -} - -export interface MarkdownLines { - readonly reads: readonly ReadLine[]; - readonly open: OpenBlock | undefined; -} - -const blankLine = /^[ \t]*$/u; - -const leadingBlanks = /^[ \t]+/u; - -const fenceOpener = /^(?:`{3,}(?=[^`]*$)|~{3,})/u; - -const listMarker = /^(?:[-+*]|\d{1,9}[.)])(?=[ \t]|$)/u; - -const interruptingItem = /^(?:[-+*]|0*1[.)])[ \t]+[^ \t]/u; - -const quoteMarker = /^ {0,3}> ?/u; - -const atxHeading = /^#{1,6}(?:[ \t]|$)/u; - -const thematicBreak = /^(?:(?:\*[ \t]*){3,}|(?:-[ \t]*){3,}|(?:_[ \t]*){3,})$/u; - -const setextUnderline = /^(?:=+|-+)[ \t]*$/u; - -const atRest: Reading = { - open: undefined, - codeMayStart: true, - inCode: false, - items: [], - startedEmpty: false, - last: undefined, -}; - -function blanksOf(line: string): string { - return line.slice(0, line.length - line.replace(leadingBlanks, '').length); -} - -function withoutBlanks(line: string): string { - return line.slice(blanksOf(line).length); -} - -function columnsOf(blanks: string, from = 0): number { - return blanks - .split('') - .reduce((column, character) => (character === '\t' ? column + 4 - (column % 4) : column + 1), from); -} - -function fromColumn(line: string, column: number): string { - let reached = 0; - let index = 0; - while (reached < column && (line[index] === ' ' || line[index] === '\t')) { - reached = columnsOf(line.charAt(index), reached); - index += 1; - } - return line.slice(index); -} - -function opensAnItem(rest: string, interrupting: boolean): boolean { - const content = withoutBlanks(rest); - const item = listMarker.test(content) && !thematicBreak.test(content); - return columnsOf(blanksOf(rest)) <= 3 && item && (!interrupting || interruptingItem.test(content)); -} - -function itemOf(rest: string, at: number): Pick { - const marker = rest.length - rest.replace(listMarker, '').length; - const spaces = blanksOf(rest.slice(marker)); - const width = columnsOf(spaces, at + marker) - at - marker; - const taken = rest.length === marker + spaces.length || width > 4 ? 1 : width; - return { at: at + marker + taken, rest: rest.slice(marker + Math.min(spaces.length, taken)) }; -} - -function strippedOnce(stripped: Stripped, paragraph: string | undefined): Stripped | undefined { - const { rest, at, quoted } = stripped; - const quote = quoteMarker.exec(rest)?.[0]; - if (quote !== undefined) { - return { ...stripped, quoted: quoted + 1, rest: rest.slice(quote.length), container: `${stripped.container}>` }; - } - if (!opensAnItem(rest, paragraph === stripped.container)) { - return undefined; - } - const item = itemOf(withoutBlanks(rest), columnsOf(blanksOf(rest), at)); - const opens = quoted === 0 ? [...stripped.opens, item.at] : stripped.opens; - return { ...stripped, ...item, opens, container: `${stripped.container}-${item.at}` }; -} - -function placeOf(line: string, items: readonly number[], paragraph: string | undefined): Place { - const kept = items.filter((column) => column <= columnsOf(blanksOf(line))); - const at = kept.at(-1) ?? 0; - const container = kept.map((column) => `-${column}`).join(''); - let stripped: Stripped = { quoted: 0, opens: [], at, rest: fromColumn(line, at), container }; - for (let next = strippedOnce(stripped, paragraph); next !== undefined; next = strippedOnce(next, paragraph)) { - stripped = next; - } - const { quoted, opens, rest } = stripped; - const content = withoutBlanks(rest); - return { quoted, kept, opens, container: stripped.container, indent: columnsOf(blanksOf(rest)), content }; -} - -function opensABlock(content: string): boolean { - const opensAContainer = listMarker.test(content) || quoteMarker.test(content); - const standsAlone = atxHeading.test(content) || thematicBreak.test(content) || fenceOpener.test(content); - return opensAContainer || standsAlone || htmlOpenedBy(content, true) !== undefined; -} - -export function standsApart(line: string): boolean { - const content = withoutBlanks(line); - return opensABlock(content) || setextUnderline.test(content); -} - -function isParagraphText(place: Place): boolean { - return place.opens.length === 0 && (place.indent > 3 || !opensABlock(place.content)); -} - -function lazilyAfter(last: ReadLine | undefined, place: Place, items: readonly number[]): ReadLine | undefined { - if (last?.prose !== true || place.quoted > last.quoted || !isParagraphText(place)) { - return undefined; - } - return place.quoted < last.quoted || place.kept.length < items.length ? last : undefined; -} - -function lineOf({ quoted }: Pick, container: string, kind: Form | 'prose' | 'other'): ReadLine { - const form = kind === 'prose' || kind === 'other' ? undefined : kind; - return { prose: kind === 'prose', text: kind === 'prose', form, separates: false, container, quoted }; -} - -function htmlLineOf(content: string, interrupting: boolean, at: Omit): BlockLine { - const html = htmlOpenedBy(content, interrupting); - if (html === undefined) { - return { block: false, open: undefined }; - } - const open = { kind: html, closer: htmlCloserOf(html, content), ...at }; - return { block: true, open: htmlEnds[html].test(content) ? undefined : open }; -} - -function blockLineOf(place: Place, interrupting: boolean, at: Omit): BlockLine { - if (place.indent > 3 || place.quoted > 0) { - return { block: false, open: undefined }; - } - const closer = fenceOpener.exec(place.content)?.[0]; - return closer === undefined - ? htmlLineOf(place.content, interrupting, at) - : { block: true, open: { kind: 'fence', closer, ...at } }; -} - -function stepOf(reading: Reading, read: ReadLine, changes: Partial): Step { - return { reading: { ...reading, startedEmpty: false, ...changes, last: read }, read }; -} - -function formOf({ content }: Place, underlines: boolean): Form | undefined { - if (atxHeading.test(content)) { - return 'atx'; - } - if (underlines && setextUnderline.test(content)) { - return 'underline'; - } - return thematicBreak.test(content) ? 'break' : undefined; -} - -function kindOf(place: Place, block: boolean, underlines: boolean): Form | 'prose' | 'other' { - if (block) { - return 'other'; - } - return (place.indent <= 3 ? formOf(place, underlines) : undefined) ?? 'prose'; -} - -function isUnderAParagraphOf(last: ReadLine | undefined, container: string): boolean { - return last?.prose === true && last.container === container; -} - -function containedStep(reading: Reading, place: Place, line: number): Step { - const items = [...place.kept, ...place.opens]; - const within = items.at(-1); - const { container } = place; - const codeMayStart = reading.codeMayStart || reading.inCode || place.opens.length > 0; - if (codeMayStart && place.indent >= 4 && place.quoted === 0) { - return stepOf(reading, lineOf(place, container, 'other'), { codeMayStart: true, inCode: true, items }); - } - const { block, open } = blockLineOf(place, reading.last?.text === true, { line, within }); - const kind = kindOf(place, block, isUnderAParagraphOf(reading.last, container)); - const changes = { open, codeMayStart: kind !== 'prose', inCode: false, items }; - return stepOf(reading, lineOf(place, container, kind), changes); -} - -function blankStep(reading: Reading, place: Place): Step { - const startedEmpty = place.opens.length > 0; - const going = reading.startedEmpty ? reading.items.slice(0, -1) : reading.items; - const items = startedEmpty || place.quoted > 0 ? [...place.kept, ...place.opens] : going; - return stepOf( - reading, - { ...lineOf(place, '', 'other'), separates: true }, - { codeMayStart: true, items, startedEmpty }, - ); -} - -function readOutside(reading: Reading, line: string, index: number): Step { - const { last } = reading; - const place = placeOf(line, reading.items, last?.prose === true ? last.container : undefined); - if (blankLine.test(place.content)) { - return blankStep(reading, place); - } - const above = lazilyAfter(reading.last, place, reading.items); - if (above !== undefined) { - const read = lineOf(above, above.container, 'prose'); - return stepOf(reading, read, { codeMayStart: false, inCode: false }); - } - return containedStep(reading, place, index); -} - -function closesFence({ closer, within }: OpenBlock, line: string): boolean { - const rest = fromColumn(line, within ?? 0); - const content = withoutBlanks(rest); - const closing = fenceOpener.exec(content)?.[0] ?? ''; - const fits = closing.startsWith(closer.charAt(0)) && closing.length >= closer.length; - return columnsOf(blanksOf(rest)) <= 3 && fits && blankLine.test(content.slice(closing.length)); -} - -function readInside(reading: Reading, open: OpenBlock, line: string): Step | undefined { - if (open.within !== undefined && !blankLine.test(line) && columnsOf(blanksOf(line)) < open.within) { - return undefined; - } - const closes = open.kind === 'fence' ? closesFence(open, line) : htmlEnds[open.kind].test(line); - const separates = closes && blankLine.test(line); - const read: ReadLine = { prose: false, text: false, form: undefined, separates, container: '', quoted: 0 }; - return stepOf(reading, read, { open: closes ? undefined : open, codeMayStart: closes }); -} - -function readLine(reading: Reading, line: string, index: number): Step { - const inside = reading.open === undefined ? undefined : readInside(reading, reading.open, line); - return inside ?? readOutside({ ...reading, open: undefined }, line, index); -} - -export function markdownLinesOf(lines: readonly string[]): MarkdownLines { - const inTable = tableLinesOf(lines); - let reading = atRest; - const reads = lines.map((line, index) => { - const step = readLine(reading, line, index); - const read = { ...step.read, prose: step.read.prose && inTable[index] === false }; - reading = { ...step.reading, last: read }; - return read; - }); - return { reads, open: reading.open }; -} diff --git a/packages/definitions/src/markdown/table-rows.ts b/packages/definitions/src/markdown/table-rows.ts deleted file mode 100644 index a4d9a8a79..000000000 --- a/packages/definitions/src/markdown/table-rows.ts +++ /dev/null @@ -1,20 +0,0 @@ -const blankLine = /^[ \t]*$/u; - -const outerBlanks = /^[ \t]+|[ \t]+$/gu; - -const delimiterCell = /^:?-+:?$/u; - -const outerPipes = /^\||\|$/gu; - -function isDelimiterRow(line: string | undefined): boolean { - const cells = line?.replaceAll(outerBlanks, '').replaceAll(outerPipes, '').split('|') ?? []; - return line?.includes('|') === true && cells.every((cell) => delimiterCell.test(cell.replaceAll(outerBlanks, ''))); -} - -export function tableLinesOf(lines: readonly string[]): readonly boolean[] { - let within = false; - return lines.map((line, index) => { - within = within ? !blankLine.test(line) : line.includes('|') && isDelimiterRow(lines.at(index + 1)); - return within; - }); -} diff --git a/packages/definitions/src/markdown/written-blocks.test.ts b/packages/definitions/src/markdown/written-blocks.test.ts deleted file mode 100644 index 243d1bfe3..000000000 --- a/packages/definitions/src/markdown/written-blocks.test.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { writtenTextIssues } from '../document.ts'; - -type Finding = readonly [number, 'heading' | 'details' | 'fence' | 'html']; - -const literalHeader = { line: 1, column: 8 }; - -function kindOf(detail: string): Finding[1] { - if (detail.includes('takes no heading')) { - return 'heading'; - } - if (detail.includes('leaves open the fence')) { - return 'fence'; - } - return detail.includes('leaves open the HTML block') ? 'html' : 'details'; -} - -function findingsOf(...lines: readonly string[]): readonly Finding[] { - const source = ['about: |', ...lines.map((line) => ` ${line}`)]; - return writtenTextIssues(lines.join('\n'), 'about', { lines: source, firstLine: 1 }, literalHeader).map( - ({ line, detail }): Finding => [line - 2, kindOf(detail)], - ); -} - -describe('a fence in a text of a document', () => { - it('opens indented three spaces after a line of a paragraph, and is a line of the paragraph indented four', () => { - expect([findingsOf('Text', ' ```', '# Inside'), findingsOf('Text', ' ```', '# After it')]).toEqual([ - [[1, 'fence']], - [[2, 'heading']], - ]); - }); - - it('closes on a line of its marker indented at most three spaces, with only spaces or tabs after it', () => { - expect([ - findingsOf('```', 'code', ' ```', '# After it'), - findingsOf('```', 'code', ' ```', '# Inside'), - findingsOf('```', 'code', '```\t ', '# After it'), - findingsOf('```', 'code', '```\u00A0', '# Inside'), - ]).toEqual([[[3, 'heading']], [[0, 'fence']], [[3, 'heading']], [[0, 'fence']]]); - }); - - it('of backticks takes no backtick after its marker, so a line that holds one is a line of a paragraph', () => { - expect([findingsOf('``` a`b', '# After it', '```'), findingsOf('~~~ a`b', '# Inside', '~~~')]).toEqual([ - [ - [1, 'heading'], - [2, 'fence'], - ], - [], - ]); - }); -}); - -describe('a fence in a list item of a text of a document', () => { - it('ends with the item, at the first line indented less than its content', () => { - expect([ - findingsOf('Text', ' ```', '## Details'), - findingsOf('- item', ' ```', '## Details'), - findingsOf('- item', ' ```', '## Details', ' ```'), - findingsOf('- item', ' ```', '## After it'), - findingsOf('- item', '', ' ```', ' # Inside', ' ```', '# After it'), - ]).toEqual([ - [[2, 'details']], - [[2, 'details']], - [ - [2, 'details'], - [3, 'fence'], - ], - [[2, 'heading']], - [[5, 'heading']], - ]); - }); - - it('is in no list item that starts empty when a blank line follows its marker, which ends the item', () => { - expect([ - findingsOf('1.', '', ' ```', ' ```', '# After it'), - findingsOf('-', ' ```', ' # Inside', ' ```', '# After it'), - ]).toEqual([[[4, 'heading']], [[4, 'heading']]]); - }); -}); - -describe('a block a text of a document leaves open', () => { - it('is refused at the line that opens it, a fence of backticks or of tildes', () => { - expect([findingsOf('Intro.', '```ts', 'code'), findingsOf('~~~', 'code', '```')]).toEqual([ - [[1, 'fence']], - [[0, 'fence']], - ]); - }); - - it('is refused at the line that opens it, an HTML block that ends at a closing tag of its raw kind', () => { - expect(['
', '