diff --git a/docs/reference/mcp.md b/docs/reference/mcp.md index 950a2aa3e..c43c3bc4e 100644 --- a/docs/reference/mcp.md +++ b/docs/reference/mcp.md @@ -218,12 +218,12 @@ A connection is a tool server that a person adds to the org through their agent, The recipes are also prompts, which a client that shows MCP prompts offers in its menu. A connection offers a prompt only when it lists every tool the recipe's steps call, and the description of `get_guide` names the same recipes. So a playbook endpoint offers no `first-playbook`, the org endpoint offers none, and a key that may only read gets none. -| Prompt | Title | Arguments | What the recipe does | -| ---------------- | ------------------------------------ | ------------------------------------ | -------------------------------------------------------------------------------------- | -| `first-playbook` | Create your first playbook | None | Creates a playbook with a first reasoning function, and runs it once the person agrees | -| `remember` | Make the playbook remember something | `what` to remember | Keeps what the playbook's runs returned or its events recorded in a recall function | -| `give-tools` | Give the playbook tools | Optional `server`, a tool server | Gives a reasoning function the tools of a tool server the playbook may use | -| `schedule` | Run a workflow on a schedule | The `workflow`, and `when` to run it | Starts a workflow on a schedule or on an event, and reads how its runs went | +| Prompt | Title | Arguments | What the recipe does | +| ---------------- | ------------------------------------ | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `first-playbook` | Create your first playbook | None | Creates a playbook with a first reasoning function, runs it once the person agrees, and, where the work should run on its own, saves the workflow that runs it | +| `remember` | Make the playbook remember something | `what` to remember | Keeps what the playbook's runs returned or its events recorded in a recall function | +| `give-tools` | Give the playbook tools | Optional `server`, a tool server | Gives a reasoning function the tools of a tool server the playbook may use | +| `schedule` | Run a workflow on a schedule | The `workflow`, and `when` to run it | Starts a workflow on a schedule or on an event, and reads how its runs went | A prompt returns the recipe with the person's words filled in, followed by the format guide it needs. diff --git a/docs/tutorials/first-workflow.md b/docs/tutorials/first-workflow.md index a4279ec8f..298f36a84 100644 --- a/docs/tutorials/first-workflow.md +++ b/docs/tutorials/first-workflow.md @@ -201,7 +201,7 @@ The playbook now holds a workflow that reviews a brief and asks for an approval, Next: - An interaction function can also send its question through a tool, such as a chat message, and take the person's reply as the answer. [Interaction function format](../reference/interaction-format.md) shows how. -- [Run a workflow on a schedule or event](../guides/triggers.md) starts a workflow without anyone asking. +- [Run a workflow on a schedule or event](../guides/triggers.md) starts a workflow without anyone asking. A new playbook can start that way from the beginning: when you ask your agent for one, it asks how the work starts, unless your words already say so, such as "every morning" or "whenever a brief arrives". Where the work should start on its own, it writes the workflow that runs your first function, once you have seen that function answer. - [Workflows and runs](../concepts/workflows.md) explains how runs start, wait and end. diff --git a/packages/api/README.md b/packages/api/README.md index 09931260d..e837f23f6 100644 --- a/packages/api/README.md +++ b/packages/api/README.md @@ -244,7 +244,7 @@ The server serves `list_models`, an org query of [`@on.auto/reasoning`](../../ca `makeAppRuntime(layer)` builds the runtime every call runs in. Its `run(effect, signal?)` answers the effect's value, or `cancelled` when the effect was interrupted, by the runtime's disposal or by the abort of the signal given, which interrupts it so its finalizers run; the server gives the signal of the call a workflow performs, so a call the workflow cancels stops the run it started. -`src/index.ts` is the entry point: `createApiHandler`, `makeAppRuntime`, `operationRoutes`, `openApiDocumentRoutes` and the document's path `openApiPath`, `guideRoutes`, `apiCatalogRoutes`, `clientMetadataRoutes` and its `ClientMetadataDocument`, `guideShelfOf`, `guideShelfFrom(shippedFiles, types)`, the shelf of the guides the API serves for the types of definition it is given, each a `ServedType`, its type, noun and guide, as a `Capability` of `@on.auto/definitions` is one, read through the `ShippedFiles` it is given: the terminology and the format page of each type, `docs` of `@on.auto/runtime`, without the markup of the documentation site and with their links resolved to the published pages, and the recipes, `src/guides/recipes` of `@on.auto/api`, each kept only when its format guide is; `apiShippedRoots()` declares the root of both packages, the runtime's two folders above the API's in the repository, the image and the npm package alike, read only when it is called, so the entry loads where `import.meta.url` names no file (`src/shipped-files/api-roots.test.ts`), `mcpRoutes`, the instructions, the types of a guide and a recipe, the guide's address and media type, the bounds, the types of the served Studio, `ApiStudio`, `HostedStudio`, `ServedStudio`, `StudioHost` and `StudioAnswer`, and their types. `@on.auto/api/testing` exports what the server's tests share: real MCP clients of the current SDK, on either revision, and of the SDK's 1.x line, each of which records in `compiledOutputSchemas` the output schemas it compiles a validator from and refuses every result it checks against one; helpers that read a tool listing, among them `takingPlaybook`, which gives a playbook endpoint's tool the `playbook` argument it has on `/mcp`, and `operationToolsIn`, which answers the listed tools but `get_guide`; helpers that read an OpenAPI document, `operationsIn`, `openApiErrorsIn`, which checks it against the published schema, `unresolvedSchemasIn`, which compiles each of its schemas alone, and `schemaErrorsIn`, which checks a value against one; the old words of the repository's vocabulary, `oldWords`, `oldWordsIn` and `foundIn`; and the `wait_forever` operation, which never finishes on its own. +`src/index.ts` is the entry point: `createApiHandler`, `makeAppRuntime`, `operationRoutes`, `openApiDocumentRoutes` and the document's path `openApiPath`, `guideRoutes`, `apiCatalogRoutes`, `clientMetadataRoutes` and its `ClientMetadataDocument`, `guideShelfOf`, `guideShelfFrom(shippedFiles, types)`, the shelf of the guides the API serves for the types of definition it is given, each a `ServedType`, its type, noun and guide, as a `Capability` of `@on.auto/definitions` is one, read through the `ShippedFiles` it is given: the terminology and the format page of each type, `docs` of `@on.auto/runtime`, without the markup of the documentation site and with their links resolved to the published pages, and the recipes, `src/guides/recipes` of `@on.auto/api`, each kept only when its format guide is, and given the words of another type, and the tools they call, only where that type's guide is carried too, as the first playbook writes the workflow that runs its function only where the workflow guide is; `apiShippedRoots()` declares the root of both packages, the runtime's two folders above the API's in the repository, the image and the npm package alike, read only when it is called, so the entry loads where `import.meta.url` names no file (`src/shipped-files/api-roots.test.ts`), `mcpRoutes`, the instructions, the types of a guide and a recipe, the guide's address and media type, the bounds, the types of the served Studio, `ApiStudio`, `HostedStudio`, `ServedStudio`, `StudioHost` and `StudioAnswer`, and their types. `@on.auto/api/testing` exports what the server's tests share: real MCP clients of the current SDK, on either revision, and of the SDK's 1.x line, each of which records in `compiledOutputSchemas` the output schemas it compiles a validator from and refuses every result it checks against one; helpers that read a tool listing, among them `takingPlaybook`, which gives a playbook endpoint's tool the `playbook` argument it has on `/mcp`, and `operationToolsIn`, which answers the listed tools but `get_guide`; helpers that read an OpenAPI document, `operationsIn`, `openApiErrorsIn`, which checks it against the published schema, `unresolvedSchemasIn`, which compiles each of its schemas alone, and `schemaErrorsIn`, which checks a value against one; the old words of the repository's vocabulary, `oldWords`, `oldWordsIn` and `foundIn`; and the `wait_forever` operation, which never finishes on its own. ## Source diff --git a/packages/api/src/guides/recipes.test.ts b/packages/api/src/guides/recipes.test.ts new file mode 100644 index 000000000..3ec7ae6ea --- /dev/null +++ b/packages/api/src/guides/recipes.test.ts @@ -0,0 +1,77 @@ +import { readFileSync } from 'node:fs'; + +import { shippedFilesOnDisk } from '@on.auto/operations/shipped-files-on-disk'; +import { describe, expect, it } from 'vitest'; + +import { apiShippedRoots } from '../index.ts'; +import type { Guide, Recipe } from './guide-shelf.ts'; +import { recipesFor } from './recipes.ts'; + +const recipeFiles = shippedFilesOnDisk(apiShippedRoots()); + +function guidesNamed(names: readonly string[]): readonly Guide[] { + return names.map((name) => ({ name, title: name, description: name, text: `# ${name}\n` })); +} + +async function firstPlaybookCarrying(...carried: readonly string[]): Promise { + const recipes = await recipesFor(recipeFiles, guidesNamed(['reasoning-function', ...carried])); + return recipes.find(({ name }) => name === 'first-playbook'); +} + +const withWorkflows = await firstPlaybookCarrying('workflow'); + +const withWorkflowsAndInteraction = await firstPlaybookCarrying('workflow', 'interaction-function'); + +const withoutWorkflows = await firstPlaybookCarrying(); + +const firstPlaybookFile = readFileSync(new URL('recipes/first-playbook.md', import.meta.url), 'utf8'); + +const callsOfTheFunction = ['list_playbooks', 'create_playbook', 'create_definition', 'execute', 'run_definition']; + +describe('the first-playbook recipe where the workflow guide is carried', () => { + it('says that a first playbook holds the workflow that runs its function where the work should run on its own', () => { + expect(withWorkflows?.text).toContain( + 'A first playbook holds a first reasoning function that the person sees answer, and, where the work should run on its own, the workflow that runs it. Read the reasoning-function guide', + ); + }); + + it('learns how the work starts after what the playbook is for, and takes words that already say so as the answer', () => { + expect(withWorkflows?.text).toContain( + 'a budget and a measurable goal. Then learn how its work starts: on its own at set times, on its own whenever something arrives, or only when someone asks. Where their words already say so, such as "every morning", "continuously", "keep reviewing" or "whenever a lead comes in", take that as the answer and say so rather than asking again. When more than one model is offered', + ); + }); + + it('writes the workflow after the function has answered, with its trigger as the workflow guide says, and saves it once the person agrees', () => { + expect(withWorkflows?.text).toContain( + "6. Ask for an input, run the function with run_definition, and tell the person what it answered, in their words.\n7. Where the work starts on its own, read the workflow guide with get_guide and write the workflow that runs the function, with a `schedule` as the guide says. For set times, `cron` in UTC or `every`: a run they start is given only its due time, so the function finds its material with its tools, or the workflow gives it a fixed input in `input.from`. For what arrives, `on` with a filter on an event `type` named in the past tense, such as `com.acme.lead.received`: a run is given a list holding the event, and the workflow passes the event's `data` to the function. Where the material comes from sources not yet connected, start with `on`, so that the person starts runs today with material they paste, and move to a clock once the function's tools reach the sources.\n8. Show the person the workflow's document, and save it with create_definition, `type` workflow, once they agree.\n", + ); + }); + + it('tells the person when it next runs or what starts it, publishes what they paste for an event, and names the schedule recipe and the workflow guide first of what comes next', () => { + expect(withWorkflows?.text).toContain( + '\n9. Tell the person when it next runs, in their own time, or what starts it. For `on`, publish the material they paste with publish_event, as an event of that type with a `source` that names where it came from, and a moment later show what its run answered with list_runs, `type` workflow, and get_run.\n\nThen tell the person what they can do next: change when it runs with the schedule recipe and the workflow guide, give it tools with the give-tools recipe, or make the playbook remember its answers with the remember recipe.\n', + ); + }); + + it('calls publish_event, list_runs and get_run beside the tools of the function', () => { + expect(withWorkflows?.calls).toEqual([...callsOfTheFunction, 'publish_event', 'list_runs', 'get_run']); + }); +}); + +describe('the first-playbook recipe where the interaction-function guide is carried too', () => { + it('names an interaction function among what comes next, after the schedule recipe and the tools', () => { + expect(withWorkflowsAndInteraction?.text).toContain( + 'Then tell the person what they can do next: change when it runs with the schedule recipe and the workflow guide, give it tools with the give-tools recipe, send someone a message through a tool and take their answer with an interaction function, or make the playbook remember its answers with the remember recipe.\n', + ); + expect(withWorkflowsAndInteraction?.calls).toEqual(withWorkflows?.calls); + }); +}); + +describe('the first-playbook recipe where the workflow guide is not carried', () => { + it('ends at the run of the function and what comes next, as its file has it, and calls only the tools of the function', () => { + expect([withoutWorkflows?.text, withoutWorkflows?.calls]).toEqual([firstPlaybookFile, callsOfTheFunction]); + expect(firstPlaybookFile).toContain( + 'tell the person what it answered, in their words.\n\nThen tell the person what they can do next: give it tools with the give-tools recipe, or make the playbook remember its answers with the remember recipe.\n', + ); + }); +}); diff --git a/packages/api/src/guides/recipes.ts b/packages/api/src/guides/recipes.ts index 2a43ca5e2..2cae6ffb0 100644 --- a/packages/api/src/guides/recipes.ts +++ b/packages/api/src/guides/recipes.ts @@ -70,30 +70,102 @@ interface Insertion { readonly words: string; } -const interactionGuide = 'interaction-function'; +interface Additions { + readonly insertions: readonly Insertion[]; + readonly calls: readonly string[]; +} + +type AdditionsByRecipe = Readonly>; + +interface AdditionsOfAGuide { + readonly guide: string; + readonly byRecipe: AdditionsByRecipe; +} -const whereInteractionIsServed: Readonly> = { +const whereInteractionIsServed: AdditionsByRecipe = { 'give-tools': { - after: 'and which of them it needs.', - words: [ - ' When the playbook only needs what a tool answers, with nothing to reason about, write an interaction function that calls the tool instead: it answers at once with what the tool answered and spends no model call, and the interaction-function guide says how to write it from the answer execute gave.', - ' When the playbook should send a person a message through a tool and take their answer, write an interaction function instead: the interaction-function guide says how it names the tool it sends through and the tool it reads replies with, each called the same way.', - ].join(''), + insertions: [ + { + after: 'and which of them it needs.', + words: [ + ' When the playbook only needs what a tool answers, with nothing to reason about, write an interaction function that calls the tool instead: it answers at once with what the tool answered and spends no model call, and the interaction-function guide says how to write it from the answer execute gave.', + ' When the playbook should send a person a message through a tool and take their answer, write an interaction function instead: the interaction-function guide says how it names the tool it sends through and the tool it reads replies with, each called the same way.', + ].join(''), + }, + ], + calls: [], }, 'first-playbook': { - after: 'give it tools with the give-tools recipe', - words: ', send someone a message through a tool and take their answer with an interaction function', + insertions: [ + { + after: 'give it tools with the give-tools recipe', + words: ', send someone a message through a tool and take their answer with an interaction function', + }, + ], + calls: [], + }, +}; + +const whereWorkflowsAreServed: AdditionsByRecipe = { + 'first-playbook': { + insertions: [ + { + after: 'the person sees answer', + words: ', and, where the work should run on its own, the workflow that runs it', + }, + { + after: 'a budget and a measurable goal.', + words: + ' Then learn how its work starts: on its own at set times, on its own whenever something arrives, or only when someone asks. Where their words already say so, such as "every morning", "continuously", "keep reviewing" or "whenever a lead comes in", take that as the answer and say so rather than asking again.', + }, + { + after: 'tell the person what it answered, in their words.', + words: [ + "\n7. Where the work starts on its own, read the workflow guide with get_guide and write the workflow that runs the function, with a `schedule` as the guide says. For set times, `cron` in UTC or `every`: a run they start is given only its due time, so the function finds its material with its tools, or the workflow gives it a fixed input in `input.from`. For what arrives, `on` with a filter on an event `type` named in the past tense, such as `com.acme.lead.received`: a run is given a list holding the event, and the workflow passes the event's `data` to the function. Where the material comes from sources not yet connected, start with `on`, so that the person starts runs today with material they paste, and move to a clock once the function's tools reach the sources.", + "\n8. Show the person the workflow's document, and save it with create_definition, `type` workflow, once they agree.", + '\n9. Tell the person when it next runs, in their own time, or what starts it. For `on`, publish the material they paste with publish_event, as an event of that type with a `source` that names where it came from, and a moment later show what its run answered with list_runs, `type` workflow, and get_run.', + ].join(''), + }, + { + after: 'what they can do next:', + words: ' change when it runs with the schedule recipe and the workflow guide,', + }, + ], + calls: ['publish_event', 'list_runs', 'get_run'], }, }; -async function textOf( +const additionsWhereServed: readonly AdditionsOfAGuide[] = [ + { guide: 'interaction-function', byRecipe: whereInteractionIsServed }, + { guide: 'workflow', byRecipe: whereWorkflowsAreServed }, +]; + +function additionsTo(name: string, carried: ReadonlySet): readonly Additions[] { + return additionsWhereServed.filter(({ guide }) => carried.has(guide)).flatMap(({ byRecipe }) => byRecipe[name] ?? []); +} + +function withInserted(text: string, insertions: readonly Insertion[]): string { + return insertions.reduce( + (inserted, { after, words }) => inserted.replace(after, (found) => `${found}${words}`), + text, + ); +} + +async function recipeOf( shippedFiles: ShippedFiles, - { name }: RecipeOutline, + outline: RecipeOutline, carried: ReadonlySet, -): Promise { - const text = await shippedFiles.text(shippedFileIn(recipesFolder, `${name}.md`)); - const insertion = carried.has(interactionGuide) ? whereInteractionIsServed[name] : undefined; - return insertion === undefined ? text : text.replace(insertion.after, `${insertion.after}${insertion.words}`); +): Promise { + const text = await shippedFiles.text(shippedFileIn(recipesFolder, `${outline.name}.md`)); + const additions = additionsTo(outline.name, carried); + return { + ...outline, + text: withInserted( + text, + additions.flatMap(({ insertions }) => insertions), + ), + calls: [...outline.calls, ...additions.flatMap(({ calls }) => calls)], + }; } export function recipesFor(shippedFiles: ShippedFiles, guides: readonly Guide[]): Promise { @@ -101,6 +173,6 @@ export function recipesFor(shippedFiles: ShippedFiles, guides: readonly Guide[]) return Promise.all( outlines .filter(({ formatGuide }) => carried.has(formatGuide)) - .map(async (outline) => Object.assign({ text: await textOf(shippedFiles, outline, carried) }, outline)), + .map((outline) => recipeOf(shippedFiles, outline, carried)), ); } diff --git a/packages/api/src/guides/recipes/first-playbook.md b/packages/api/src/guides/recipes/first-playbook.md index b6fc73c6d..d661f7c0a 100644 --- a/packages/api/src/guides/recipes/first-playbook.md +++ b/packages/api/src/guides/recipes/first-playbook.md @@ -1,6 +1,6 @@ # Create your first playbook -A first playbook holds one reasoning function that the person can run and see the answer of. Read the reasoning-function guide with get_guide before you write the function. +A first playbook holds a first reasoning function that the person sees answer. Read the reasoning-function guide with get_guide before you write the function. 1. Call list_playbooks to see the playbooks of the org, and list_models where this connection has it to see the models this server can call. When it lists none and says in `provider_setup` how the person adds a model provider, help them do that first. 2. Ask the person what the playbook is for, in their own words, and what its first function should do, such as checking a campaign brief for an audience, a budget and a measurable goal. When more than one model is offered, ask which to use; a model whose id ends in `*` stands for many and is not a model to run. @@ -8,4 +8,5 @@ A first playbook holds one reasoning function that the person can run and see th 4. Write the reasoning function in the format of the reasoning-function guide: its model, a description, an input schema with the fields the person will give, and a prompt. When it should use a tool server's tools, follow the give-tools recipe: to learn what a tool answers, call it with execute, and never make a function to look. Show the person the whole document and ask whether to save it. 5. Once they agree, save it with create_definition, `type` reasoning. A name that is taken is refused; choose another with the person rather than changing the function that has it. 6. Ask for an input, run the function with run_definition, and tell the person what it answered, in their words. -7. Tell the person what they can do next: run it again on another input, change it, give it tools with the give-tools recipe, make the playbook remember its answers with the remember recipe, or run it on a schedule with the schedule recipe. + +Then tell the person what they can do next: give it tools with the give-tools recipe, or make the playbook remember its answers with the remember recipe. diff --git a/packages/api/src/guides/served-guides.test.ts b/packages/api/src/guides/served-guides.test.ts index dfa60a669..ed8b5621c 100644 --- a/packages/api/src/guides/served-guides.test.ts +++ b/packages/api/src/guides/served-guides.test.ts @@ -48,6 +48,8 @@ const servedWithInteraction = await guideShelfFrom(guideFiles, [...everyType, in const servedForReasoningAndWorkflows = await guideShelfFrom(guideFiles, [reasoning, workflow]); +const servedForReasoning = await guideShelfFrom(guideFiles, [reasoning]); + function recipeTextsOf({ recipes }: GuideShelf): Readonly> { return Object.fromEntries(recipes.map(({ name, text }) => [name, text])); } @@ -213,7 +215,7 @@ describe('the recipes that give a playbook tools', () => { "When it should use a tool server's tools, follow the give-tools recipe: to learn what a tool answers, call it with execute, and never make a function to look.", ); expect([byteLengthOf(String(texts['give-tools'])), byteLengthOf(String(texts['first-playbook']))]).toEqual([ - 2716, 1898, + 2716, 3431, ]); }); }); @@ -226,24 +228,16 @@ describe('the recipes of a server that serves interaction functions', () => { '5. Ask the person what the function should do with the tools, and which of them it needs. When the playbook only needs what a tool answers, with nothing to reason about, write an interaction function that calls the tool instead: it answers at once with what the tool answered and spends no model call, and the interaction-function guide says how to write it from the answer execute gave. When the playbook should send a person a message through a tool and take their answer, write an interaction function instead: the interaction-function guide says how it names the tool it sends through and the tool it reads replies with, each called the same way.\n', ); expect(texts['first-playbook']).toContain( - 'give it tools with the give-tools recipe, send someone a message through a tool and take their answer with an interaction function, make the playbook remember its answers', + 'give it tools with the give-tools recipe, send someone a message through a tool and take their answer with an interaction function, or make the playbook remember its answers', ); expect([ bytesOf(texts, 'first-playbook', 'remember', 'give-tools', 'schedule'), bytesOf(recipeTextsOf(served), 'first-playbook', 'remember', 'give-tools', 'schedule'), ]).toEqual([ - [1988, 2327, 3277, 1294], - [1898, 2327, 2716, 1294], + [3521, 2327, 3277, 1294], + [3431, 2327, 2716, 1294], ]); }); - - it('name the same tools as on a server without them', () => { - const { recipes } = servedWithInteraction; - - expect(recipes.map(({ name, text }) => [name, toolsNamedIn(text).toSorted()])).toEqual( - recipes.map(({ name, calls }) => [name, calls.toSorted()]), - ); - }); }); const calledWhereListed: ReadonlySet = new Set(['get_guide', 'list_models', 'add_connection']); @@ -259,11 +253,14 @@ function toolsNamedIn(text: string): readonly string[] { } describe('the tools a recipe calls', () => { - it('are every tool its steps name, but the guide tool, listed on every connection, list_models and add_connection, called only where they are listed, and the fields of a tool they name', () => { + it('are every tool its steps name, on a server with or without workflows and interaction functions, but the guide tool, listed on every connection, list_models and add_connection, called only where they are listed, and the fields of a tool they name', () => { const { recipes } = served; + const onEveryServer = [served, servedWithInteraction, servedForReasoningAndWorkflows, servedForReasoning].flatMap( + (shelf) => shelf.recipes, + ); - expect(recipes.map(({ name, text }) => [name, toolsNamedIn(text).toSorted()])).toEqual( - recipes.map(({ name, calls }) => [name, calls.toSorted()]), + expect(onEveryServer.map(({ name, text }) => [name, toolsNamedIn(text).toSorted()])).toEqual( + onEveryServer.map(({ name, calls }) => [name, calls.toSorted()]), ); expect(recipes[0]?.text).toContain('list_models where this connection has it'); expect(recipes.find(({ name }) => name === 'give-tools')?.text).toContain( diff --git a/packages/api/src/testing/served-instructions.ts b/packages/api/src/testing/served-instructions.ts index d5561b453..b8eeacce8 100644 --- a/packages/api/src/testing/served-instructions.ts +++ b/packages/api/src/testing/served-instructions.ts @@ -65,7 +65,16 @@ export const definitionTypes: readonly DefinitionType[] = [ export const recipes: readonly RecipeCalls[] = [ { name: 'first-playbook', - calls: ['list_playbooks', 'create_playbook', 'create_definition', 'execute', 'run_definition'], + calls: [ + 'list_playbooks', + 'create_playbook', + 'create_definition', + 'execute', + 'run_definition', + 'publish_event', + 'list_runs', + 'get_run', + ], }, { name: 'remember', diff --git a/packages/server/src/mcp/guides-over-mcp.test.ts b/packages/server/src/mcp/guides-over-mcp.test.ts index 1bbc0a3b3..372fc4d71 100644 --- a/packages/server/src/mcp/guides-over-mcp.test.ts +++ b/packages/server/src/mcp/guides-over-mcp.test.ts @@ -140,6 +140,24 @@ describe('the prompts of the server', () => { }); }); +describe('the first-playbook prompt of the server', () => { + it('answers the recipe get_guide reads, its workflow steps among them, and embeds the reasoning-function guide', async () => { + const { prompt, read } = await onMcp(async (session) => ({ + prompt: Schema.decodeUnknownSync(PromptSchema)(await session.getPrompt('first-playbook')), + read: Schema.decodeUnknownSync(TextSchema)(await session.callTool('get_guide', { guide: 'first-playbook' })), + })); + const [asked, embedded] = prompt.messages; + const recipe = read.content.map(({ text }) => text).join(''); + + expect(asked.content.text).toBe(`Create my first playbook.\n\n${recipe}`); + expect(recipe).toContain( + '\n7. Where the work starts on its own, read the workflow guide with get_guide and write the workflow that runs the function,', + ); + expect(recipe).toContain('publish the material they paste with publish_event'); + expect(embedded.content.resource.uri).toBe('guide://reasoning-function'); + }); +}); + const PromptNamesSchema = Schema.Struct({ prompts: Schema.Array(Schema.Struct({ name: Schema.String })) }); function promptNamesIn(listed: unknown): readonly string[] {