diff --git a/capabilities/interaction/src/requests/list-interactions.test.ts b/capabilities/interaction/src/requests/list-interactions.test.ts index cef0d64b2..d160507ee 100644 --- a/capabilities/interaction/src/requests/list-interactions.test.ts +++ b/capabilities/interaction/src/requests/list-interactions.test.ts @@ -38,6 +38,14 @@ async function listed(playbook: Awaited id), page: page.output }; } +describe('the description of the open requests', () => { + it('says that their pages are read on only as the person needs', () => { + expect(listInteractions.registration.description).toContain( + '`cursor` is the next_cursor of the page before, to read on only as the person needs.', + ); + }); +}); + describe('the open requests of a playbook', () => { it('are listed newest first, kept to one party or one function when asked', async () => { const playbook = await playbookWithThreeRequests(); diff --git a/capabilities/interaction/src/requests/list-interactions.ts b/capabilities/interaction/src/requests/list-interactions.ts index 714ff250f..b9dd04245 100644 --- a/capabilities/interaction/src/requests/list-interactions.ts +++ b/capabilities/interaction/src/requests/list-interactions.ts @@ -73,10 +73,10 @@ const FilterText = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength const description = [ 'Lists the open requests of the playbook, newest first: what each function asked, of whom, through which tool, or in the inbox,', 'until when and how its delivery stands, with the `run_id` that answer_interaction takes.', - 'Each carries its `answer_schema`, the shape answer_interaction checks an answer against, as recorded when it was asked,', + 'Each carries its `answer_schema`, the shape answer_interaction checks an answer against, as recorded when asked,', 'which get_definition may no longer show; null for a notification.', 'Use it when the person asks what the playbook is waiting on, or to find the request they answer.', - '`to` keeps the requests to one party, `type` to one type of function and `function` to one name, which with `type` names one function; `cursor` is the next_cursor of the page before.', + '`to` keeps the requests to one party, `type` to one type of function and `function` to one name, which with `type` names one function; `cursor` is the next_cursor of the page before, to read on only as the person needs.', 'Every reader of the playbook sees each party and message, as a run’s input is seen.', ].join(' '); diff --git a/docs/assets/openapi.json b/docs/assets/openapi.json index 1c3879098..b3da0845a 100644 --- a/docs/assets/openapi.json +++ b/docs/assets/openapi.json @@ -4,7 +4,7 @@ "info": { "title": "Auto Runtime", "version": "0.15.0", - "description": "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 through workflows; runs belong to the playbook. Before writing a definition, read its format with get_guide, which also holds what these words mean and the recipes for common tasks. Every operation answers a problem document when it does not succeed, and says in its description what it does, when to use it and which operation to use instead." + "description": "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. Before writing a definition, read its format with get_guide, which also holds what these words mean and the recipes for common tasks. Every operation answers a problem document when it does not succeed, and says in its description what it does, when to use it and which operation to use instead." }, "servers": [ { @@ -987,7 +987,7 @@ "get": { "operationId": "search_in_org", "summary": "Search tools", - "description": "Searches the tools of the tool servers this playbook's functions may use, which a function names in its tools as server/tool or server/*, and execute calls. Without `terms` it lists the servers and, while they offer at most 100 tools together, every tool; with `terms`, the tools whose names, arguments or descriptions hold the words, best first. Each server is asked for its tools when this is called, and one that cannot be asked just now says why in place of its tools. `server` keeps one server, `only` the tools that read or those that may write, and introspect describes a tool whole. Without `playbook` it searches the tool servers of the whole org, naming the playbooks each serves. `providers` lists what a person may connect that no server serves yet.", + "description": "Searches the tools of the tool servers this playbook's functions may use, named in a function's tools as server/tool or server/* and called by execute. Without `terms` it lists the servers and, while they offer at most 100 tools together, every tool; with `terms`, the tools whose names, arguments or descriptions hold them, best first. Each server is asked at each call, and one that cannot be asked just now says why in place of its tools. `server` keeps one server, `only` the tools that read or those that may write; introspect describes a tool whole. Without `playbook` it searches every tool server of the org, with the playbooks each serves. With `terms` naming a service, `providers` lists what a person may connect that no server serves yet, found as tools are, for add_connection by name.", "tags": [ "tool-servers" ], @@ -1315,7 +1315,7 @@ "post": { "operationId": "add_connection", "summary": "Add connection", - "description": "Connects a service the org's functions should reach: a provider search lists, by its name and placeholders, answered pending with a link to Studio's page. A server given by hand takes name, type, entry and credential instead, none holding a secret. The person approves it, and gives its credential, on the page the link opens within ten minutes.", + "description": "Starts connecting a person's account at a service, named by a provider that search lists, by its name and placeholders, answered pending with a link to Studio's page. A server given by hand takes name, type, entry and credential instead, none holding a secret. The person approves it, and gives its credential, on the page the link opens within ten minutes.", "tags": [ "connections" ], @@ -7873,7 +7873,7 @@ "get": { "operationId": "list_runs", "summary": "List runs", - "description": "Lists the runs of the playbook a page at a time, newest first, each with its definition, its status, who started it and when it ended, without its output. Use it to find a run the person means, such as the runs of a scheduled workflow; get_run reads one run in full. `type` and `name` keep the runs of one definition and `status` those in one status. A filtered page looks at up to 1000 runs, so it may hold fewer runs than `limit`, or none, while has_more is true. `cursor` is the next_cursor of the page before.", + "description": "Lists the runs of the playbook a page at a time, newest first, each with its definition, its status, who started it and when it ended, without its output. Use it to find a run the person means, such as the runs of a scheduled workflow; get_run reads one run in full. `type` and `name` keep the runs of one definition and `status` those in one status. A filtered page looks at up to 1000 runs, so it may hold fewer runs than `limit`, or none, while has_more is true. `cursor` is the next_cursor of the page before, to read on only as the person needs.", "tags": [ "runs" ], @@ -8890,7 +8890,7 @@ "get": { "operationId": "list_playbook_events", "summary": "List playbook events", - "description": "Lists what happened in the playbook a page at a time, newest first: definitions saved and retired, runs started and how they ended, the steps of workflow runs, tool calls, and the events published to it, each with a summary in plain words, its fact as data with a large field as its size, and its metadata; get_event reads one event whole. Use it to follow the playbook's activity, to see what one run did, or to find the events a recall function or a workflow's schedule can take. `type` keeps one type of event, `run_id` one run and every run it started, `since` what was recorded from that time on, and `cursor` is the next_cursor of the page before. The playbook's own creation, update and retirement are not among the events; list_playbooks shows them.", + "description": "Lists what happened in the playbook a page at a time, newest first: definitions saved and retired, runs started and how they ended, the steps of workflow runs, tool calls, and the events published to it, each with a summary in plain words, its fact as data with a large field as its size, and its metadata; get_event reads one event whole. Use it to follow the playbook's activity, to see what one run did, or to find the events a recall function or a workflow's schedule can take. `type` keeps one type of event, `run_id` one run and every run it started, `since` what was recorded from that time on, and `cursor` is the next_cursor of the page before, to read on only as the person needs. The playbook's own creation, update and retirement are not among the events; list_playbooks shows them.", "tags": [ "events" ], @@ -9709,7 +9709,7 @@ "get": { "operationId": "search", "summary": "Search tools", - "description": "Searches the tools of the tool servers this playbook's functions may use, which a function names in its tools as server/tool or server/*, and execute calls. Without `terms` it lists the servers and, while they offer at most 100 tools together, every tool; with `terms`, the tools whose names, arguments or descriptions hold the words, best first. Each server is asked for its tools when this is called, and one that cannot be asked just now says why in place of its tools. `server` keeps one server, `only` the tools that read or those that may write, and introspect describes a tool whole.", + "description": "Searches the tools of the tool servers this playbook's functions may use, named in a function's tools as server/tool or server/* and called by execute. Without `terms` it lists the servers and, while they offer at most 100 tools together, every tool; with `terms`, the tools whose names, arguments or descriptions hold them, best first. Each server is asked at each call, and one that cannot be asked just now says why in place of its tools. `server` keeps one server, `only` the tools that read or those that may write; introspect describes a tool whole.", "tags": [ "tool-servers" ], @@ -10855,7 +10855,7 @@ "get": { "operationId": "list_interactions", "summary": "List open requests", - "description": "Lists the open requests of the playbook, newest first: what each function asked, of whom, through which tool, or in the inbox, until when and how its delivery stands, with the `run_id` that answer_interaction takes. Each carries its `answer_schema`, the shape answer_interaction checks an answer against, as recorded when it was asked, which get_definition may no longer show; null for a notification. Use it when the person asks what the playbook is waiting on, or to find the request they answer. `to` keeps the requests to one party, `type` to one type of function and `function` to one name, which with `type` names one function; `cursor` is the next_cursor of the page before. Every reader of the playbook sees each party and message, as a run’s input is seen.", + "description": "Lists the open requests of the playbook, newest first: what each function asked, of whom, through which tool, or in the inbox, until when and how its delivery stands, with the `run_id` that answer_interaction takes. Each carries its `answer_schema`, the shape answer_interaction checks an answer against, as recorded when asked, which get_definition may no longer show; null for a notification. Use it when the person asks what the playbook is waiting on, or to find the request they answer. `to` keeps the requests to one party, `type` to one type of function and `function` to one name, which with `type` names one function; `cursor` is the next_cursor of the page before, to read on only as the person needs. Every reader of the playbook sees each party and message, as a run’s input is seen.", "tags": [ "interactions" ], diff --git a/docs/engineering/reference/mcp.md b/docs/engineering/reference/mcp.md index dcf17143e..6d5563905 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, the definition types the server runs and the recipes whose tools are listed: 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 and recipes are, how a reasoning function names its model and tools and, where `execute` and `dry_run` are listed, what each does, that `add_connection` adds a provider, or a server by hand, and that a person approves, where it is listed, 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, the paging rule, what to say of what was done and what a refusal says. With every tool they take 1,997 characters on `/mcp`, 1,680 on `/orgs/{org}/mcp`, where `search` and `add_connection` are listed, and 1,876 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 761 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,513 bytes on the wire on `/mcp`, 12,970 on the org endpoint and 30,767 on the playbook endpoint, as `packages/server/src/mcp/served-schemas.test.ts` pins them, under the 48,000, 13,000 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, 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. | Endpoint | Tools | Use it | | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | diff --git a/docs/reference/mcp.md b/docs/reference/mcp.md index c7f1f7833..6002489c3 100644 --- a/docs/reference/mcp.md +++ b/docs/reference/mcp.md @@ -33,13 +33,15 @@ 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 and the recipes; +- that `get_guide` returns the format of each definition; - how a reasoning function names its model and its tools; -- where `add_connection` is listed, that it adds a provider, or a server by hand, and that a person approves; +- 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; - that when the person answers what a run waits on, such as an approval, the agent answers the request with `answer_interaction` and starts no new run for it. The tool's description gives the shape of the answer; -- that runs, events and requests come one page at a time; -- how to reply to the person: what was done and what they can do next, in the words of playbooks, functions, workflows and runs. An id or a status appears only when the person needs it to act. +- how to reply to the person: what was done and what they can do next, in the words of playbooks, functions, workflows and runs, not the tools' names, fields or rules. An id or a status appears only where the person needs it to act; +- that a tool that cannot do what was asked says why and what to change. + +What the instructions leave out, the tools' descriptions say. The description of `get_guide` names the recipes the connection's tools can follow. Those of `execute` and `dry_run` say what each does. Those of `list_runs`, `list_playbook_events` and `list_interactions` say that their pages come one at a time and are read on only as the person needs. The recall sentence says that a recall function reads run results, definitions and events from the playbook's history, never a run's input or its tool calls. The prediction sentence says that a prediction function predicts from a model a trainer built on the playbook's history. It also says that a build (the input `{ build: {} }`) returns `started`. @@ -175,7 +177,7 @@ Four tools work with the tool servers of the playbook, those set up in the confi - A call that names a server or a tool the playbook may not use is refused, and the refusal's words say what `search` shows. - A connection is listed with `connection`, its id, and named by its name. One that is not ready is refused, and listed unavailable, with `because` `connection_pending` while nobody has approved it, `connection_broken` once its credential is gone from the secret store or is not the tokens of a consent, its consent was revoked or has ended, or its client is no longer registered, `connection_not_allowed` when a rule of the operator no longer takes it, or `connection_unavailable` while its credential cannot be read just now, which a later call may find put right. - Each server carries `provider`: the name of the provider a connection was added from or an entry of the configuration names, or `null` for a server given by hand. -- On `/mcp` and the org endpoint, to a caller who may add a connection, `search` also lists in `providers` the [providers](http.md#providers) a person may connect that no server serves yet, each with its `name`, `title` and `description`, found by `terms` as tools are. A provider none of whose ways is offered, as while a value whoever runs Auto sets for it is not set, is left out. An agent asked for a service no server serves can then offer to connect one with `add_connection`. A key, a caller who may only read and a runtime where nobody may add get no `providers`, and neither does `search` with `server`. +- On `/mcp` and the org endpoint, to a caller who may add a connection, `search` also lists in `providers` the [providers](http.md#providers) a person may connect that no server serves yet, each with its `name`, `title` and `description`, found by `terms` as tools are. A provider none of whose ways is offered, as while a value whoever runs Auto sets for it is not set, is left out. An agent asked to connect a service, or for one no server serves, finds its provider there by the service's words and connects it with `add_connection`. A key, a caller who may only read and a runtime where nobody may add get no `providers`, and neither does `search` with `server`. - When no server serves the playbook, the words of `search` say that whoever runs the server sets one up. Where people may add connections, they also say that a person adds one, or, where `search` lists a provider, that a person connects a provider it lists. [Tool servers](http.md#tool-servers) gives the fields, limits and refusals of all five. @@ -212,7 +214,7 @@ A connection is a tool server that a person adds to the org through their agent, | `reasoning-function`, `interaction-function`, `prediction-function`, `recall-function`, `computation-function` and `workflow` | The reference page of each format, as the documentation publishes it. `reasoning-function` ends with what this runtime offers | | `first-playbook`, `remember`, `give-tools` and `schedule` | Recipes for common tasks, listed below | -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. So a playbook endpoint offers no `first-playbook`, the org endpoint offers none, and a key that may only read gets none. +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 | | ---------------- | ------------------------------------ | ------------------------------------ | -------------------------------------------------------------------------------------- | diff --git a/packages/api/README.md b/packages/api/README.md index 99ace01d7..2a3069ab9 100644 --- a/packages/api/README.md +++ b/packages/api/README.md @@ -146,13 +146,13 @@ So invalid arguments are a tool result with `isError` and an `invalid_input` pro **Server identity.** `serverInfo` carries the name and version the server passes in, the root package `@on.auto/runtime` and its release version from the manifest, and the title `Auto Runtime`, which a client shows in place of the name. -**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 through workflows, and runs belong to the playbook. +**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, registered definition types and available recipes. 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, say that `add_connection` adds a provider or a server by hand where it is listed, and direct an answer to an outstanding interaction back to the same run. Scoped connections and read-only callers receive only applicable instructions and recipes. `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. +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. **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. -**Guides.** Every endpoint serves `get_guide`, a read-only tool whose `guide` argument is the enum of the guides' names and whose answer is one guide's whole Markdown as text, and lists the same guides as resources, `guide://`, `text/markdown`, annotated for the assistant, which answer the same text. The server passes them as `shelf`, the `GuideShelf` `guideShelfOf(guides, recipes, definitionTypes)` builds once and checks against the bounds, from the guides, a `Guide` each with its `name`, `title`, `description` and `text`, the recipes and the definition types, and hands the same shelf to `openApiDocumentRoutes` and `guideRoutes`. A `Recipe` is a guide with the `arguments` its prompt takes, the `formatGuide` it embeds, the tools its steps `call` and the `request` it makes of the person's words. Each recipe whose steps call only tools the connection lists, the rule that names it in the instructions, is also a prompt, under its name and title, so a playbook endpoint offers no `first-playbook` and a key that may only read no prompt: `prompts/get` answers one user message, the request with the person's words filled in and then the recipe, followed by its format guide as an embedded resource. `guideShelfOf` refuses a shelf without the guide of each definition type and the format guide of each recipe, so no endpoint mounts without them. +**Guides.** Every endpoint serves `get_guide`, a read-only tool whose `guide` argument is the enum of the guides' names and whose answer is one guide's whole Markdown as text, and lists the same guides as resources, `guide://`, `text/markdown`, annotated for the assistant, which answer the same text. The server passes them as `shelf`, the `GuideShelf` `guideShelfOf(guides, recipes, definitionTypes)` builds once and checks against the bounds, from the guides, a `Guide` each with its `name`, `title`, `description` and `text`, the recipes and the definition types, and hands the same shelf to `openApiDocumentRoutes` and `guideRoutes`. A `Recipe` is a guide with the `arguments` its prompt takes, the `formatGuide` it embeds, the tools its steps `call` and the `request` it makes of the person's words. Each recipe whose steps call only tools the connection lists, the rule by which `get_guide`'s description names it, is also a prompt, under its name and title, so a playbook endpoint offers no `first-playbook` and a key that may only read no prompt: `prompts/get` answers one user message, the request with the person's words filled in and then the recipe, followed by its format guide as an embedded resource. `guideShelfOf` refuses a shelf without the guide of each definition type and the format guide of each recipe, so no endpoint mounts without them. **Bounds.** Each bound is enforced where it can be reached, and tested at the bound and one over in `src/bounds/served-bounds.test.ts`, `src/bounds/word-bounds.test.ts` and `src/tools/tool-definition.test.ts`: @@ -162,7 +162,7 @@ The instructions are composed from the tools the caller may use, registered defi | A tool description | 800 characters, 3 to 8 sentences | The server refuses to start | | An argument's description | 300 characters, at any depth of the input | The server refuses to start | | Tools on a connection | a limit of 32, `get_guide` included, which does not move when a tool is added; the server lists 28 on `/mcp`, 10 on the org endpoint and 20 on the playbook endpoint | The server refuses to start | -| The listing of a connection | 48,000 bytes on the wire on `/mcp`, 13,000 on the org endpoint and 40,000 on the playbook endpoint, the bound that binds, measured as `tools/list` answers under the 2025-11-25 revision (`src/tools/tool-listing.ts`); see [The cost of a listing](#the-cost-of-a-listing) | The server refuses to start | +| The listing of a connection | 48,000 bytes on the wire on `/mcp`, 13,100 on the org endpoint and 40,000 on the playbook endpoint, the bound that binds, measured as `tools/list` answers under the 2025-11-25 revision (`src/tools/tool-listing.ts`); see [The cost of a listing](#the-cost-of-a-listing) | The server refuses to start | | A guide | 64 KiB | The server refuses to start | | A recipe | 4 KiB, its format guide embedded beside it | The server refuses to start | | Guides | one a definition type and the terminology | The server refuses to start | @@ -175,11 +175,11 @@ 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,513 | 48,000 | -| `POST /orgs/{org}/mcp` | 10 | 12,970 | 13,000 | -| `POST /orgs/{org}/playbooks/{playbook}/mcp` | 20 | 30,767 | 40,000 | +| `POST /mcp` | 28 | 43,778 | 48,000 | +| `POST /orgs/{org}/mcp` | 10 | 13,019 | 13,100 | +| `POST /orgs/{org}/playbooks/{playbook}/mcp` | 20 | 30,932 | 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. 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. +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. **Which errors look how.** An MCP endpoint answers in three shapes: diff --git a/packages/api/src/bounds/listing-bounds.test.ts b/packages/api/src/bounds/listing-bounds.test.ts index 42ae47491..e51a7679b 100644 --- a/packages/api/src/bounds/listing-bounds.test.ts +++ b/packages/api/src/bounds/listing-bounds.test.ts @@ -110,7 +110,7 @@ const listings: readonly Listing[] = [ { endpoint: 'org', path: '/orgs/acme/mcp', - most: 13_000, + most: 13_100, servingWith: (extra) => ({ operations: orgToolsFilled(12, extra, longestArgument) }), }, { diff --git a/packages/api/src/bounds/served-bounds.test.ts b/packages/api/src/bounds/served-bounds.test.ts index d2db0fac4..1e9eb0f98 100644 --- a/packages/api/src/bounds/served-bounds.test.ts +++ b/packages/api/src/bounds/served-bounds.test.ts @@ -135,7 +135,7 @@ function workflowCalled(noun: string): DefinitionType { } function instructionLengthWith(noun: string): number { - return instructionsFor('own org', { orgTools: [], playbookTools: ['get_run'] }, [workflowCalled(noun)], []).length; + return instructionsFor('own org', { orgTools: [], playbookTools: ['get_run'] }, [workflowCalled(noun)]).length; } function servingTypeOf(noun: string): Serving { diff --git a/packages/api/src/bounds/served-bounds.ts b/packages/api/src/bounds/served-bounds.ts index 88f6c3bac..51c18038d 100644 --- a/packages/api/src/bounds/served-bounds.ts +++ b/packages/api/src/bounds/served-bounds.ts @@ -16,7 +16,7 @@ export const mostToolsOnAConnection = 32; export const mostListingBytes: Readonly> = { 'own org': 48_000, - org: 13_000, + org: 13_100, playbook: 40_000, }; diff --git a/packages/api/src/guides/guide-serving.ts b/packages/api/src/guides/guide-serving.ts index 8be0e3b90..fbe526829 100644 --- a/packages/api/src/guides/guide-serving.ts +++ b/packages/api/src/guides/guide-serving.ts @@ -8,7 +8,7 @@ import { serveRecipePrompts } from './recipe-prompts.ts'; export type GuideRegistry = Readonly>; export function serveGuides(server: GuideRegistry, shelf: GuideShelf, prompts: readonly ShelvedRecipe[]): void { - serveGuideTool(server, shelf); + serveGuideTool(server, shelf, prompts); serveGuideResources(server, shelf); serveRecipePrompts(server, prompts); } diff --git a/packages/api/src/guides/guide-tool.ts b/packages/api/src/guides/guide-tool.ts index 3674bed20..34bde01f9 100644 --- a/packages/api/src/guides/guide-tool.ts +++ b/packages/api/src/guides/guide-tool.ts @@ -5,7 +5,7 @@ import { Result, Schema, SchemaIssue, type SchemaAST } from 'effect'; import type { ToolDefinition } from '../tools/tool-definition.ts'; import { unsuccessfulResultOf } from '../tools/tool-result.ts'; import { advertisedSchema } from '../tools/tool-schema.ts'; -import type { GuideShelf } from './guide-shelf.ts'; +import type { Guide, GuideShelf } from './guide-shelf.ts'; export const guideToolName = 'get_guide'; @@ -15,6 +15,21 @@ export const guideToolDescription = [ '`guide` names one of them, and the answer is its whole text in Markdown.', ].join(' '); +type Followed = readonly Pick[]; + +function inTurn(items: readonly string[]): string { + return items.length < 2 ? items.join('') : `${items.slice(0, -1).join(', ')} and ${String(items.at(-1))}`; +} + +function descriptionFollowing(recipes: Followed): string { + const names = recipes.map(({ name }) => name); + if (names.length === 0) { + return guideToolDescription; + } + const held = names.length === 1 ? 'the recipe' : 'the recipes'; + return `${guideToolDescription} It holds ${held} ${inTurn(names)}, which the tools listed here can follow.`; +} + export const guideArgumentDescription = 'The guide to read: the terminology, the format of a definition type, or a recipe for a common task'; @@ -22,10 +37,10 @@ const strictly: SchemaAST.ParseOptions = { onExcessProperty: 'error', errors: 'a const failureOf = SchemaIssue.makeFormatterStandardSchemaV1(); -export function guideToolDefinitionOf({ everyGuide }: GuideShelf): ToolDefinition { +export function guideToolDefinitionOf({ everyGuide }: GuideShelf, followed: Followed): ToolDefinition { return { title: 'Get guide', - description: guideToolDescription, + description: descriptionFollowing(followed), inputSchema: advertisedSchema({ $schema: 'https://json-schema.org/draft/2020-12/schema', type: 'object', @@ -70,6 +85,10 @@ function guideReader({ named }: GuideShelf): (input: unknown) => CallToolResult }; } -export function serveGuideTool(server: Readonly>, shelf: GuideShelf): void { - server.registerTool(guideToolName, guideToolDefinitionOf(shelf), guideReader(shelf)); +export function serveGuideTool( + server: Readonly>, + shelf: GuideShelf, + followed: Followed, +): void { + server.registerTool(guideToolName, guideToolDefinitionOf(shelf, followed), guideReader(shelf)); } diff --git a/packages/api/src/guides/guides.test.ts b/packages/api/src/guides/guides.test.ts index fdf551712..31749bec1 100644 --- a/packages/api/src/guides/guides.test.ts +++ b/packages/api/src/guides/guides.test.ts @@ -1,7 +1,8 @@ import { Schema } from 'effect'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; -import { testGuides, testRecipes } from '../testing/guides.ts'; +import { recipesFollowedWith } from '../mcp/instructions.ts'; +import { testGuides, testRecipes, testShelf } from '../testing/guides.ts'; import { listenOnLoopback, type Listening } from '../testing/listening.ts'; import { mcpClientKinds, @@ -12,7 +13,9 @@ import { type McpSession, } from '../testing/mcp-clients.ts'; import { acmeAdmin, acmeReader, operationServer, type OperationServer } from '../testing/operation-server.ts'; +import { playbookEndpoint, recipes } from '../testing/served-instructions.ts'; import { guideToolName, listedTools } from '../testing/tool-listing.ts'; +import { guideToolDefinitionOf, guideToolDescription } from './guide-tool.ts'; let server: OperationServer; let listening: Listening; @@ -76,6 +79,36 @@ describe('the guide tool', () => { ); }); +function guideDescribedOn(path: string, key = acmeAdmin.key): Promise { + return on( + path, + async (session) => listedTools(await session.listTools()).find(({ name }) => name === guideToolName)?.description, + key, + ); +} + +describe('what the guide tool says of the recipes', () => { + const following = `${guideToolDescription} It holds the recipe take-a-note, which the tools listed here can follow.`; + + it('names the recipe the tools of /mcp and of the playbook endpoint can follow, and none on the org endpoint, which can follow none', async () => { + expect(await Promise.all(endpoints.map((path) => guideDescribedOn(path)))).toEqual([ + following, + guideToolDescription, + following, + ]); + }); + + it('names no recipe to a key that may only read, whose tools can follow none', async () => { + expect(await guideDescribedOn('/mcp', acmeReader.key)).toBe(guideToolDescription); + }); + + it('names in turn the recipes a playbook endpoint can follow', () => { + expect(guideToolDefinitionOf(testShelf, recipesFollowedWith(playbookEndpoint, recipes)).description).toBe( + `${guideToolDescription} It holds the recipes remember, give-tools and schedule, which the tools listed here can follow.`, + ); + }); +}); + describe('the guide tool, for any key', () => { it('is offered to a key that may only read', async () => { const tools = listedTools(await on('/mcp', (session) => session.listTools(), acmeReader.key)); diff --git a/packages/api/src/mcp/instructions.test.ts b/packages/api/src/mcp/instructions.test.ts index bfb111df8..2e9af5d04 100644 --- a/packages/api/src/mcp/instructions.test.ts +++ b/packages/api/src/mcp/instructions.test.ts @@ -10,37 +10,38 @@ import { orgEndpoint, ownOrg, queriesInsideAPlaybook, - recipes, + withConnections, } from '../testing/served-instructions.ts'; 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 through 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 asks a person and answers started. A computation function runs a program on its input and gives the same output every time. Recall retrieves retained 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 own notes. A prediction function answers from a model trained on the playbook's history; a build, the input { build: {} }, answers started. A workflow runs functions in steps, waits for input and can start on a schedule or on 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, which also holds the recipes: first-playbook, remember, give-tools and schedule. A reasoning function names a model that list_models lists and may name tools that search finds; execute calls one and shows its answer, and dry_run what it would check. 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. Runs, events and requests come in pages; read on only when the person needs more. 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, and an id or status only when 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. 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."; 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 through 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 asks a person and answers started. A computation function runs a program on its input and gives the same output every time. Recall retrieves retained 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 own notes. A prediction function answers from a model trained on the playbook's history; a build, the input { build: {} }, answers started. A workflow runs functions in steps, waits for input and can start on a schedule or on 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. 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, and an id or status only when 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 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 through 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 asks a person and answers started. A computation function runs a program on its input and gives the same output every time. Recall retrieves retained 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 own notes. A prediction function answers from a model trained on the playbook's history; a build, the input { build: {} }, answers started. A workflow runs functions in steps, waits for input and can start on a schedule or on an event. This connection acts inside one playbook. Before writing a definition, read its format with get_guide, which also holds the recipes: remember, give-tools and schedule. A reasoning function names a model this server can call, as the reasoning-function guide says, and may name tools that search finds; execute calls one and shows its answer, and dry_run what it would check. 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. Runs, events and requests come in pages; read on only when the person needs more. 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, and an id or status only when 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. 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."; const interactionSentence = - 'An interaction function asks a system and answers at once, or asks a person and answers started.'; + 'An interaction function asks a system and answers at once, or a person and answers started.'; const answeringSentence = 'When the person approves, rejects or otherwise answers what a run waits on, answer its request with answer_interaction, not a new run.'; -const recallSentence = 'Recall retrieves retained run results, definitions and events, never run inputs or tool calls.'; +const recallSentence = 'Recall retrieves run results, definitions and events, never run inputs or tool calls.'; const whatThePersonWantsKept = - 'What the person wants kept goes in a recall function, for anyone or a workflow to ask later, not your own notes.'; + 'What the person wants kept goes in a recall function, for anyone or a workflow to ask later, not your notes.'; const predictionSentence = - "A prediction function answers from a model trained on the playbook's history; a build, the input { build: {} }, answers started."; + "A prediction function answers from a model trained on the playbook's history; a build, input { build: {} }, answers started."; function asServedWithFourTypes(servedText: string): string { - return [interactionSentence, predictionSentence, answeringSentence] - .reduce((text, sentence) => text.replace(` ${sentence}`, ''), servedText) - .replace('Runs, events and requests come', 'Runs and events come'); + return [interactionSentence, predictionSentence, answeringSentence].reduce( + (text, sentence) => text.replace(` ${sentence}`, ''), + servedText, + ); } const typesAfterTheFirstFour: ReadonlySet = new Set(['interaction', 'prediction']); @@ -62,33 +63,31 @@ const resourcesOnTheTerminologyPage: ReadonlySet = new Set( describe('the instructions of each endpoint, for a key that may call every tool', () => { it.each([ - ['own org', ownOrg, servedOnMcp], - ['org', orgEndpoint, servedOnAnOrg], + ['own org', withConnections(ownOrg), servedOnMcp], + ['org', withConnections(orgEndpoint), servedOnAnOrg], ['playbook', playbookEndpoint, servedOnAPlaybook], ] as const)( 'read on the %s endpoint with a sentence per type the server runs and a sentence per tool it lists that needs one', (endpoint, served, servedText) => { - expect(instructionsFor(endpoint, served, definitionTypes, recipes)).toBe(servedText); + expect(instructionsFor(endpoint, served, definitionTypes)).toBe(servedText); }, ); it.each([ - ['own org', ownOrg, servedOnMcp], - ['org', orgEndpoint, servedOnAnOrg], + ['own org', withConnections(ownOrg), servedOnMcp], + ['org', withConnections(orgEndpoint), servedOnAnOrg], ['playbook', playbookEndpoint, servedOnAPlaybook], ] as const)( 'read on the %s endpoint of a server that runs neither interaction nor prediction functions without their sentences', (endpoint, served, servedText) => { const fourTypes = definitionTypes.filter(({ type }) => !typesAfterTheFirstFour.has(type)); - expect(instructionsFor(endpoint, withoutInteraction(served), fourTypes, recipes)).toBe( - asServedWithFourTypes(servedText), - ); + expect(instructionsFor(endpoint, withoutInteraction(served), fourTypes)).toBe(asServedWithFourTypes(servedText)); }, ); it.each(everyEndpoint)('stay under 2,000 characters on the %s endpoint', (endpoint, served) => { - expect(instructionsFor(endpoint, served, definitionTypes, recipes).length).toBeLessThan(2000); + expect(instructionsFor(endpoint, served, definitionTypes).length).toBeLessThan(2000); }); }); @@ -97,9 +96,7 @@ describe('what the instructions say of what the person wants kept', () => { expect( everyEndpoint.filter( ([endpoint, served]) => - !instructionsFor(endpoint, served, definitionTypes, recipes).includes( - `${recallSentence} ${whatThePersonWantsKept}`, - ), + !instructionsFor(endpoint, served, definitionTypes).includes(`${recallSentence} ${whatThePersonWantsKept}`), ), ).toEqual([]); }); @@ -109,14 +106,14 @@ describe('what the instructions say of what the person wants kept', () => { expect( everyEndpoint.filter(([endpoint, served]) => - instructionsFor(endpoint, served, withoutRecall, recipes).includes('wants kept'), + instructionsFor(endpoint, served, withoutRecall).includes('wants kept'), ), ).toEqual([]); }); }); describe('the instructions of a key that may only read', () => { - it('name no command, and point to the guides without the recipes the key could not follow', () => { + it('name no command, and point to the guides for what the words mean', () => { const reading = instructionsFor( 'own org', { @@ -124,7 +121,6 @@ describe('the instructions of a key that may only read', () => { playbookTools: queriesInsideAPlaybook, }, definitionTypes, - recipes, ); expect( @@ -134,7 +130,7 @@ describe('the instructions of a key that may only read', () => { 'run_definition', 'send_run_event', 'answer_interaction', - 'first-playbook', + 'add_connection', ].filter((name) => reading.includes(name)), ).toEqual([]); expect(reading).toContain("This connection acts in the caller's own org: list_playbooks shows its playbooks."); @@ -148,7 +144,6 @@ describe('instructions scoped to a playbook endpoint', () => { 'playbook', { orgTools: [], playbookTools: queriesInsideAPlaybook }, definitionTypes, - recipes, ); expect(reading).toContain( @@ -159,15 +154,13 @@ describe('instructions scoped to a playbook endpoint', () => { it('on an org endpoint that lists the models and no tool servers, say which models a reasoning function names', () => { const served = { orgTools: ['list_playbooks', 'list_models', 'get_guide'], playbookTools: [] }; - expect(instructionsFor('org', served, definitionTypes, recipes)).toContain( + expect(instructionsFor('org', served, definitionTypes)).toContain( 'list_models lists the models this server can call, which a reasoning function names. get_guide holds', ); }); it('on an org endpoint that only creates playbooks, say that it makes a playbook', () => { - expect( - instructionsFor('org', { orgTools: ['create_playbook'], playbookTools: [] }, definitionTypes, recipes), - ).toContain( + expect(instructionsFor('org', { orgTools: ['create_playbook'], playbookTools: [] }, definitionTypes)).toContain( "This connection manages the playbooks of one org: create_playbook makes a playbook, and a playbook's functions and workflows are made on the playbook's own connection, /orgs/{org}/playbooks/{playbook}/mcp.", ); }); @@ -177,11 +170,11 @@ describe('what the instructions say of a prediction function', () => { it('say, where the server runs one, that it answers a question and that its build answers started', () => { const predicting = definitionTypes.filter(({ type }) => type === 'prediction'); - expect(instructionsFor('playbook', playbookEndpoint, predicting, recipes)).toContain( - `Functions run directly or through workflows; runs belong to the playbook. ${predictionSentence} This connection acts inside one playbook.`, + expect(instructionsFor('playbook', playbookEndpoint, predicting)).toContain( + `Functions run directly or in workflows; runs belong to the playbook. ${predictionSentence} This connection acts inside one playbook.`, ); expect( - instructionsFor('playbook', playbookEndpoint, definitionTypes.slice(0, 1), recipes).includes(predictionSentence), + instructionsFor('playbook', playbookEndpoint, definitionTypes.slice(0, 1)).includes(predictionSentence), ).toBe(false); }); }); @@ -189,7 +182,7 @@ describe('what the instructions say of a prediction function', () => { describe('what the instructions say of the definition types', () => { it('give a sentence only to a type the server runs', () => { const reasoningAlone = definitionTypes.slice(0, 1); - const instructions = instructionsFor('playbook', playbookEndpoint, reasoningAlone, recipes); + const instructions = instructionsFor('playbook', playbookEndpoint, reasoningAlone); expect(instructions).toContain('A reasoning function has a prompt and calls a language model.'); expect( @@ -201,8 +194,8 @@ describe('what the instructions say of the definition types', () => { 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, recipes)).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 through workflows; runs belong to the playbook. This connection acts inside one playbook. Before writing a definition', + 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', ); }); @@ -212,34 +205,41 @@ describe('what the instructions say of the definition types', () => { expect( ['list_models', 'search'].filter((name) => [ - instructionsFor('own org', ownOrg, withoutReasoning, recipes), - instructionsFor('org', orgEndpoint, withoutReasoning, recipes), + instructionsFor('own org', ownOrg, withoutReasoning), + instructionsFor('org', orgEndpoint, withoutReasoning), ].some((instructions) => instructions.includes(name)), ), ).toEqual([]); }); +}); - it('point to get_guide without recipes where no recipe can be followed', () => { - expect(instructionsFor('playbook', playbookEndpoint, definitionTypes, [])).toContain( - 'Before writing a definition, read its format with get_guide. A reasoning function', - ); - }); +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", () => { + const writing = everyEndpoint.filter(([endpoint]) => endpoint !== 'org'); - it('name the one recipe that can be followed without a list', () => { - expect(instructionsFor('playbook', playbookEndpoint, definitionTypes, recipes.slice(1, 2))).toContain( - 'read its format with get_guide, which also holds the recipes: remember.', - ); + expect( + writing.filter( + ([endpoint, served]) => + !instructionsFor(endpoint, served, definitionTypes).includes( + 'Before writing a definition, read its format with get_guide. A reasoning function', + ), + ), + ).toEqual([]); + expect( + everyEndpoint.filter(([endpoint, served]) => + instructionsFor(endpoint, served, definitionTypes).includes('recipe'), + ), + ).toEqual([]); }); }); describe('what the instructions say of a run that waits', () => { it('say how to answer what a run waits on only where answer_interaction is listed', () => { - const answering = instructionsFor('playbook', playbookEndpoint, definitionTypes, recipes); + const answering = instructionsFor('playbook', playbookEndpoint, definitionTypes); const reading = instructionsFor( 'playbook', { orgTools: [], playbookTools: queriesInsideAPlaybook }, definitionTypes, - recipes, ); expect(answering).toContain(answeringSentence); @@ -251,7 +251,7 @@ describe('the instructions of every endpoint', () => { it('teaches the same ownership and lifecycle as the terminology page', () => { expect(terminology).toContain('A playbook contains one brain and its workflows.'); for (const [endpoint, served] of everyEndpoint) { - const words = instructionsFor(endpoint, served, definitionTypes, recipes); + const words = instructionsFor(endpoint, served, definitionTypes); expect(words).toContain('A playbook contains one brain (its reusable functions) and workflows.'); expect(words).toContain('The brain shares its lifecycle.'); expect(words).toContain('runs belong to the playbook'); @@ -264,10 +264,7 @@ describe('the instructions of every endpoint', () => { it('use no term of the internal vocabulary once the address of a playbook is taken out, and identifies the runtime', () => { const instructions = everyEndpoint.map(([endpoint, served]) => - instructionsFor(endpoint, served, definitionTypes, recipes).replace( - ', /orgs/{org}/playbooks/{playbook}/mcp.', - '.', - ), + instructionsFor(endpoint, served, definitionTypes).replace(', /orgs/{org}/playbooks/{playbook}/mcp.', '.'), ); expect(instructions.flatMap((text) => internalTermsIn(text))).toEqual([]); diff --git a/packages/api/src/mcp/instructions.ts b/packages/api/src/mcp/instructions.ts index 1de48ac23..dfded2309 100644 --- a/packages/api/src/mcp/instructions.ts +++ b/packages/api/src/mcp/instructions.ts @@ -23,7 +23,6 @@ interface Serving { readonly listed: (name: string) => boolean; readonly definitionTypes: readonly DefinitionType[]; readonly reasoning: DefinitionType | undefined; - readonly recipes: readonly RecipeCalls[]; } type Sentences = (serving: Serving) => readonly string[]; @@ -32,36 +31,28 @@ const reasoningFunctionType = 'reasoning'; const purposesByType: Readonly> = { reasoning: ['A reasoning function has a prompt and calls a language model.'], - interaction: ['An interaction function asks a system and answers at once, or asks a person and answers started.'], - computation: ['A computation function runs a program on its input and gives the same output every time.'], + interaction: ['An interaction function asks a system and answers at once, or a person and answers started.'], + computation: ['A computation function runs a program whose output is the same for the same input.'], recall: [ - 'Recall retrieves retained 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 own notes.', + '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.', ], prediction: [ - "A prediction function answers from a model trained on the playbook's history; a build, the input { build: {} }, answers started.", + "A prediction function answers from a model trained on the playbook's history; a build, input { build: {} }, answers started.", ], - workflow: ['A workflow runs functions in steps, waits for input and can start on a schedule or on an event.'], + workflow: ['A workflow runs functions in steps, waits for input and can start on a schedule or an event.'], }; export const whatAPlaybookIs = [ '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 through workflows; runs belong to the playbook.', + '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.', ]; -const pagedReads = ['list_runs', 'list_playbook_events', 'list_interactions']; - -const howToAnswer = [ - '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, and an id or status only when needed to act.", -].join(' '); +const howToAnswer = + "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."; const whenAToolCannot = 'A tool that cannot do what was asked says why and what to change.'; -function inTurn(items: readonly string[]): string { - return items.length < 2 ? items.join('') : `${items.slice(0, -1).join(', ')} and ${String(items.at(-1))}`; -} - function playbooksClause({ listed }: Serving, shows: string): readonly string[] { const shown = listed('list_playbooks') ? [`list_playbooks shows ${shows}`] : []; const made = listed('create_playbook') ? [`create_playbook makes ${shown.length === 0 ? 'a playbook' : 'one'}`] : []; @@ -93,32 +84,17 @@ const modelsBeforeWriting: Sentences = ({ reasoning, listed }) => ? ['list_models lists the models this server can call, which a reasoning function names.'] : []; -const guides: Sentences = ({ listed, recipes }) => { - if (!listed('create_definition')) { - return ['get_guide holds what these words mean and how each kind of definition is written.']; - } - const served = recipes.map(({ name }) => name); - const reading = 'Before writing a definition, read its format with get_guide'; - return [served.length === 0 ? `${reading}.` : `${reading}, which also holds the recipes: ${inTurn(served)}.`]; -}; - -function callingWords(listed: Serving['listed']): string { - const executing = listed('execute'); - const dryRunning = listed('dry_run'); - if (executing && dryRunning) { - return '; execute calls one and shows its answer, and dry_run what it would check'; - } - if (executing) { - return '; execute calls one and shows its answer'; - } - return dryRunning ? '; dry_run shows what a call of one would check' : ''; -} +const guides: Sentences = ({ listed }) => [ + listed('create_definition') + ? 'Before writing a definition, read its format with get_guide.' + : 'get_guide holds what these words mean and how each kind of definition is written.', +]; const modelsAndTools: Sentences = ({ reasoning, listed }) => { if (reasoning === undefined || !listed('search')) { return []; } - const tools = `may name tools that search finds${callingWords(listed)}`; + const tools = 'may name tools that search finds'; return [ listed('list_models') ? `A reasoning function names a model that list_models lists and ${tools}.` @@ -126,8 +102,12 @@ const modelsAndTools: Sentences = ({ reasoning, listed }) => { ]; }; -const connections: Sentences = ({ listed }) => - listed('add_connection') ? ['add_connection adds a provider, or a server by hand; a person approves.'] : []; +const connectingAService = [ + "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.', +]; + +const connections: Sentences = ({ listed }) => (listed('search') && listed('add_connection') ? connectingAService : []); const typesThatFinishLater: ReadonlySet = new Set(['workflow']); @@ -147,13 +127,6 @@ const answeringWhatRunsWaitOn: Sentences = ({ listed }) => ] : []; -const paging: Sentences = ({ listed }) => { - const paged = listed('list_interactions') ? 'Runs, events and requests' : 'Runs and events'; - return pagedReads.some((name) => listed(name)) - ? [`${paged} come in pages; read on only when the person needs more.`] - : []; -}; - const closing: Sentences = () => [howToAnswer, whenAToolCannot]; const orientation: readonly Sentences[] = [ @@ -165,7 +138,6 @@ const orientation: readonly Sentences[] = [ connections, runsThatFinishLater, answeringWhatRunsWaitOn, - paging, closing, ]; @@ -185,7 +157,6 @@ export function instructionsFor( endpoint: McpEndpoint, tools: ServedTools, definitionTypes: readonly DefinitionType[], - recipes: readonly RecipeCalls[], ): string { const names = namesOf(tools); const serving: Serving = { @@ -193,7 +164,6 @@ export function instructionsFor( listed: (name) => names.includes(name), definitionTypes, reasoning: definitionTypes.find(({ type }) => type === reasoningFunctionType), - recipes: recipesFollowedWith(tools, recipes), }; return [...whatAPlaybookIs, ...orientation.flatMap((sentences) => sentences(serving))].join(' '); } diff --git a/packages/api/src/mcp/mcp-clients.test.ts b/packages/api/src/mcp/mcp-clients.test.ts index 4aad82737..e44110f9f 100644 --- a/packages/api/src/mcp/mcp-clients.test.ts +++ b/packages/api/src/mcp/mcp-clients.test.ts @@ -3,7 +3,7 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { instructionsFor, type RegisterRoutes } from '../index.ts'; import { createTestHandler } from '../testing/api-calls.ts'; -import { testDefinitionTypes, testRecipes } from '../testing/guides.ts'; +import { testDefinitionTypes } from '../testing/guides.ts'; import { listenOnLoopback, type Listening } from '../testing/listening.ts'; import { expectedProtocolVersion, @@ -93,7 +93,7 @@ describe.each(mcpClientKinds)('the %s client connecting to a playbook endpoint', expect(connected).toEqual({ protocolVersion: expectedProtocolVersion[kind], serverVersion: { ...testServerInfo, title: 'Auto Runtime' }, - instructions: instructionsFor('playbook', { orgTools: [], playbookTools }, testDefinitionTypes, testRecipes), + instructions: instructionsFor('playbook', { orgTools: [], playbookTools }, testDefinitionTypes), }); }); diff --git a/packages/api/src/mcp/mcp-connection.ts b/packages/api/src/mcp/mcp-connection.ts index 01e09f3f9..49eb64165 100644 --- a/packages/api/src/mcp/mcp-connection.ts +++ b/packages/api/src/mcp/mcp-connection.ts @@ -40,7 +40,7 @@ const noChangesAnnounced = { listChanged: false }; export const serverTitle = 'Auto Runtime'; function instructionsOf({ endpoint, shelf }: Connection, served: ServedTools): string { - return instructionsFor(endpoint, served, shelf.definitionTypes, shelf.recipes); + return instructionsFor(endpoint, served, shelf.definitionTypes); } export interface EveryTool { @@ -54,7 +54,8 @@ export function connectionOf(endpoint: McpEndpoint, serving: McpServing, { serve serving, shelf: serving.shelf, }; - const listing = [...listed, { name: guideToolName, definition: guideToolDefinitionOf(connection.shelf) }]; + const guideTool = guideToolDefinitionOf(connection.shelf, recipesFollowedWith(served, connection.shelf.recipes)); + const listing = [...listed, { name: guideToolName, definition: guideTool }]; requireWithin(`The ${endpoint} endpoint`, listing.length, mostToolsOnAConnection, 'tools'); requireWithin( `The listing of the ${endpoint} endpoint`, diff --git a/packages/api/src/mcp/mcp-own-org.test.ts b/packages/api/src/mcp/mcp-own-org.test.ts index 695a7d332..91052b24f 100644 --- a/packages/api/src/mcp/mcp-own-org.test.ts +++ b/packages/api/src/mcp/mcp-own-org.test.ts @@ -59,8 +59,10 @@ function listingOn(path: string): Promise { ); } +const followingTheNoteRecipe = ' It holds the recipe take-a-note, which the tools listed here can follow.'; + describe('the tools of /mcp', () => { - it('are the tools of the org endpoint as they are, then those of a playbook endpoint taking a playbook', async () => { + it('are the tools of the org endpoint as they are, then those of a playbook endpoint taking a playbook, and get_guide as the playbook endpoint lists it', async () => { const [own, org, playbook] = await Promise.all([ listingOn('/mcp'), listingOn('/orgs/acme/mcp'), @@ -75,7 +77,11 @@ describe('the tools of /mcp', () => { ...playbook.filter(({ name }) => name !== guideToolName).map((tool) => takingPlaybook(tool)), guide, ]); - expect([org.at(-1), playbook.at(-1)]).toEqual([guide, guide]); + expect(guide?.description).toContain(followingTheNoteRecipe); + expect([org.at(-1), playbook.at(-1)]).toEqual([ + { ...guide, description: guide?.description?.replace(followingTheNoteRecipe, '') }, + guide, + ]); }); it('have self-contained input schemas with an object root, and no output schema', async () => { @@ -91,12 +97,7 @@ describe('the tools of /mcp', () => { const instructions = await asKey(acmeAdmin.key, (session) => Promise.resolve(session.instructions)); expect(instructions).toBe( - instructionsFor( - 'own org', - { orgTools, playbookTools }, - [{ type: 'notes', noun: 'note', guide: 'notebook' }], - [{ name: 'take-a-note', calls: ['add_note'] }], - ), + instructionsFor('own org', { orgTools, playbookTools }, [{ type: 'notes', noun: 'note', guide: 'notebook' }]), ); expect(instructions).toContain("This connection acts in the caller's own org."); }); diff --git a/packages/api/src/mcp/tool-calling-instructions.test.ts b/packages/api/src/mcp/tool-calling-instructions.test.ts index cfd89fb43..6bb089d8f 100644 --- a/packages/api/src/mcp/tool-calling-instructions.test.ts +++ b/packages/api/src/mcp/tool-calling-instructions.test.ts @@ -7,16 +7,15 @@ import { orgEndpoint, ownOrg, queriesInsideAPlaybook, - recipes, withConnections, } from '../testing/served-instructions.ts'; import { instructionsFor } from './instructions.ts'; describe('the length of the instructions with every tool', () => { - it('take 1,997 characters on /mcp, 1,680 on the org endpoint and 1,876 on the playbook endpoint', () => { + it('take 1,993 characters on /mcp, 1,912 on the org endpoint and 1,618 on the playbook endpoint', () => { expect( - everyEndpoint.map(([endpoint, served]) => instructionsFor(endpoint, served, definitionTypes, recipes).length), - ).toEqual([1680, 1876, 1997]); + everyEndpoint.map(([endpoint, served]) => instructionsFor(endpoint, served, definitionTypes).length), + ).toEqual([1912, 1618, 1993]); }); }); @@ -26,65 +25,63 @@ describe('what the instructions say of calling a tool', () => { playbookTools: queriesInsideAPlaybook, }; - it('say what execute and dry_run do in the sentence on tools, where they are listed, and keep the closing sentence', () => { - const sentence = - 'may name tools that search finds; execute calls one and shows its answer, and dry_run what it would check.'; - - expect(instructionsFor('own org', ownOrg, definitionTypes, recipes)).toContain( - `A reasoning function names a model that list_models lists and ${sentence}`, + it('end the sentence on tools at what search finds, leaving execute and dry_run to their own descriptions, and keep the closing sentence', () => { + expect(instructionsFor('own org', ownOrg, definitionTypes)).toContain( + 'A reasoning function names a model that list_models lists and may name tools that search finds. A run of a workflow', ); - expect(instructionsFor('playbook', playbookEndpoint, definitionTypes, recipes)).toContain( - `A reasoning function names a model this server can call, as the reasoning-function guide says, and ${sentence}`, + expect(instructionsFor('playbook', playbookEndpoint, definitionTypes)).toContain( + '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', ); + expect( + everyEndpoint.filter(([endpoint, served]) => + /\b(?:execute|dry_run)\b/u.test(instructionsFor(endpoint, served, definitionTypes)), + ), + ).toEqual([]); expect( everyEndpoint.filter( ([endpoint, served]) => - !instructionsFor(endpoint, served, definitionTypes, recipes).endsWith( + !instructionsFor(endpoint, served, definitionTypes).endsWith( 'A tool that cannot do what was asked says why and what to change.', ), ), ).toEqual([]); }); - it('name dry_run alone for a key that may only read, whose instructions keep within their bound', () => { + it('keep within their bound for a key that may only read', () => { const lengths = [ - instructionsFor('own org', reading, definitionTypes, recipes), - instructionsFor( - 'own org', - { ...reading, orgTools: ['list_playbooks', 'search', 'get_guide'] }, - definitionTypes, - recipes, - ), - instructionsFor('org', { orgTools: reading.orgTools, playbookTools: [] }, definitionTypes, recipes), - instructionsFor('playbook', { orgTools: [], playbookTools: queriesInsideAPlaybook }, definitionTypes, recipes), + instructionsFor('own org', reading, definitionTypes), + instructionsFor('own org', { ...reading, orgTools: ['list_playbooks', 'search', 'get_guide'] }, definitionTypes), + instructionsFor('org', { orgTools: reading.orgTools, playbookTools: [] }, definitionTypes), + instructionsFor('playbook', { orgTools: [], playbookTools: queriesInsideAPlaybook }, definitionTypes), ]; - expect(lengths.filter((instructions) => /\bexecute\b/u.test(instructions))).toEqual([]); - expect(lengths[0]).toContain('may name tools that search finds; dry_run shows what a call of one would check.'); - expect(lengths.map(({ length }) => length)).toEqual([1673, 1710, 1578, 1670]); + expect(lengths.map(({ length }) => length)).toEqual([1507, 1544, 1540, 1504]); }); }); -describe('what the instructions say of calling a tool, to a key that may only write', () => { - it('name execute alone for a key that may change a playbook but not read it', () => { - const writing = { orgTools: ['list_models', 'search'], playbookTools: ['execute'] }; +describe('the instructions of an endpoint that lists search and add_connection', () => { + const connecting = [ + "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.', + ].join(' '); - expect(instructionsFor('own org', writing, definitionTypes, [])).toContain( - 'may name tools that search finds; execute calls one and shows its answer.', + it('say after the sentence on tools that a service is connected through search and add_connection, on /mcp and the org endpoint', () => { + expect(instructionsFor('own org', withConnections(ownOrg), definitionTypes)).toContain( + `may name tools that search finds. ${connecting} A run of a workflow`, + ); + expect(instructionsFor('org', withConnections(orgEndpoint), definitionTypes)).toContain( + `may name tools that search finds. ${connecting} Say what was done`, ); }); -}); - -describe('the instructions of an endpoint that lists add_connection', () => { - const connectionSentence = 'add_connection adds a provider, or a server by hand; a person approves.'; - it('say what a connection is after the sentence on tools, and an endpoint that does not list it does not', () => { - const listing = instructionsFor('own org', withConnections(ownOrg), definitionTypes, recipes); + it('say nothing of connecting a service where search or add_connection is not listed', () => { + const notConnecting = [ + instructionsFor('own org', ownOrg, definitionTypes), + instructionsFor('org', orgEndpoint, definitionTypes), + instructionsFor('playbook', playbookEndpoint, definitionTypes), + instructionsFor('org', { orgTools: ['add_connection', 'list_connections'], playbookTools: [] }, definitionTypes), + ]; - expect(listing).toContain(`and dry_run what it would check. ${connectionSentence} A run of a workflow`); - expect(instructionsFor('org', withConnections(orgEndpoint), definitionTypes, recipes)).toContain( - `may name tools that search finds. ${connectionSentence} Say what was done`, - ); - expect(instructionsFor('own org', ownOrg, definitionTypes, recipes)).not.toContain('add_connection'); + expect(notConnecting.filter((instructions) => /connected|add_connection/u.test(instructions))).toEqual([]); }); }); diff --git a/packages/connections/src/operations/add-connection.test.ts b/packages/connections/src/operations/add-connection.test.ts index e9d8b2cc6..0610a91d7 100644 --- a/packages/connections/src/operations/add-connection.test.ts +++ b/packages/connections/src/operations/add-connection.test.ts @@ -178,6 +178,18 @@ describe('the name of a connection', () => { }); }); +describe('the description of adding a connection', () => { + it("opens with a person's account at a service, named by a provider search lists, and keeps a server given by hand and the page the person finishes on", () => { + expect(operationNamed(harness().operations, 'add_connection').registration.description).toBe( + [ + "Starts connecting a person's account at a service, named by a provider that search lists, by its name and placeholders, answered pending with a link to Studio's page.", + 'A server given by hand takes name, type, entry and credential instead, none holding a secret.', + 'The person approves it, and gives its credential, on the page the link opens within ten minutes.', + ].join(' '), + ); + }); +}); + describe('the permission to add a connection', () => { it('is org:write', async () => { expect(await adding(harness(), crm, acmeReader)).toMatchObject({ status: 'rejected', reason: 'forbidden' }); diff --git a/packages/connections/src/operations/add-connection.ts b/packages/connections/src/operations/add-connection.ts index 3870f4c55..b8a65544a 100644 --- a/packages/connections/src/operations/add-connection.ts +++ b/packages/connections/src/operations/add-connection.ts @@ -11,7 +11,7 @@ import { connectionNamed, linkWords } from './connection-words.ts'; import { additionOf, recordInRegistry } from './registry-access.ts'; const description = [ - "Connects a service the org's functions should reach: a provider search lists, by its name and placeholders, answered pending with a link to Studio's page.", + "Starts connecting a person's account at a service, named by a provider that search lists, by its name and placeholders, answered pending with a link to Studio's page.", 'A server given by hand takes name, type, entry and credential instead, none holding a secret.', 'The person approves it, and gives its credential, on the page the link opens within ten minutes.', ].join(' '); diff --git a/packages/definitions/src/operations/definition-descriptions.test.ts b/packages/definitions/src/operations/definition-descriptions.test.ts index 34c5d75d9..233212a69 100644 --- a/packages/definitions/src/operations/definition-descriptions.test.ts +++ b/packages/definitions/src/operations/definition-descriptions.test.ts @@ -102,6 +102,14 @@ describe('the JSON Schema of the input of the operations', () => { }); }); +describe('the description of list_runs', () => { + it('says that its pages are read on only as the person needs', () => { + expect(described('list_runs')).toContain( + '`cursor` is the next_cursor of the page before, to read on only as the person needs.', + ); + }); +}); + describe('the JSON Schema of the filters of the runs', () => { it('describes the runs a filter keeps', () => { const listing = operations.find(({ name }) => name === 'list_runs'); diff --git a/packages/definitions/src/reading/list-runs.ts b/packages/definitions/src/reading/list-runs.ts index 8fe1a78b1..d55160532 100644 --- a/packages/definitions/src/reading/list-runs.ts +++ b/packages/definitions/src/reading/list-runs.ts @@ -23,7 +23,7 @@ const description = [ 'Use it to find a run the person means, such as the runs of a scheduled workflow; get_run reads one run in full.', '`type` and `name` keep the runs of one definition and `status` those in one status.', `A filtered page looks at up to ${mostExaminedInAPage} runs, so it may hold fewer runs than \`limit\`, or none, while has_more is true.`, - '`cursor` is the next_cursor of the page before.', + '`cursor` is the next_cursor of the page before, to read on only as the person needs.', ].join(' '); function listRunsInputOf(typeField: KnownCapabilities['field']) { diff --git a/packages/playbooks/src/feed/list-playbook-events.test.ts b/packages/playbooks/src/feed/list-playbook-events.test.ts index 3bff16137..24ae77eb7 100644 --- a/packages/playbooks/src/feed/list-playbook-events.test.ts +++ b/packages/playbooks/src/feed/list-playbook-events.test.ts @@ -34,6 +34,12 @@ describe('list_playbook_events', () => { ); }); + it('says that its pages are read on only as the person needs', () => { + expect(listPlaybookEvents.registration.description).toContain( + 'and `cursor` is the next_cursor of the page before, to read on only as the person needs.', + ); + }); + it('needs a presenter that shows at least one type of event', () => { expect(() => defineListPlaybookEvents([presenterOf('notes', { added: null })])).toThrow( 'The events of a playbook need a presenter that shows at least one type of event', diff --git a/packages/playbooks/src/feed/list-playbook-events.ts b/packages/playbooks/src/feed/list-playbook-events.ts index 39fbbaba5..823d1f2ac 100644 --- a/packages/playbooks/src/feed/list-playbook-events.ts +++ b/packages/playbooks/src/feed/list-playbook-events.ts @@ -25,7 +25,7 @@ const description = [ 'its fact as data with a large field as its size, and its metadata; get_event reads one event whole.', "Use it to follow the playbook's activity, to see what one run did, or to find the events a recall function or a workflow's schedule can take.", '`type` keeps one type of event, `run_id` one run and every run it started, `since` what was recorded from that time on,', - 'and `cursor` is the next_cursor of the page before.', + 'and `cursor` is the next_cursor of the page before, to read on only as the person needs.', "The playbook's own creation, update and retirement are not among the events; list_playbooks shows them.", ].join(' '); diff --git a/packages/server/src/mcp/answering-what-runs-wait-on.test.ts b/packages/server/src/mcp/answering-what-runs-wait-on.test.ts index f3fac6524..8767f0c14 100644 --- a/packages/server/src/mcp/answering-what-runs-wait-on.test.ts +++ b/packages/server/src/mcp/answering-what-runs-wait-on.test.ts @@ -194,11 +194,11 @@ describe('the shape of the answer an open request takes, as the agent reads it', const description = descriptionIn(meetings.surfaces, 'list_interactions'); expect(sentenceNaming(description, 'answer_schema')).toBe( - 'Each carries its `answer_schema`, the shape answer_interaction checks an answer against, as recorded when it was asked, which get_definition may no longer show; null for a notification.', + 'Each carries its `answer_schema`, the shape answer_interaction checks an answer against, as recorded when asked, which get_definition may no longer show; null for a notification.', ); expect(descriptionIn(meetings.surfaces, 'answer_interaction')).toContain( "`answer` takes the shape of the request's answer_schema, which list_interactions shows,", ); - expect([description.length, sentencesOf(description).length]).toEqual([765, 5]); + expect([description.length, sentencesOf(description).length]).toEqual([795, 5]); }); }); diff --git a/packages/server/src/mcp/asking-in-a-chat.test.ts b/packages/server/src/mcp/asking-in-a-chat.test.ts index f48f58bb8..57d8f5478 100644 --- a/packages/server/src/mcp/asking-in-a-chat.test.ts +++ b/packages/server/src/mcp/asking-in-a-chat.test.ts @@ -120,7 +120,7 @@ describe('episode 9: asked to send the person a draft in the chat and take their const seen = await meetings.onMcp(askedToSendADraft); expect(sentenceNaming(meetings.surfaces.instructions, 'An interaction function')).toBe( - 'An interaction function asks a system and answers at once, or asks a person and answers started.', + 'An interaction function asks a system and answers at once, or a person and answers started.', ); expect([seen.sent.structuredContent, seen.read.structuredContent]).toMatchObject([ { outcome: 'result', text: namingTheMessage }, @@ -184,7 +184,7 @@ describe('episode 10: asked to put the replies of a thread into the weekly summa const seen = await meetings.onMcp(askedForTheRepliesOfAThread); expect(sentenceNaming(meetings.surfaces.instructions, 'An interaction function')).toBe( - 'An interaction function asks a system and answers at once, or asks a person and answers started.', + 'An interaction function asks a system and answers at once, or a person and answers started.', ); expect(seen.recipe).toContain( 'When the playbook only needs what a tool answers, with nothing to reason about, write an interaction function that calls the tool instead', diff --git a/packages/server/src/mcp/mcp-endpoint.test.ts b/packages/server/src/mcp/mcp-endpoint.test.ts index f0b8597e6..d1460bdca 100644 --- a/packages/server/src/mcp/mcp-endpoint.test.ts +++ b/packages/server/src/mcp/mcp-endpoint.test.ts @@ -127,13 +127,6 @@ const definitionTypes = [ { type: 'workflow', noun: 'workflow', guide: 'workflow' }, ]; -const recipes = [ - { name: 'first-playbook', calls: ['create_playbook', 'create_definition', 'run_definition'] }, - { name: 'remember', calls: ['create_definition', 'run_definition'] }, - { name: 'give-tools', calls: ['search', 'create_definition'] }, - { name: 'schedule', calls: ['create_definition', 'list_runs'] }, -]; - const terminology = readFileSync(new URL('../../../../docs/concepts/terminology.md', import.meta.url), 'utf8'); const resourcesOnTheTerminologyPage: ReadonlySet = new Set( @@ -183,7 +176,7 @@ describe('the instructions an agent receives when it connects', () => { const instructions = await onMcp(path, (session) => Promise.resolve(session.instructions)); - expect(instructions).toBe(instructionsFor(endpoint, served, definitionTypes, recipes)); + expect(instructions).toBe(instructionsFor(endpoint, served, definitionTypes)); expect(instructions).toContain(sentence); expect(unnamed.filter((name) => instructions?.includes(name) === true)).toEqual([]); }, diff --git a/packages/server/src/mcp/served-schemas.test.ts b/packages/server/src/mcp/served-schemas.test.ts index 5af8a0cd5..afc3dd263 100644 --- a/packages/server/src/mcp/served-schemas.test.ts +++ b/packages/server/src/mcp/served-schemas.test.ts @@ -87,9 +87,9 @@ interface Endpoint { } const endpoints: readonly Endpoint[] = [ - { path: '/mcp', tools: 28, bytes: 43_513, mostBytes: mostListingBytes['own org'] }, - { path: '/orgs/local/mcp', tools: 10, bytes: 12_970, mostBytes: mostListingBytes.org }, - { path: '/orgs/local/playbooks/alpha/mcp', tools: 20, bytes: 30_767, mostBytes: mostListingBytes.playbook }, + { path: '/mcp', tools: 28, bytes: 43_778, mostBytes: mostListingBytes['own org'] }, + { path: '/orgs/local/mcp', tools: 10, bytes: 13_019, mostBytes: mostListingBytes.org }, + { path: '/orgs/local/playbooks/alpha/mcp', tools: 20, bytes: 30_932, mostBytes: mostListingBytes.playbook }, ]; describe('the tools of each endpoint', () => { diff --git a/packages/server/src/mcp/served-tools.test.ts b/packages/server/src/mcp/served-tools.test.ts index 09ff29d02..ba4f41a5e 100644 --- a/packages/server/src/mcp/served-tools.test.ts +++ b/packages/server/src/mcp/served-tools.test.ts @@ -128,12 +128,39 @@ describe('the descriptions of the tools of /mcp', () => { }); describe('the descriptions of the tools that find and connect a provider', () => { - it('name providers in search, at 761 of its 800 characters, and say in add_connection that search lists them', () => { - const [searching, adding] = [descriptionOf('search'), descriptionOf('add_connection')]; + it('name in search the providers that match a service and no server serves yet, at 798 of its 800 characters', () => { + const searching = descriptionOf('search'); - expect(searching).toContain('`providers` lists what a person may connect that no server serves yet.'); - expect(adding).toContain('a provider search lists, by its name and placeholders'); - expect([searching.length, adding.length]).toEqual([761, 345]); + expect(searching).toContain( + 'With `terms` naming a service, `providers` lists what a person may connect that no server serves yet, found as tools are, for add_connection by name.', + ); + expect(searching).toHaveLength(798); + }); + + it("open add_connection with a person's account at a service that search lists, at 357 characters", () => { + const adding = descriptionOf('add_connection'); + + expect(adding).toMatch( + /^Starts connecting a person's account at a service, named by a provider that search lists, /u, + ); + expect(adding).toHaveLength(357); + }); +}); + +describe('the descriptions that say what the instructions leave to them', () => { + it('name in get_guide the recipes the tools of /mcp can follow', () => { + expect(descriptionOf('get_guide')).toContain( + 'It holds the recipes first-playbook, remember, give-tools and schedule, which the tools listed here can follow.', + ); + }); + + it('say in each paged tool that its pages are read on only as the person needs', () => { + expect( + ['list_runs', 'list_playbook_events', 'list_interactions'].filter( + (name) => + !descriptionOf(name).includes('the next_cursor of the page before, to read on only as the person needs.'), + ), + ).toEqual([]); }); }); diff --git a/packages/server/src/mcp/speaking-to-agents.test.ts b/packages/server/src/mcp/speaking-to-agents.test.ts index 8deafb529..11fa5449f 100644 --- a/packages/server/src/mcp/speaking-to-agents.test.ts +++ b/packages/server/src/mcp/speaking-to-agents.test.ts @@ -82,7 +82,7 @@ describe('episode 3: asked to use Slack', () => { const listed = await onMcp((session) => session.callTool('search', { playbook: 'meetings' })); expect(sentenceNaming(surfaces.instructions, 'search finds')).toBe( - 'A reasoning function names a model that list_models lists and may name tools that search finds; execute calls one and shows its answer, and dry_run what it would check.', + 'A reasoning function names a model that list_models lists and may name tools that search finds.', ); expect(plainTextIn(listed)).toMatch(/^This playbook's functions may use 2 tool servers\. /u); expect(plainTextIn(listed)).toContain('“slack” offers'); @@ -244,11 +244,14 @@ async function lookedAtOverMcp(session: McpSession) { } describe('episode 8: asked what a gateway-style tool server offers, and then to post to #random', () => { - it('finds the clause in the instructions, finds the search tool by its words, calls it, and makes no function to look', async () => { + it('finds search in the instructions and what execute answers in its description, finds the search tool by its words, calls it, and makes no function to look', async () => { const seen = await onMcp(lookedAtOverMcp); - expect(sentenceNaming(surfaces.instructions, 'search finds', 'execute')).toBe( - 'A reasoning function names a model that list_models lists and may name tools that search finds; execute calls one and shows its answer, and dry_run what it would check.', + expect(sentenceNaming(surfaces.instructions, 'search finds')).toBe( + 'A reasoning function names a model that list_models lists and may name tools that search finds.', + ); + expect(descriptionOf('execute')).toContain( + "as a run of a reasoning function would call it, and answers what that run's model would see.", ); expect(seen.found.structuredContent).toMatchObject({ servers: [{ name: 'slack' }], diff --git a/packages/server/src/mcp/tool-servers-without-a-playbook.test.ts b/packages/server/src/mcp/tool-servers-without-a-playbook.test.ts index 3a14b8149..4d9cb2ed2 100644 --- a/packages/server/src/mcp/tool-servers-without-a-playbook.test.ts +++ b/packages/server/src/mcp/tool-servers-without-a-playbook.test.ts @@ -58,7 +58,7 @@ describe('episode 9: asked for the tool servers on the connection of the whole o expect(required).not.toContain('playbook'); expect(properties.playbook.description).toContain('without it, every tool server of the org'); expect(sentenceNaming(descriptionIn(meetings.surfaces, 'search'), 'Without `playbook`')).toBe( - 'Without `playbook` it searches the tool servers of the whole org, naming the playbooks each serves.', + 'Without `playbook` it searches every tool server of the org, with the playbooks each serves.', ); }); diff --git a/packages/server/src/recall/what-the-person-wants-kept.test.ts b/packages/server/src/recall/what-the-person-wants-kept.test.ts index 69b10cd68..7cea95dce 100644 --- a/packages/server/src/recall/what-the-person-wants-kept.test.ts +++ b/packages/server/src/recall/what-the-person-wants-kept.test.ts @@ -79,7 +79,7 @@ describe('episode 11: asked to keep the revenue of each month, to ask later for })); expect(sentenceNaming(instructions, 'wants kept')).toBe( - 'What the person wants kept goes in a recall function, for anyone or a workflow to ask later, not your own notes.', + 'What the person wants kept goes in a recall function, for anyone or a workflow to ask later, not your notes.', ); expect(String(prompt.messages[0]?.content.text)).toContain( "3. For what the person tells you, publish it to the playbook with publish_event once they agree, one event for each fact, such as each month's figure:", diff --git a/packages/tool-servers/src/search/search-in-org.test.ts b/packages/tool-servers/src/search/search-in-org.test.ts index 9467ee8f1..d478ede2c 100644 --- a/packages/tool-servers/src/search/search-in-org.test.ts +++ b/packages/tool-servers/src/search/search-in-org.test.ts @@ -100,7 +100,7 @@ describe('search of the org', () => { mayChangeOutside: false, }); expect(registration.description).toMatch( - /^Searches the tools of the tool servers this playbook's functions may use, .* Without `playbook` it searches the tool servers of the whole org, naming the playbooks each serves\. `providers` lists what a person may connect that no server serves yet\.$/u, + /^Searches the tools of the tool servers this playbook's functions may use, .* Without `playbook` it searches every tool server of the org, with the playbooks each serves\. With `terms` naming a service, `providers` lists what a person may connect that no server serves yet, found as tools are, for add_connection by name\.$/u, ); }); diff --git a/packages/tool-servers/src/search/search-in-org.ts b/packages/tool-servers/src/search/search-in-org.ts index 84f12614e..6fabc96aa 100644 --- a/packages/tool-servers/src/search/search-in-org.ts +++ b/packages/tool-servers/src/search/search-in-org.ts @@ -20,8 +20,8 @@ import { noServerNamed, searchSentences } from './search.ts'; const description = [ ...searchSentences, - 'Without `playbook` it searches the tool servers of the whole org, naming the playbooks each serves.', - '`providers` lists what a person may connect that no server serves yet.', + 'Without `playbook` it searches every tool server of the org, with the playbooks each serves.', + 'With `terms` naming a service, `providers` lists what a person may connect that no server serves yet, found as tools are, for add_connection by name.', ].join(' '); export type PlaybookLookup = (playbook: string) => Effect.Effect; diff --git a/packages/tool-servers/src/search/search.ts b/packages/tool-servers/src/search/search.ts index 2b784ee1d..a4cd4a1f8 100644 --- a/packages/tool-servers/src/search/search.ts +++ b/packages/tool-servers/src/search/search.ts @@ -7,10 +7,10 @@ import { FoundSchema, SearchInputSchema } from './search-schemas.ts'; import { searchAttempted, searchTask, searched } from './search-words.ts'; export const searchSentences = [ - "Searches the tools of the tool servers this playbook's functions may use, which a function names in its tools as server/tool or server/*, and execute calls.", - 'Without `terms` it lists the servers and, while they offer at most 100 tools together, every tool; with `terms`, the tools whose names, arguments or descriptions hold the words, best first.', - 'Each server is asked for its tools when this is called, and one that cannot be asked just now says why in place of its tools.', - '`server` keeps one server, `only` the tools that read or those that may write, and introspect describes a tool whole.', + "Searches the tools of the tool servers this playbook's functions may use, named in a function's tools as server/tool or server/* and called by execute.", + 'Without `terms` it lists the servers and, while they offer at most 100 tools together, every tool; with `terms`, the tools whose names, arguments or descriptions hold them, best first.', + 'Each server is asked at each call, and one that cannot be asked just now says why in place of its tools.', + '`server` keeps one server, `only` the tools that read or those that may write; introspect describes a tool whole.', ]; export function noServerNamed(server: string, holder: string): InvalidInput {