[Experiment] Two-tool surface: stable gitea dispatcher + gitea_help (9,734 B vs 65,159 B) #51

Open
opened 2026-10-24 20:30:15 +00:00 by dsh · 1 comment
Owner

Idea

Instead of 96 typed tools, expose one stable dispatcher plus one schema fetcher:

  • gitea { operation, args } — runs any operation; validates args server-side against the existing per-operation schema.
  • gitea_help { operation } — returns that operation's description and JSON Schema as a tool result.

This is the harness's own PTC "code mode" philosophy (one transport tool + generated SDK) implemented in plugin space for plain native function calling — without depending on PTC being composed.

Measured against the live 0.8.4 schemas

Shape Bytes vs canonical
canonical 96 tools 65,159 1×
+ 37 deprecated aliases (today) 107,157 1.64×
dispatcher, no catalog (183 B) + help (210 B) 393 166×
dispatcher + 96 one-line op catalog (9,524 B) + help (210 B) 9,734 6.7×
top-15 typed tools, for reference 11,815 5.5×

The catalog variant is the realistic one: the model has to know which operations exist, so the dispatcher description carries one line per operation. That catalog alone measured 9,146 B; the dispatcher skeleton is 183 B and gitea_help 210 B.

Why this is harness-compatible (unlike #50)

  • The registry never changes, so the request prefix stays stable and KV-cache reuse survives.
  • Schema delivery happens as an append-only tool result — the docs say that content "follows the reusable request prefix" instead of invalidating it.
  • No dependence on tool-addition records or deferLoading provider support, i.e. no bet on the least stable part of DSH before 1.0.

Tradeoffs to measure

  • +1 round trip for an unfamiliar operation (help → call). Hot operations (the top 15 are 70 % of traffic) should be called directly from memory.
  • Weaker first-try argument accuracy without the schema in context. Server-side validation against the existing DSL turns a bad call into a clear error with the schema, but every retry is a full step.
  • Loses per-tool UI presentation and per-operation permission rules; policy would key on the dispatcher argument instead of the tool name.
  • Error text becomes the teaching surface: gitea_help should stay under ~1.5 KB for the largest operation.

Sketch

- id: dsh-gitea
  config:
    preset: dispatcher   # opt-in; `full` remains the default

TOOL_DEFS stays the single source of truth: the dispatcher validates args with the same per-operation schema, and gitea_help renders from TOOL_DEFS — no second schema corpus that can drift. Shares the preset plumbing from #49 rather than inventing a parallel switch.

Impact

A 6.7× smaller permanent tool block (≈9.7 KB vs 65 KB, ≈2.6k vs ≈17.6k tokens) with no dependence on dynamic registration, so it is the fallback if #50's spike says dynamic injection is too expensive or too fragile.

Acceptance criteria (spike gate)

  • opt-in preset: dispatcher; default unchanged and byte-identical
  • measured per-step prompt tokens and first-try argument accuracy on the top-15 operations vs core and full
  • gitea_help output < 1.5 KB per operation and available for every operation
  • multi-instance / instance parameter keeps working through the dispatcher
  • decision recorded (ship / revise / close)

Boundaries

  • No change to handlers or canonical schemas; dispatch and validation only.
  • Not a replacement for #49 — presets stay the default lever.
  • No change to how DSH binds or presents tools.

Dependencies

Complementary to #50 and mutually exclusive with it in practice: both answer "how small can the surface get", one by dynamic injection, one by a stable dispatcher. #49's preset plumbing is shared.

## Idea Instead of 96 typed tools, expose one **stable dispatcher** plus one **schema fetcher**: - `gitea { operation, args }` — runs any operation; validates `args` server-side against the existing per-operation schema. - `gitea_help { operation }` — returns that operation's description and JSON Schema as a **tool result**. This is the harness's own PTC "code mode" philosophy (one transport tool + generated SDK) implemented in plugin space for plain native function calling — without depending on PTC being composed. ## Measured against the live 0.8.4 schemas | Shape | Bytes | vs canonical | |---|---|---| | canonical 96 tools | 65,159 | 1× | | + 37 deprecated aliases (today) | 107,157 | 1.64× | | dispatcher, no catalog (183 B) + help (210 B) | 393 | 166× | | **dispatcher + 96 one-line op catalog (9,524 B) + help (210 B)** | **9,734** | **6.7×** | | top-15 typed tools, for reference | 11,815 | 5.5× | The catalog variant is the realistic one: the model has to know which operations exist, so the dispatcher description carries one line per operation. That catalog alone measured 9,146 B; the dispatcher skeleton is 183 B and `gitea_help` 210 B. ## Why this is harness-compatible (unlike #50) - The registry never changes, so the request prefix stays stable and KV-cache reuse survives. - Schema delivery happens as an **append-only tool result** — the docs say that content "follows the reusable request prefix" instead of invalidating it. - No dependence on `tool-addition` records or `deferLoading` provider support, i.e. no bet on the least stable part of DSH before 1.0. ## Tradeoffs to measure - **+1 round trip** for an unfamiliar operation (`help` → call). Hot operations (the top 15 are 70 % of traffic) should be called directly from memory. - **Weaker first-try argument accuracy** without the schema in context. Server-side validation against the existing DSL turns a bad call into a clear error with the schema, but every retry is a full step. - **Loses per-tool UI presentation and per-operation permission rules**; policy would key on the dispatcher argument instead of the tool name. - **Error text becomes the teaching surface**: `gitea_help` should stay under ~1.5 KB for the largest operation. ## Sketch ```yaml - id: dsh-gitea config: preset: dispatcher # opt-in; `full` remains the default ``` `TOOL_DEFS` stays the single source of truth: the dispatcher validates `args` with the same per-operation schema, and `gitea_help` renders from `TOOL_DEFS` — no second schema corpus that can drift. Shares the `preset` plumbing from #49 rather than inventing a parallel switch. ## Impact A 6.7× smaller permanent tool block (≈9.7 KB vs 65 KB, ≈2.6k vs ≈17.6k tokens) with no dependence on dynamic registration, so it is the fallback if #50's spike says dynamic injection is too expensive or too fragile. ## Acceptance criteria (spike gate) - [ ] opt-in `preset: dispatcher`; default unchanged and byte-identical - [ ] measured per-step prompt tokens and first-try argument accuracy on the top-15 operations vs `core` and `full` - [ ] `gitea_help` output < 1.5 KB per operation and available for every operation - [ ] multi-instance / `instance` parameter keeps working through the dispatcher - [ ] decision recorded (ship / revise / close) ## Boundaries - No change to handlers or canonical schemas; dispatch and validation only. - Not a replacement for #49 — presets stay the default lever. - No change to how DSH binds or presents tools. ## Dependencies Complementary to #50 and mutually exclusive with it in practice: both answer "how small can the surface get", one by dynamic injection, one by a stable dispatcher. #49's `preset` plumbing is shared.
Author
Owner

Spike branch is up: experiment/dispatcher (commit c921667, branched off v0.8.5). Not tagged, not published — master stays at 0.8.5 and this branch is rebased or dropped after the gate.

What exists

  • preset: dispatcher (opt-in; full stays the default and is unchanged) registers exactly two tools:
    • gitea { operation, args } — validates args against that operation's own schema, then runs the same execution path as the typed tool (executeCanonicalTool(), extracted so the two cannot drift).
    • gitea_help { operation } — returns the operation's description + compact JSON Schema as a tool result.
  • lib/dispatcher.js is pure and derives everything from TOOL_DEFS: one-line catalog, help rendering, validation. No second schema corpus.
  • Validation is deliberately small but covers the dispatcher's new failure modes and teaches: missing required, wrong type, bad enum, and unknown argument (a silently ignored typo is the worst case without per-argument UI). Every error names the schema source: Call gitea_help { operation: "gitea_issue_get" } for its schema.
  • instance passes through untouched, so multi-instance keeps working.

Measured surface (wire bytes, JSON.stringify({name, description, parameters}))

Shape Tools Bytes vs full
full (canonical, default) 96 65,159 1×
core 15 11,640 5.6×
dispatcher + catalog 2 8,973 7.3×
none 0 0 —

Breakdown of the 8,973 B: catalog + instructions 8,531 B, gitea tool parameters ~300 B, gitea_help description 163 B. Help payloads average 514 B, largest 947 B — every operation is inside the ~1.5 KB ceiling (enforced by test/dispatcher.test.mjs over all 96).

So the measured shape beats the issue's 9,734 B / 6.7× estimate, and — the important part for this versus #50 — the registry never changes, so the request prefix stays reusable.

Gate status

  • prototype behind opt-in config; default behaviour byte-identical (full still 65,159 B in test/dispatcher.test.mjs)
  • gitea_help under 1.5 KB for every operation
  • multi-instance instance flows through the dispatcher
  • per-step prompt tokens and first-try argument accuracy on the top-15 operations vs core/full — needs live sessions
  • decision recorded

The missing piece needs a profile change to run: preset: dispatcher in the dsh-gitea row of <profile>/cordis.patch.yml, which is outside this workspace (a profile write, so it needs operator approval — say the word and I'll do it with approval, or run a throwaway profile). The measurement itself is the request/header + usage method from #48/#49 plus an accuracy column (first-call success vs retry after a gitea_help round-trip).

Two tradeoffs to settle with the numbers, both from the issue: +1 round trip for unfamiliar operations, and weaker first-try arguments without schemas in context. The dispatch does not hide either — it turns them into measured columns.

Spike branch is up: **`experiment/dispatcher`** (commit `c921667`, branched off `v0.8.5`). Not tagged, not published — master stays at 0.8.5 and this branch is rebased or dropped after the gate. ## What exists - `preset: dispatcher` (opt-in; `full` stays the default and is unchanged) registers exactly two tools: - `gitea { operation, args }` — validates `args` against that operation's own schema, then runs the **same** execution path as the typed tool (`executeCanonicalTool()`, extracted so the two cannot drift). - `gitea_help { operation }` — returns the operation's description + compact JSON Schema as a tool result. - `lib/dispatcher.js` is pure and derives everything from `TOOL_DEFS`: one-line catalog, help rendering, validation. No second schema corpus. - Validation is deliberately small but covers the dispatcher's new failure modes and *teaches*: missing required, wrong type, bad enum, and **unknown argument** (a silently ignored typo is the worst case without per-argument UI). Every error names the schema source: `Call gitea_help { operation: "gitea_issue_get" } for its schema.` - `instance` passes through untouched, so multi-instance keeps working. ## Measured surface (wire bytes, `JSON.stringify({name, description, parameters})`) | Shape | Tools | Bytes | vs `full` | |:--|--:|--:|--:| | `full` (canonical, default) | 96 | 65,159 | 1× | | `core` | 15 | 11,640 | 5.6× | | **`dispatcher` + catalog** | **2** | **8,973** | **7.3×** | | `none` | 0 | 0 | — | Breakdown of the 8,973 B: catalog + instructions 8,531 B, `gitea` tool parameters ~300 B, `gitea_help` description 163 B. Help payloads average 514 B, largest 947 B — every operation is inside the ~1.5 KB ceiling (enforced by `test/dispatcher.test.mjs` over all 96). So the measured shape beats the issue's 9,734 B / 6.7× estimate, and — the important part for this versus #50 — **the registry never changes**, so the request prefix stays reusable. ## Gate status - [x] prototype behind opt-in config; default behaviour byte-identical (full still 65,159 B in `test/dispatcher.test.mjs`) - [x] `gitea_help` under 1.5 KB for every operation - [x] multi-instance `instance` flows through the dispatcher - [ ] **per-step prompt tokens and first-try argument accuracy on the top-15 operations vs `core`/`full`** — needs live sessions - [ ] decision recorded The missing piece needs a profile change to run: `preset: dispatcher` in the `dsh-gitea` row of `<profile>/cordis.patch.yml`, which is outside this workspace (a profile write, so it needs operator approval — say the word and I'll do it with approval, or run a throwaway profile). The measurement itself is the `request/header` + `usage` method from #48/#49 plus an accuracy column (first-call success vs retry after a `gitea_help` round-trip). Two tradeoffs to settle with the numbers, both from the issue: +1 round trip for unfamiliar operations, and weaker first-try arguments without schemas in context. The dispatch does not hide either — it turns them into measured columns.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
dsh-plugins/dsh-gitea#51
No description provided.