diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 070ca56f..e39a6739 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -142,7 +142,8 @@ change follows, whether a person or a coding agent writes it. ## Releasing Maintainers bump `version` in the root, `apps/hub`, `apps/web`, -`packages/client` and `packages/schemas` package.json files, run +`packages/client`, `packages/schemas`, `packages/tools-hub` and +`packages/tools-skills` package.json files, run `bun install`, and merge. Once the publish workflow for that commit on main has finished, `git tag -s vX.Y.Z && git push origin vX.Y.Z` retags its hub and sidecar images as `X.Y.Z` and publishes the GitHub release; a 0.x version or diff --git a/apps/hub/package.json b/apps/hub/package.json index c39cedd8..f840e4df 100644 --- a/apps/hub/package.json +++ b/apps/hub/package.json @@ -20,6 +20,8 @@ "@corbits-builder/designer": "workspace:*", "@corbits-builder/reviewer": "workspace:*", "@corbits-builder/schemas": "workspace:*", + "@corbits-builder/tools-hub": "workspace:*", + "@corbits-builder/tools-skills": "workspace:*", "@corbits-builder/writer": "workspace:*", "@corbits/agent-token": "0.2.2", "@corbits/artifacts": "0.3.0", diff --git a/apps/hub/src/artifacts/design-tools.ts b/apps/hub/src/artifacts/design-tools.ts index 91af0408..95edce29 100644 --- a/apps/hub/src/artifacts/design-tools.ts +++ b/apps/hub/src/artifacts/design-tools.ts @@ -8,8 +8,10 @@ import { DesignBundle } from "@corbits-builder/schemas/artifact"; import { designCheckFor } from "../builds/design-check.ts"; import { withDesignTokens } from "./design-tokens.ts"; import { DESIGN_STYLES } from "./design-tweaks.ts"; -import { resolveWorkflowProjectScope } from "../workflows/project-scope.ts"; -import { workflowHttpJson } from "../workflows/http-json.ts"; +import { + resolveWorkflowProjectScope, + workflowHttpJson, +} from "@corbits-builder/tools-hub"; import { failure, HubArtifactReceipt } from "./tools.ts"; interface DesignToolEnv extends BaseEnv { diff --git a/apps/hub/src/artifacts/tools.ts b/apps/hub/src/artifacts/tools.ts index 11579584..6bf8d704 100644 --- a/apps/hub/src/artifacts/tools.ts +++ b/apps/hub/src/artifacts/tools.ts @@ -15,11 +15,12 @@ import { } from "@corbits-builder/schemas/artifact"; import type { WorkflowRole } from "@corbits-builder/schemas/installed-workflows"; import { unpackDesign } from "./design-tools.ts"; +import { projectScopeFailure } from "../workflows/project-scope.ts"; import { - projectScopeFailure, resolveWorkflowProjectScope, -} from "../workflows/project-scope.ts"; -import { workflowHttpJson, WorkflowHttpError } from "../workflows/http-json.ts"; + workflowHttpJson, + WorkflowHttpError, +} from "@corbits-builder/tools-hub"; interface ArtifactToolEnv extends BaseEnv { capabilities: RuntimeCapabilities; diff --git a/apps/hub/src/builds/app-repository.ts b/apps/hub/src/builds/app-repository.ts index d6145afc..096c1705 100644 --- a/apps/hub/src/builds/app-repository.ts +++ b/apps/hub/src/builds/app-repository.ts @@ -32,7 +32,7 @@ import { mediatedHttpJson, WorkflowHttpError, workflowHttpJson, -} from "../workflows/http-json.ts"; +} from "@corbits-builder/tools-hub"; const AppPushAccess = type({ path: "string", diff --git a/apps/hub/src/builds/app-snapshot.ts b/apps/hub/src/builds/app-snapshot.ts index 6398fb0e..3c34924c 100644 --- a/apps/hub/src/builds/app-snapshot.ts +++ b/apps/hub/src/builds/app-snapshot.ts @@ -12,7 +12,10 @@ import { DesignDraft, MAX_APP_BYTES, } from "@corbits-builder/schemas/artifact"; -import { WorkflowHttpError, workflowHttpJson } from "../workflows/http-json.ts"; +import { + WorkflowHttpError, + workflowHttpJson, +} from "@corbits-builder/tools-hub"; import { INPUTS_DIRECTORY } from "../config/inputs.ts"; // Installs and builds are rebuilt by setup and check-app, and .builder and the inputs come from diff --git a/apps/hub/src/builds/sidecar-tools.ts b/apps/hub/src/builds/sidecar-tools.ts index fef7123c..86907265 100644 --- a/apps/hub/src/builds/sidecar-tools.ts +++ b/apps/hub/src/builds/sidecar-tools.ts @@ -11,8 +11,10 @@ import { type ToolRunner, } from "@intx/types/runtime"; import type { RuntimeCapabilities } from "@intx/types/runtime-capabilities"; -import { resolveWorkflowProjectScope } from "../workflows/project-scope.ts"; -import { WorkflowHttpError } from "../workflows/http-json.ts"; +import { + resolveWorkflowProjectScope, + WorkflowHttpError, +} from "@corbits-builder/tools-hub"; import { APP_STATUS_TOOL, createCodingTools } from "./coding-tools.ts"; import { createPosixBuildExecutor } from "./posix-sandbox.ts"; import { browserTools } from "./browser.ts"; diff --git a/apps/hub/src/projects/deploy-tools.ts b/apps/hub/src/projects/deploy-tools.ts index acd66ef8..5bfb12bf 100644 --- a/apps/hub/src/projects/deploy-tools.ts +++ b/apps/hub/src/projects/deploy-tools.ts @@ -1,7 +1,9 @@ +import { projectScopeFailure } from "../workflows/project-scope.ts"; import { - projectScopeFailure, resolveWorkflowProjectScope, -} from "../workflows/project-scope.ts"; + workflowHttpJson, + WorkflowHttpError, +} from "@corbits-builder/tools-hub"; import { defineTool, type BaseEnv, @@ -50,7 +52,6 @@ import { designLookIdeas, } from "../config/design.ts"; import { QUESTIONS_ASKED } from "../config/questions.ts"; -import { workflowHttpJson, WorkflowHttpError } from "../workflows/http-json.ts"; import { assignmentToWriterMessage } from "../workflows/writer-task.ts"; import { coderTaskMessage } from "@corbits-builder/coder/contract"; diff --git a/apps/hub/src/skills/tools.ts b/apps/hub/src/skills/tools.ts deleted file mode 100644 index d9cf3658..00000000 --- a/apps/hub/src/skills/tools.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { type } from "arktype"; -import { defineTool } from "@intx/agent/authoring"; -import { designSkills } from "@corbits-builder/designer/skills" with { type: "macro" }; -import { documentSkills } from "@corbits-builder/writer/skills" with { type: "macro" }; - -const SKILLS = { ...designSkills(), ...documentSkills() }; -const ReadSkill = type({ - "+": "reject", - name: "string > 0", - "file?": "string > 0", - "part?": "number.integer > 0", -}); -// A tool result over 10000 characters is spilled to a file, so a skill file is read in parts that -// fit; a designer reading back a spilled 12000-character SKILL.md never reached its end. -const PART_CHARS = 8000; - -const describe = (name: string) => - SKILLS[name]?.["SKILL.md"]?.match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? - ""; - -// Agent Skills disclosed progressively: the tool description lists each skill's name and when to -// use it; a call returns the skill's instructions, or one of the reference files they point to. -export function createSkillTools(names: readonly string[]) { - for (const name of names) - if (!SKILLS[name]) throw new Error(`unknown_skill: ${name}`); - const definitions = [ - { - name: "read_skill", - description: [ - "Read a skill: tested instructions for one kind of work. Pass name to get its instructions; pass file as well to read one of the reference files they name. A long file comes in numbered parts, and part reads the next one.", - ...names.map((name) => `- ${name}: ${describe(name)}`), - ].join("\n"), - inputSchema: { ...ReadSkill.toJsonSchema() }, - }, - ]; - return defineTool({ - id: "@corbits-builder/skills/tools", - definitions: definitions.map(({ name }) => ({ name })), - factory: () => ({ - definitions, - run: async (call) => { - const input = ReadSkill(call.arguments); - if (input instanceof type.errors) - return { callId: call.id, content: input.summary, isError: true }; - // Writers asked for problem_brief and product_requirements, the document kinds their task - // names, 13 times in 20 eval runs; a skill is named for its kind with hyphens. - const name = input.name.replaceAll("_", "-"); - const skill = names.includes(name) ? SKILLS[name] : undefined; - if (!skill) - return { - callId: call.id, - content: `no skill named ${input.name}; skills: ${names.join(", ")}`, - isError: true, - }; - const file = input.file ?? "SKILL.md"; - const content = Object.hasOwn(skill, file) ? skill[file] : undefined; - if (content === undefined) - return { - callId: call.id, - content: `${input.name} has no file ${file}; files: ${Object.keys(skill).join(", ")}`, - isError: true, - }; - const parts = Math.max(1, Math.ceil(content.length / PART_CHARS)); - if (parts === 1) return { callId: call.id, content }; - const part = Math.min(input.part ?? 1, parts); - const slice = content.slice((part - 1) * PART_CHARS, part * PART_CHARS); - return { - callId: call.id, - content: - part < parts - ? `${slice}\n[part ${part} of ${parts}; read_skill with part ${part + 1} for the rest]` - : `${slice}\n[part ${part} of ${parts}, the end]`, - }; - }, - }), - }); -} diff --git a/apps/hub/src/workflows/coder-package.ts b/apps/hub/src/workflows/coder-package.ts index 40d9ab9a..5903d2fa 100644 --- a/apps/hub/src/workflows/coder-package.ts +++ b/apps/hub/src/workflows/coder-package.ts @@ -8,7 +8,7 @@ import { browserCatalog } from "../builds/browser-catalog.ts" with { type: "macr import { mcpTools } from "@corbits/mcp/interchange/client"; import { mail, type MailToolEnv } from "@intx/tools-mail/sidecar-bundle"; import { createArtifactTools } from "../artifacts/tools.ts"; -import { createSkillTools } from "../skills/tools.ts"; +import { createSkillTools } from "@corbits-builder/tools-skills"; import { roleInference } from "./role-inference.ts"; import { compactingDirector, diff --git a/apps/hub/src/workflows/entries/coordinator.ts b/apps/hub/src/workflows/entries/coordinator.ts index d0077150..efd6e6d7 100644 --- a/apps/hub/src/workflows/entries/coordinator.ts +++ b/apps/hub/src/workflows/entries/coordinator.ts @@ -2,7 +2,7 @@ import { createCoordinatorAgent } from "@corbits-builder/coordinator/agent"; import { createCoordinatorWorkflow } from "@corbits-builder/coordinator"; import { createArtifactTools } from "../../artifacts/tools.ts"; import { createDeployTools } from "../../projects/deploy-tools.ts"; -import { createSkillTools } from "../../skills/tools.ts"; +import { createSkillTools } from "@corbits-builder/tools-skills"; import { roleInference } from "../role-inference.ts"; import { compactingDirector, diff --git a/apps/hub/src/workflows/entries/writer.ts b/apps/hub/src/workflows/entries/writer.ts index 9894de26..e54f800b 100644 --- a/apps/hub/src/workflows/entries/writer.ts +++ b/apps/hub/src/workflows/entries/writer.ts @@ -1,7 +1,7 @@ import { createWriterAgent } from "@corbits-builder/writer/agent"; import { createWriterWorkflow } from "@corbits-builder/writer"; import { createArtifactTools } from "../../artifacts/tools.ts"; -import { createSkillTools } from "../../skills/tools.ts"; +import { createSkillTools } from "@corbits-builder/tools-skills"; import { roleInference } from "../role-inference.ts"; import { compactingDirector, diff --git a/apps/hub/src/workflows/project-scope.ts b/apps/hub/src/workflows/project-scope.ts index 7ed53d54..121c38b1 100644 --- a/apps/hub/src/workflows/project-scope.ts +++ b/apps/hub/src/workflows/project-scope.ts @@ -1,47 +1,13 @@ -import { HUB_CREDENTIAL_HANDLE } from "@intx/types/credential-delivery"; -import type { RuntimeCapabilities } from "@intx/types/runtime-capabilities"; -import { - WorkflowProjectScope, - type WorkflowRole, -} from "@corbits-builder/schemas/installed-workflows"; import { getLogger } from "@intx/log"; +import { projectScopeCode } from "@corbits-builder/tools-hub"; import { requestErrorMetadata } from "../request-error.ts"; -import { WorkflowHttpError, workflowHttpJson } from "./http-json.ts"; const log = getLogger(["corbits-builder", "hub", "workflow-project-scope"]); -const SAFE_CODE = /^[a-z0-9_:]+$/; -// A deployed agent keeps the bundle it started with; a field a newer hub adds must not refuse its scope. -const HubProjectScope = WorkflowProjectScope.onDeepUndeclaredKey("ignore"); -// The model sees only the original code (never a refusal body or arbitrary message); the cause stays in the hub log. export function projectScopeFailure(callId: string, error: unknown) { log.error( "Workflow project scope or tool call failed", requestErrorMetadata(error), ); - const code = - error instanceof WorkflowHttpError && error.refusalCode - ? error.refusalCode - : error instanceof Error && SAFE_CODE.test(error.message) - ? error.message - : "workflow_project_scope_unavailable"; - return { callId, content: code, isError: true }; -} - -export async function resolveWorkflowProjectScope( - capabilities: RuntimeCapabilities, - role: WorkflowRole, - signal: AbortSignal, -) { - const scope = HubProjectScope.assert( - await workflowHttpJson({ - capabilities, - handle: HUB_CREDENTIAL_HANDLE, - path: "/api/workflow-project-scope", - maxResponseBytes: 16384, - signal, - }), - ); - if (scope.role !== role) throw new Error("workflow_project_role_refused"); - return scope; + return { callId, content: projectScopeCode(error), isError: true }; } diff --git a/bun.lock b/bun.lock index 09de2913..2840e89a 100644 --- a/bun.lock +++ b/bun.lock @@ -67,6 +67,8 @@ "@corbits-builder/designer": "workspace:*", "@corbits-builder/reviewer": "workspace:*", "@corbits-builder/schemas": "workspace:*", + "@corbits-builder/tools-hub": "workspace:*", + "@corbits-builder/tools-skills": "workspace:*", "@corbits-builder/writer": "workspace:*", "@corbits/agent-token": "0.2.2", "@corbits/artifacts": "0.3.0", @@ -158,6 +160,24 @@ "arktype": "catalog:", }, }, + "packages/tools-hub": { + "name": "@corbits-builder/tools-hub", + "version": "0.1.0-rc.2", + "dependencies": { + "@corbits-builder/schemas": "workspace:*", + "@intx/types": "workspace:*", + }, + }, + "packages/tools-skills": { + "name": "@corbits-builder/tools-skills", + "version": "0.1.0-rc.2", + "dependencies": { + "@corbits-builder/designer": "workspace:*", + "@corbits-builder/writer": "workspace:*", + "@intx/agent": "workspace:*", + "arktype": "catalog:", + }, + }, "vendor/interchange/apps/sidecar": { "name": "@intx/sidecar-app", "version": "0.5.0", @@ -732,6 +752,10 @@ "@corbits-builder/schemas": ["@corbits-builder/schemas@workspace:packages/schemas"], + "@corbits-builder/tools-hub": ["@corbits-builder/tools-hub@workspace:packages/tools-hub"], + + "@corbits-builder/tools-skills": ["@corbits-builder/tools-skills@workspace:packages/tools-skills"], + "@corbits-builder/web": ["@corbits-builder/web@workspace:apps/web"], "@corbits-builder/writer": ["@corbits-builder/writer@workspace:workflows/writer"], diff --git a/docs/DEV.md b/docs/DEV.md index ad399fc0..982c2f00 100644 --- a/docs/DEV.md +++ b/docs/DEV.md @@ -7,16 +7,18 @@ running with Docker Compose is in ## Architecture in brief -| Path | What it is | -| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `vendor/interchange` | [Interchange](https://github.com/faremeter/interchange) at the commit in `vendor/VENDORED_REVISION`, plus local patches recorded in `vendor/interchange.manifest.json`. It owns the agent loop, mail, spawning, approvals, credentials, sidecars and run lifecycle. | -| `apps/hub` | The server: a Postgres control plane, tenancy and sign-in, documents (versioned artifacts with content hashes), approvals and the glue that starts each role. It runs Interchange in-process. | -| `packages/client` | A typed client for the hub's API (`@corbits-builder/client`). It never touches the database or Interchange internals. | -| `apps/web` | The UI: Vite and React over that client, with its own small router in `src/routes.ts`. | -| `workflows/*` | One package per role: its agent, prompt and workflow definition; the hub supplies its tools. | -| `packages/schemas` | Types and schemas shared by the hub, the client and the workflows. | -| `scripts/` | Dev and verification scripts: pin and vendor checks, the commit check, the Google sign-in emulator and React UI API generation. | -| `deploy/` | The Dockerfile and Compose files ([SELF_HOST.md](https://github.com/corbitsdev/corbits-builder/blob/main/docs/SELF_HOST.md)). | +| Path | What it is | +| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `vendor/interchange` | [Interchange](https://github.com/faremeter/interchange) at the commit in `vendor/VENDORED_REVISION`, plus local patches recorded in `vendor/interchange.manifest.json`. It owns the agent loop, mail, spawning, approvals, credentials, sidecars and run lifecycle. | +| `apps/hub` | The server: a Postgres control plane, tenancy and sign-in, documents (versioned artifacts with content hashes), approvals and the glue that starts each role. It runs Interchange in-process. | +| `packages/client` | A typed client for the hub's API (`@corbits-builder/client`). It never touches the database or Interchange internals. | +| `apps/web` | The UI: Vite and React over that client, with its own small router in `src/routes.ts`. | +| `workflows/*` | One package per role: its agent, prompt and workflow definition; the hub supplies its tools. | +| `packages/schemas` | Types and schemas shared by the hub, the client and the workflows. | +| `packages/tools-hub` | The helpers the hub's agent tools use to call the hub and resolve a run's project (`@corbits-builder/tools-hub`). | +| `packages/tools-skills` | The `read_skill` tool that gives agents the designer's and writer's skills (`@corbits-builder/tools-skills`). | +| `scripts/` | Dev and verification scripts: pin and vendor checks, the commit check, the Google sign-in emulator and React UI API generation. | +| `deploy/` | The Dockerfile and Compose files ([SELF_HOST.md](https://github.com/corbitsdev/corbits-builder/blob/main/docs/SELF_HOST.md)). | The roles: diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md index 6cb077d1..c775f19d 100644 --- a/docs/IMPLEMENTATION.md +++ b/docs/IMPLEMENTATION.md @@ -78,7 +78,7 @@ run's own credential: - design filing: `file_design_option` ([apps/hub/src/artifacts/design-tools.ts](https://github.com/corbitsdev/corbits-builder/blob/main/apps/hub/src/artifacts/design-tools.ts)); - skills: `read_skill` - ([apps/hub/src/skills/tools.ts](https://github.com/corbitsdev/corbits-builder/blob/main/apps/hub/src/skills/tools.ts)); + ([packages/tools-skills](https://github.com/corbitsdev/corbits-builder/tree/main/packages/tools-skills)); - workspace tools for the designer and coder (files, shell, search, package installs, app status, browser) ([apps/hub/src/builds/](https://github.com/corbitsdev/corbits-builder/blob/main/apps/hub/src/builds)). @@ -111,22 +111,24 @@ Environment files, installs and build output never leave the workspace. ## Package layout -| Path | Package | What it holds | -| ------------------------- | ------------------------------ | ---------------------------------------------------------------- | -| `apps/hub` | `@corbits-builder/hub` | The server: routes, services, hub tools, provisioning, live app. | -| `packages/client` | `@corbits-builder/client` | The typed client for the hub's API. | -| `apps/web` | `@corbits-builder/web` | The interface, built only on the client and the schemas. | -| `workflows/coordinator` | `@corbits-builder/coordinator` | The coordinator's agent, prompt and workflow. | -| `workflows/writer` | `@corbits-builder/writer` | The document writer and its per-kind skills. | -| `workflows/reviewer` | `@corbits-builder/reviewer` | The document reviewer. | -| `workflows/designer` | `@corbits-builder/designer` | The designer, its starter, themes and skills. | -| `workflows/coder` | `@corbits-builder/coder` | The coder, its app starter and `check-app`. | -| `packages/schemas` | `@corbits-builder/schemas` | Types and schemas shared by the hub, client and workflows. | -| `vendor/interchange` | | Interchange at the pinned revision. | -| `deploy/` | | The Dockerfile and Compose files. | -| `scripts/` | | Dev and verification scripts. | -| `skills/` | | Agent skills to install Builder and drive it through its API. | -| `test/integration`, `e2e` | | Cross-module and full-stack tests. | +| Path | Package | What it holds | +| ------------------------- | ------------------------------- | ---------------------------------------------------------------- | +| `apps/hub` | `@corbits-builder/hub` | The server: routes, services, hub tools, provisioning, live app. | +| `packages/client` | `@corbits-builder/client` | The typed client for the hub's API. | +| `apps/web` | `@corbits-builder/web` | The interface, built only on the client and the schemas. | +| `workflows/coordinator` | `@corbits-builder/coordinator` | The coordinator's agent, prompt and workflow. | +| `workflows/writer` | `@corbits-builder/writer` | The document writer and its per-kind skills. | +| `workflows/reviewer` | `@corbits-builder/reviewer` | The document reviewer. | +| `workflows/designer` | `@corbits-builder/designer` | The designer, its starter, themes and skills. | +| `workflows/coder` | `@corbits-builder/coder` | The coder, its app starter and `check-app`. | +| `packages/schemas` | `@corbits-builder/schemas` | Types and schemas shared by the hub, client and workflows. | +| `packages/tools-hub` | `@corbits-builder/tools-hub` | The hub HTTP/JSON and project-scope helpers agent tools use. | +| `packages/tools-skills` | `@corbits-builder/tools-skills` | The `read_skill` tool over the designer and writer skills. | +| `vendor/interchange` | | Interchange at the pinned revision. | +| `deploy/` | | The Dockerfile and Compose files. | +| `scripts/` | | Dev and verification scripts. | +| `skills/` | | Agent skills to install Builder and drive it through its API. | +| `test/integration`, `e2e` | | Cross-module and full-stack tests. | Workflow packages declare their agents and workflows with Interchange's definition exports (`@intx/agent/authoring`, `@intx/workflow/definition`). diff --git a/packages/tools-hub/README.md b/packages/tools-hub/README.md new file mode 100644 index 00000000..698332e3 --- /dev/null +++ b/packages/tools-hub/README.md @@ -0,0 +1,30 @@ +# Tools hub + +`@corbits-builder/tools-hub`: the helpers agent tools use to reach the +hub from inside a run. `workflowHttpJson` calls a hub route through the run's +mediated hub credential and throws `WorkflowHttpError` on a refusal, +`resolveWorkflowProjectScope` reads the project a run belongs to, and +`projectScopeCode` turns a failure into the only code the model sees. + +## Where it fits + +Pure functions with no tool definitions and no runtime of their own. It +imports only [`packages/schemas`][schemas] and Interchange's types. The +[hub][hub]'s agent tools use it, and the workflow bundles carry it with +those tools. + +## Layout + +- `src/index.ts`: the package's exports. +- `src/http-json.ts`: `workflowHttpJson`, `mediatedHttpJson` and + `WorkflowHttpError`. +- `src/project-scope.ts`: `resolveWorkflowProjectScope` and + `projectScopeCode`. + +## Working on it + +Refusal bodies never reach the model; keep any new field to a public code. +`bun run typecheck` checks every importer and `bun run test` runs the tests. + +[hub]: https://github.com/corbitsdev/corbits-builder/tree/main/apps/hub +[schemas]: https://github.com/corbitsdev/corbits-builder/tree/main/packages/schemas diff --git a/packages/tools-hub/package.json b/packages/tools-hub/package.json new file mode 100644 index 00000000..25b12461 --- /dev/null +++ b/packages/tools-hub/package.json @@ -0,0 +1,15 @@ +{ + "name": "@corbits-builder/tools-hub", + "version": "0.1.0-rc.2", + "private": true, + "license": "SEE LICENSE IN ../../LICENSE.md", + "author": "Sawyer Cutler ", + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "dependencies": { + "@corbits-builder/schemas": "workspace:*", + "@intx/types": "workspace:*" + } +} diff --git a/apps/hub/src/workflows/http-json.ts b/packages/tools-hub/src/http-json.ts similarity index 100% rename from apps/hub/src/workflows/http-json.ts rename to packages/tools-hub/src/http-json.ts diff --git a/packages/tools-hub/src/index.ts b/packages/tools-hub/src/index.ts new file mode 100644 index 00000000..ce9147e6 --- /dev/null +++ b/packages/tools-hub/src/index.ts @@ -0,0 +1,9 @@ +export { + mediatedHttpJson, + WorkflowHttpError, + workflowHttpJson, +} from "./http-json.ts"; +export { + projectScopeCode, + resolveWorkflowProjectScope, +} from "./project-scope.ts"; diff --git a/packages/tools-hub/src/project-scope.test.ts b/packages/tools-hub/src/project-scope.test.ts new file mode 100644 index 00000000..47ed721b --- /dev/null +++ b/packages/tools-hub/src/project-scope.test.ts @@ -0,0 +1,17 @@ +import { expect, test } from "bun:test"; +import { WorkflowHttpError } from "./http-json.ts"; +import { projectScopeCode } from "./project-scope.ts"; + +test("project scope failures expose only a refusal code, a safe code or the fallback", () => { + expect( + projectScopeCode( + new WorkflowHttpError(403, "unspecified", "project_access_refused"), + ), + ).toBe("project_access_refused"); + expect(projectScopeCode(new Error("workflow_project_role_refused"))).toBe( + "workflow_project_role_refused", + ); + expect(projectScopeCode(new Error("ENOENT: /secret/path"))).toBe( + "workflow_project_scope_unavailable", + ); +}); diff --git a/packages/tools-hub/src/project-scope.ts b/packages/tools-hub/src/project-scope.ts new file mode 100644 index 00000000..c3a4c198 --- /dev/null +++ b/packages/tools-hub/src/project-scope.ts @@ -0,0 +1,39 @@ +import { HUB_CREDENTIAL_HANDLE } from "@intx/types/credential-delivery"; +import type { RuntimeCapabilities } from "@intx/types/runtime-capabilities"; +import { + WorkflowProjectScope, + type WorkflowRole, +} from "@corbits-builder/schemas/installed-workflows"; +import { WorkflowHttpError, workflowHttpJson } from "./http-json.ts"; + +// A deployed agent keeps the bundle it started with; a field a newer hub adds must not refuse its scope. +const HubProjectScope = WorkflowProjectScope.onDeepUndeclaredKey("ignore"); + +export async function resolveWorkflowProjectScope( + capabilities: RuntimeCapabilities, + role: WorkflowRole, + signal: AbortSignal, +) { + const scope = HubProjectScope.assert( + await workflowHttpJson({ + capabilities, + handle: HUB_CREDENTIAL_HANDLE, + path: "/api/workflow-project-scope", + maxResponseBytes: 16384, + signal, + }), + ); + if (scope.role !== role) throw new Error("workflow_project_role_refused"); + return scope; +} + +const SAFE_CODE = /^[a-z0-9_:]+$/; + +// The model sees only the original code (never a refusal body or arbitrary message); the cause stays in the hub log. +export function projectScopeCode(error: unknown): string { + if (error instanceof WorkflowHttpError && error.refusalCode) + return error.refusalCode; + if (error instanceof Error && SAFE_CODE.test(error.message)) + return error.message; + return "workflow_project_scope_unavailable"; +} diff --git a/packages/tools-skills/README.md b/packages/tools-skills/README.md new file mode 100644 index 00000000..0b95563c --- /dev/null +++ b/packages/tools-skills/README.md @@ -0,0 +1,28 @@ +# Tools skills + +`@corbits-builder/tools-skills`: the `read_skill` tool. `createSkillTools` +takes the skill names a role may read and returns a tool bundle whose +description lists each skill and when to use it; a call returns the skill's +`SKILL.md` or one of its reference files, in numbered parts when long. + +## Where it fits + +The skills come from the [designer][designer] and [writer][writer] packages +through Bun macros, so they ship inside each workflow bundle. The [hub][hub] +attaches the tool to the coordinator, writer and coder. + +## Layout + +- `src/index.ts`: the package's exports. +- `src/read-skill.ts`: `readSkill`, the call's logic over a skills map. +- `src/definitions.ts`: `readSkillDefinition`, the tool definition. +- `src/tools.ts`: `createSkillTools`, the `defineTool` bundle. + +## Working on it + +The tool name and bundle id form the grant name; keep both stable. +`bun run typecheck` checks every importer. + +[designer]: https://github.com/corbitsdev/corbits-builder/tree/main/workflows/designer +[hub]: https://github.com/corbitsdev/corbits-builder/tree/main/apps/hub +[writer]: https://github.com/corbitsdev/corbits-builder/tree/main/workflows/writer diff --git a/packages/tools-skills/package.json b/packages/tools-skills/package.json new file mode 100644 index 00000000..2832a944 --- /dev/null +++ b/packages/tools-skills/package.json @@ -0,0 +1,17 @@ +{ + "name": "@corbits-builder/tools-skills", + "version": "0.1.0-rc.2", + "private": true, + "license": "SEE LICENSE IN ../../LICENSE.md", + "author": "Sawyer Cutler ", + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "dependencies": { + "@corbits-builder/designer": "workspace:*", + "@corbits-builder/writer": "workspace:*", + "@intx/agent": "workspace:*", + "arktype": "catalog:" + } +} diff --git a/packages/tools-skills/src/definitions.ts b/packages/tools-skills/src/definitions.ts new file mode 100644 index 00000000..a93cc4b1 --- /dev/null +++ b/packages/tools-skills/src/definitions.ts @@ -0,0 +1,15 @@ +import { describeSkill, ReadSkill, type Skills } from "./read-skill.ts"; + +// Agent Skills disclosed progressively: the tool description lists each skill's name and when to +// use it; a call returns the skill's instructions, or one of the reference files they point to. +export const readSkillDefinition = ( + skills: Skills, + names: readonly string[], +) => ({ + name: "read_skill", + description: [ + "Read a skill: tested instructions for one kind of work. Pass name to get its instructions; pass file as well to read one of the reference files they name. A long file comes in numbered parts, and part reads the next one.", + ...names.map((name) => `- ${name}: ${describeSkill(skills, name)}`), + ].join("\n"), + inputSchema: { ...ReadSkill.toJsonSchema() }, +}); diff --git a/packages/tools-skills/src/index.ts b/packages/tools-skills/src/index.ts new file mode 100644 index 00000000..2610dff8 --- /dev/null +++ b/packages/tools-skills/src/index.ts @@ -0,0 +1 @@ +export { createSkillTools } from "./tools.ts"; diff --git a/packages/tools-skills/src/read-skill.ts b/packages/tools-skills/src/read-skill.ts new file mode 100644 index 00000000..ccecd90e --- /dev/null +++ b/packages/tools-skills/src/read-skill.ts @@ -0,0 +1,57 @@ +import { type } from "arktype"; + +export type Skills = Record>; + +export const ReadSkill = type({ + "+": "reject", + name: "string > 0", + "file?": "string > 0", + "part?": "number.integer > 0", +}); +// A tool result over 10000 characters is spilled to a file, so a skill file is read in parts that +// fit; a designer reading back a spilled 12000-character SKILL.md never reached its end. +const PART_CHARS = 8000; + +export function describeSkill(skills: Skills, name: string): string { + const description = skills[name]?.["SKILL.md"] + ?.match(/^description:\s*(.+)$/m)?.[1] + ?.trim(); + if (!description) throw new Error(`skill ${name} has no description`); + return description; +} + +export function readSkill( + skills: Skills, + names: readonly string[], + args: unknown, +): { content: string; isError?: true } { + const input = ReadSkill(args); + if (input instanceof type.errors) + return { content: input.summary, isError: true }; + // Writers asked for problem_brief and product_requirements, the document kinds their task + // names, 13 times in 20 eval runs; a skill is named for its kind with hyphens. + const name = input.name.replaceAll("_", "-"); + const skill = names.includes(name) ? skills[name] : undefined; + if (!skill) + return { + content: `no skill named ${input.name}; skills: ${names.join(", ")}`, + isError: true, + }; + const file = input.file ?? "SKILL.md"; + const content = Object.hasOwn(skill, file) ? skill[file] : undefined; + if (content === undefined) + return { + content: `${input.name} has no file ${file}; files: ${Object.keys(skill).join(", ")}`, + isError: true, + }; + const parts = Math.max(1, Math.ceil(content.length / PART_CHARS)); + if (parts === 1) return { content }; + const part = Math.min(input.part ?? 1, parts); + const slice = content.slice((part - 1) * PART_CHARS, part * PART_CHARS); + return { + content: + part < parts + ? `${slice}\n[part ${part} of ${parts}; read_skill with part ${part + 1} for the rest]` + : `${slice}\n[part ${part} of ${parts}, the end]`, + }; +} diff --git a/packages/tools-skills/src/tools.ts b/packages/tools-skills/src/tools.ts new file mode 100644 index 00000000..5356c1cb --- /dev/null +++ b/packages/tools-skills/src/tools.ts @@ -0,0 +1,24 @@ +import { defineTool } from "@intx/agent/authoring"; +import { designSkills } from "@corbits-builder/designer/skills" with { type: "macro" }; +import { documentSkills } from "@corbits-builder/writer/skills" with { type: "macro" }; +import { readSkillDefinition } from "./definitions.ts"; +import { readSkill } from "./read-skill.ts"; + +const SKILLS = { ...designSkills(), ...documentSkills() }; + +export function createSkillTools(names: readonly string[]) { + for (const name of names) + if (!SKILLS[name]) throw new Error(`unknown_skill: ${name}`); + const definition = readSkillDefinition(SKILLS, names); + return defineTool({ + id: "@corbits-builder/skills/tools", + definitions: [{ name: definition.name }], + factory: () => ({ + definitions: [definition], + run: async (call) => ({ + callId: call.id, + ...readSkill(SKILLS, names, call.arguments), + }), + }), + }); +}