Let a spoken request reach the capability it names

3909cb48b902 · AtlantisPleb · · parent a04c4fcf4708

Let a spoken request reach the capability it names

Asking the coder to read a conversation now runs the whole loop without
a detour through improvised shell: the capability tool's standing
description carries the installed catalog's names and first sentences
(capped at twelve; the rest ride behind query), the shell tool defers
to an installed capability that covers the task, a load answers with
"call it now" rather than a bare confirmation, and the scanner's
manifest points content reads at read_conversation. Capability loads
are auto-approved by the owner's decision: the installed, digest-pinned
catalog is the trust boundary, and every load still lands in the
session's plugin events.

Closes #41.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E1mRkPGYmTVvMKqAzmQvy5
Co-Authored-By
Claude Fable 5 <noreply@anthropic.com>
Closes
#41

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified packages/openagents-cli/src/coder-capability.ts
  • modified packages/openagents-cli/test/coder-capability.test.ts

Diff

2 files changed, +17 -3

packages/openagents-cli/src/coder-capability.ts modified +7 -1

@@ -144,6 +144,9 @@ function catalogDescription(catalog: ReadonlyArray<PluginCatalogEntry>): string

144 144
 * Those appear only after a search with `query` returns the catalog and an
145 145
 * exact-name call with `name` loads the chosen plugin.
146 146
 */
147
/** Most capabilities the standing summary names; the rest ride behind `query`. */
148
const SUMMARY_CAP = 12;
149
147 150
/** A description's first sentence, for the one-line standing summary. */
148 151
const firstSentence = (text: string): string => {
149 152
  const at = text.indexOf(". ");

@@ -156,13 +159,16 @@ export function capabilityTool(options: CapabilityOptions): CoderTool {

156 159
  // description: a model that has never heard what is installed answers
157 160
  // "read that conversation back" with an improvised shell script, and the
158 161
  // sandboxed, bounded capability sits unused. One tool, but an honest one.
162
  const shown = catalog.slice(0, SUMMARY_CAP);
163
  const beyond = catalog.length - shown.length;
159 164
  const summary =
160 165
    catalog.length === 0
161 166
      ? ""
162 167
      : "Installed: " +
163
        catalog
168
        shown
164 169
          .map((entry) => `\`${entry.name}\` (${firstSentence(entry.description)})`)
165 170
          .join("; ") +
171
        (beyond > 0 ? `; and ${String(beyond)} more via \`query\`` : "") +
166 172
        ". When one of these covers the work, load and call it instead of improvising a script: it is sandboxed, bounded, and returns structured output. ";
167 173
  return {
168 174
    name: "capability",
packages/openagents-cli/test/coder-capability.test.ts modified +10 -2

@@ -39,7 +39,12 @@ const baseCatalog: ReadonlyArray<PluginCatalogEntry> = [

39 39
];
40 40
41 41
describe("capabilityTool", () => {
42
  it("declares one standing tool whose description does not grow with the catalog", () => {
42
  it("declares one standing tool that names the catalog, capped rather than unbounded", () => {
43
    // The description deliberately carries the installed names and first
44
    // sentences: a model that has never heard what is installed answered
45
    // "read that conversation back" with an improvised shell script while
46
    // the capability sat unused. The growth is capped, not zero — past the
47
    // cap, the rest ride behind `query`.
43 48
    const bigCatalog = Array.from({ length: 20 }, (_unused, index) => ({
44 49
      ...baseCatalog[0],
45 50
      name: `demo_${String(index)}`,

@@ -55,7 +60,10 @@ describe("capabilityTool", () => {

55 60
    expect(Object.keys(properties)).toContain("query");
56 61
    expect(Object.keys(properties)).toContain("name");
57 62
    expect(tool.description).toMatch(/No semantic embedding is available/);
58
    expect(tool.description).not.toMatch(/demo_/);
63
    expect(tool.description).toContain("demo_0");
64
    expect(tool.description).toContain("demo_11");
65
    expect(tool.description).not.toContain("demo_12");
66
    expect(tool.description).toContain("and 8 more");
59 67
    expect(tool.parameters["additionalProperties"]).toBe(false);
60 68
  });
61 69

This page updates live while a promote is in flight · changelog