diff --git a/CHANGELOG.md b/CHANGELOG.md index 988f834..7ada9ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,24 @@ a MUST, SHOULD or MAY, or a wire surface — adds an entry here; editorial changes do not. No spec carries a revision counter: these entries are the revision record. +## OVOS-TOOLS-1 — Agent Tools Bus Contract + +### 1 + +- New. Specifies the agent tools bus surface: the toolbox topics + `ovos.persona.tools.discover` and `ovos.persona.tools.call` with + `data.toolbox_id` addressing (§4), the tool service topics + `ovos.tools.list`, `ovos.tools.get`, `ovos.tools.invoke` and + `ovos.tools.reload` (§5), and the tool definition shape (§3). + ovos-plugin-manager's `ToolBox` speaks the discover topic and a + per-toolbox call topic; ovos-PHAL-plugin-tools speaks the four tool + service topics. Two things are new work. The static call topic with + the toolbox in the payload, per OVOS-MSG-1 §2.1.1, lands in + `ToolBox.bind`, which keeps the per-toolbox topic one stable cycle + (§6). The §5 rule that a tool service answers an error when two + toolboxes own one tool name lands in ovos-PHAL-plugin-tools, whose + registry maps a name to the last toolbox loaded and silently picks it. + ## OVOS-INSTALL-1 — Plugin Installation Bus Contract ### 1 diff --git a/README.md b/README.md index 18411b7..6f4cb64 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,7 @@ The **Version** column carries the specification's compatibility class | OVOS-SESSION-2 | [Session Lifecycle and State Ownership](session-2.md) | 1 | Draft | | OVOS-BRIDGE-1 | [Bus Bridge and Opaque Relay](bridge-1.md) | 2 | Draft | | OVOS-INSTALL-1 | [Plugin Installation Bus Contract](install-1.md) | 1 | Draft | +| OVOS-TOOLS-1 | [Agent Tools Bus Contract](tools-1.md) | 1 | Draft | ### Orchestrator stack — what processes utterances diff --git a/appendix/divergences.md b/appendix/divergences.md index 08520c2..4783e7e 100644 --- a/appendix/divergences.md +++ b/appendix/divergences.md @@ -556,6 +556,14 @@ responder's identity in `data.skill_id` and the round identified by | `question:query` | `ovos.common_query.request` | Static topic; the skill being asked is named in `data.skill_id`. See §5.2. | | `question:query.response` | `ovos.common_query.response` | Answer correlated by `utterance_id`, not by the `phrase` string. | +#### Agent tools topics (TOOLS-1) + +| Predecessor topic | v1 replacement | Notes | +|--------------|---------------|-------| +| `ovos.persona.tools..call` | `ovos.persona.tools.call` | Identifier moves from the topic to `data.toolbox_id` (TOOLS-1 §4.2); payloads unchanged. Shipped `ovos-plugin-manager` `ToolBox.bind` subscribes to the predecessor; kept one stable cycle. | +| `ovos.persona.tools.discover` | **unchanged** | Already a static broadcast answered per toolbox with `data.toolbox_id` (TOOLS-1 §4.1). | +| `ovos.tools.list` / `.get` / `.invoke` / `.reload` | **unchanged** | Already static topics with the tool in the payload; TOOLS-1 §5 fixes their payloads. | + #### Listening-failure topic (AUDIO-IN-1) | Predecessor topic | v2 replacement | Notes | diff --git a/tools-1.md b/tools-1.md new file mode 100644 index 0000000..9094c4b --- /dev/null +++ b/tools-1.md @@ -0,0 +1,218 @@ +# Agent Tools Bus Contract Specification + +**Spec ID:** OVOS-TOOLS-1 · **Version:** 1 · **Status:** Draft + +This specification defines the bus surface by which a component finds +and calls the tools a persona can use: the topics a toolbox host +answers, the topics a tool service answers on behalf of every toolbox +it loads, the payload field that addresses one toolbox among several, +the tool definition shape, and the replies. + +A toolbox is a plugin that groups tools, each with a typed argument +schema and a typed output schema. A persona offers those tools to a +model; the model asks for one by name; the host runs it and returns +the output. Over the bus, any component can do the same. + +Dependencies: OVOS-MSG-1 (envelope, `reply` and `response` +derivations). + +The key words **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT**, +**RECOMMENDED**, **MAY** are used as in RFC 2119. + +--- + +## 1. Scope + +This specification defines: the tool definition shape, the toolbox +discovery and call topics, the payload field that addresses one +toolbox, the tool service topics that aggregate every loaded toolbox, +the reply payloads and error form, and the pre-spec per-toolbox topic +it replaces. + +It does **not** define: how a toolbox implements a tool, the Python +plugin contract a toolbox is written against, how a persona chooses +tools for a model, the HTTP renderings of the same tools (UTCP, MCP, +OpenAI function calling), or any authorization of who may call a +tool. Authorization is a layer-2 concern (OVOS-MSG-1 §3.4). + +--- + +## 2. Governing rule + +Two clauses of OVOS-MSG-1 fix the shape of every topic below. + +§2.1.1: "Nothing about a Message's target travels in a dotted topic: +addressing beyond the topic is **payload** (`data`), and the routing +pair (§3.2–§3.3) is owned by the `reply` swap (§5.2) alone." + +§5.3: "Equivalent to `reply(T + ".response", D')`. A `response` is a +`reply` whose topic is the source topic suffixed with `.response`. +Topics defined in other specifications **MAY** rely on the +`.response` suffix convention to mark a Message as the answer to a +prior one." + +Every topic in this specification is therefore a static dotted string, +the toolbox a request is for travels in `data.toolbox_id`, and every +answer is the `response` derivation of the request. + +--- + +## 3. Tool definition + +A tool is described by one object: + +| Field | Type | Meaning | +|-------|------|---------| +| `name` | string | Tool name, unique inside its toolbox. | +| `description` | string | What the tool does, written for a model. | +| `argument_schema` | object | JSON Schema of the call arguments. | +| `output_schema` | object | JSON Schema of the result. | +| `toolbox_id` | string | The toolbox that owns the tool. | + +A toolbox **MUST** give every tool a `name` that is unique within the +toolbox. Two toolboxes **MAY** own tools of the same name; the pair +`(toolbox_id, name)` is the identity of a tool on the bus. + +A `toolbox_id` is an identifier the toolbox chooses, stable across +restarts, with no `:` and no `.`. Two toolboxes on one bus **MUST +NOT** share a `toolbox_id`. + +--- + +## 4. Toolbox topics + +A toolbox host is a component that loads one or more toolboxes and +binds each to the bus. It **MUST** subscribe to both topics for each +toolbox it hosts. + +### 4.1 `ovos.persona.tools.discover` + +A broadcast. Every toolbox host answers for every toolbox it hosts, +one `response` per toolbox: + +```json +{ + "type": "ovos.persona.tools.discover.response", + "data": { + "toolbox_id": "wordnet", + "tools": [ { "name": "define_word", "description": "...", + "argument_schema": { }, "output_schema": { }, + "toolbox_id": "wordnet" } ] + } +} +``` + +The request carries no payload fields this specification reads. A +host **MUST** refresh its tool list before answering, so the answer +reflects tools discovered after start. + +### 4.2 `ovos.persona.tools.call` + +A call to one tool in one toolbox: + +```json +{ + "type": "ovos.persona.tools.call", + "data": { + "toolbox_id": "wordnet", + "name": "define_word", + "kwargs": { "word": "serendipity", "lang": "en" } + } +} +``` + +`data.toolbox_id` and `data.name` are required. `data.kwargs` is an +object matching the tool's `argument_schema`; absent means `{}`. + +A host **MUST** compare `toolbox_id` exactly and **MUST** ignore a +call naming a toolbox it does not host: it runs nothing and **MUST +NOT** answer, not even to decline. A refusal from every other host +would turn one addressed call into a burst of replies the caller +cannot tell from the real answer. A call naming no toolbox, or a +toolbox nobody hosts, is answered by nobody. + +The host that owns the toolbox answers with one `response`: + +```json +{ + "type": "ovos.persona.tools.call.response", + "data": { + "toolbox_id": "wordnet", + "result": { "definitions": [ ["noun", "good luck in making unexpected and fortunate discoveries"] ], "examples": [] } + } +} +``` + +`result` is the tool output validated against `output_schema`. On +failure the same topic carries `error` instead of `result`: + +```json +{ "type": "ovos.persona.tools.call.response", + "data": { "toolbox_id": "wordnet", "error": "ValueError: unknown tool 'x'" } } +``` + +A response **MUST** carry exactly one of `result` and `error`. `error` +is a string. When the tool ran and raised, the string starts with the +exception class name, then `: `, then the message. A tool that is not +in the toolbox, arguments that fail `argument_schema`, and an output +that fails `output_schema` are all errors on this topic, each stated in +plain words. + +--- + +## 5. Tool service topics + +A tool service is a component that loads every installed toolbox and +answers for all of them under one flat namespace, so a caller need not +know which toolbox owns a tool. The PHAL tools plugin is the shipped +tool service. A tool service **MUST** subscribe to the four topics. + +| Topic | Request `data` | `response` `data` | +|-------|----------------|-------------------| +| `ovos.tools.list` | none read | `tools`: list of tool definitions (§3), every toolbox. | +| `ovos.tools.get` | `name` | one tool definition (§3), or `error`. | +| `ovos.tools.invoke` | `name`, `args` (object, absent means `{}`) | `name` and `result`, or `name` and `error`. | +| `ovos.tools.reload` | none read | `tools`: the list after every toolbox refreshed. | + +`ovos.tools.get` and `ovos.tools.invoke` answer `error` in the §4.2 +form when `name` is absent (`Missing required field: 'name'`) or names +no tool (`Unknown tool: ''`), and with the class-name prefix when +the tool ran and raised. A tool service resolves `name` across +every toolbox it loaded; when two toolboxes own the same name the +service **MUST** answer with an error rather than pick one, since the +flat namespace cannot address the pair of §3. + +A tool service is a toolbox host as well (§4) for the toolboxes it +loads, so both surfaces answer on one bus with one loaded set. + +--- + +## 6. Predecessor topic + +Shipped toolbox hosts subscribe to a per-toolbox call topic: + +| Predecessor topic | Replacement | Notes | +|-------------------|-------------|-------| +| `ovos.persona.tools..call` | `ovos.persona.tools.call` with `data.toolbox_id` | Identifier moves from the topic to the payload. Request and response payloads are unchanged. | + +A host **SHOULD** keep answering the predecessor for one stable cycle +after it adopts §4.2, and a caller **SHOULD** send the replacement. +The predecessor is catalogued in appendix/divergences.md §5.7. + +--- + +## 7. Conformance + +A toolbox host conforms when: it answers `ovos.persona.tools.discover` +once per hosted toolbox with the §3 shape; it answers +`ovos.persona.tools.call` only for its own `toolbox_id`, with exactly +one of `result` and `error`; and it stays silent on a call for a +toolbox it does not host. + +A tool service conforms when it answers the four §5 topics with the +stated payloads, resolves `name` across every loaded toolbox, and +reports a duplicate name as an error. + +A conformance suite drives each check over a real bus with one hosted +toolbox that owns one tool, and asserts the payload values, the count +of responses, and the absence of a response where silence is required.