diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 50419b5a..1c4aa0d7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -142,8 +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`, `packages/schemas`, `packages/tools-design` and -`packages/tools-hub` package.json files, run +`packages/client`, `packages/schemas`, `packages/tools-artifacts`, +`packages/tools-design` and `packages/tools-hub` 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 965ac9d6..ad46722a 100644 --- a/apps/hub/package.json +++ b/apps/hub/package.json @@ -20,6 +20,7 @@ "@corbits-builder/designer": "workspace:*", "@corbits-builder/reviewer": "workspace:*", "@corbits-builder/schemas": "workspace:*", + "@corbits-builder/tools-artifacts": "workspace:*", "@corbits-builder/tools-design": "workspace:*", "@corbits-builder/tools-hub": "workspace:*", "@corbits-builder/writer": "workspace:*", diff --git a/apps/hub/src/artifacts/tools.ts b/apps/hub/src/artifacts/tools.ts index 2ab17b6c..ef4a1d7b 100644 --- a/apps/hub/src/artifacts/tools.ts +++ b/apps/hub/src/artifacts/tools.ts @@ -1,57 +1,5 @@ -import { type } from "arktype"; -import { - defineTool, - type AnnotatedToolFactory, - type BaseEnv, -} from "@intx/agent/authoring"; -import { HUB_CREDENTIAL_HANDLE } from "@intx/types/credential-delivery"; -import type { RuntimeCapabilities } from "@intx/types/runtime-capabilities"; -import { - ArtifactContent, - DocumentWrite, - ArtifactList, - ArtifactRead, - ArtifactReceipt, -} from "@corbits-builder/schemas/artifact"; -import type { WorkflowRole } from "@corbits-builder/schemas/installed-workflows"; -import { unpackDesign } from "./design-bundle.ts"; +import { WorkflowHttpError } from "@corbits-builder/tools-hub"; import { projectScopeFailure } from "../workflows/project-scope.ts"; -import { - resolveWorkflowProjectScope, - workflowHttpJson, - WorkflowHttpError, -} from "@corbits-builder/tools-hub"; - -interface ArtifactToolEnv extends BaseEnv { - capabilities: RuntimeCapabilities; -} - -const MAX_RESULT_BYTES = 300 * 1024; - -// A deployed agent keeps the tool bundle it started with while the hub moves on; a field a newer -// hub adds must not fail every read of an older agent. -export const HubArtifactList = ArtifactList.onDeepUndeclaredKey("ignore"); -export const HubArtifactReceipt = ArtifactReceipt.onDeepUndeclaredKey("ignore"); -const HubArtifactContent = ArtifactContent.onDeepUndeclaredKey("ignore"); - -// A tool result holds at most 10000 characters (inference size-cap), and a cut document sends agents -// re-reading until the doom guard fails the run, so a long document is returned in parts that fit, -// escaped JSON and metadata included. -const PART_CHARS = 7000; -const ToolRead = ArtifactRead.merge({ "part?": "number.integer > 0" }); - -const readDefinition = { - name: "read_artifact", - description: - "See which project documents exist, or read one.\nWith no arguments it lists: each document's artifactId, kind, title, current version, sha256, approvedVersion, the version a person approved, and openQuestionCount, the questions its current version leaves open for the person; inFlight, the writer, reviewer, coder and designer runs still open, each with its status, deployed until its first assignment arrives and running after, and each working (mid-turn) or waiting (parked for your next mail), with progress once it has acted: its turns so far, its latest tool calls as tool and target, the files it has written, and lastActivityAt; failed, the latest runs that stopped on an error, with when and why, which deliver nothing more; awaitingApproval, the kinds of filed documents whose current version waits on the person's approval; and preferenceGuidance, how the person has said they want to work.\nWith a kind or an artifactId it returns that document's content at its current version, or at the given version, with the openQuestions that version leaves for the person; a long document comes in numbered parts, and part reads the next one.", - inputSchema: { ...ToolRead.toJsonSchema() }, -}; -const writeDefinition = { - name: "write_artifact", - description: - "Write the next version of the project document of one kind. The hub assigns its id, version and sha256 and returns them. Writing a version is not approval.\nopenQuestions lists each question in the document's Open questions section that the person must answer: a short id, the question as they would read it, whyItBlocks, and suggestedAnswers, your recommendation first. A version written without openQuestions leaves none open.\nOn the approaches document, approaches carries the approaches it compares, for the person to see side by side: each with a short id, its name, a one-line summary, bestFor, its pros, cons and risks as short phrases, and its effort; recommended names the id of the one you recommend.", - inputSchema: { ...DocumentWrite.toJsonSchema() }, -}; export function failure(callId: string, error: unknown) { if (error instanceof WorkflowHttpError && error.status) @@ -67,158 +15,3 @@ export function failure(callId: string, error: unknown) { }; return projectScopeFailure(callId, error); } - -export function createArtifactTools( - role: WorkflowRole, -): AnnotatedToolFactory { - // A writer's or reviewer's task carries every document it needs in full; a read tool sends - // writers looking for documents that do not exist. - const definitions = - role === "coordinator" - ? [readDefinition, writeDefinition] - : role === "writer" - ? [writeDefinition] - : [readDefinition]; - return defineTool({ - id: "@corbits-builder/artifacts/tools", - credentials: [{ handle: HUB_CREDENTIAL_HANDLE }], - // The coder's read of the design unpacks it into its workspace. - requires: role === "coder" ? ["capabilities", "toolCwd"] : ["capabilities"], - definitions: definitions.map(({ name }) => ({ name })), - factory: (env) => { - // Filing the same document again makes a new identical version each time until the doom - // guard fails the run; say the earlier write already landed. - const written = new Map(); - return { - definitions, - run: async (call, signal) => { - let target: string | undefined; - if (!definitions.some(({ name }) => name === call.name)) - return { - callId: call.id, - content: "unknown_artifact_tool", - isError: true, - }; - try { - const scope = await resolveWorkflowProjectScope( - env.capabilities, - role, - signal, - ); - const base = - "/api/tenants/" + - encodeURIComponent(scope.tenantId) + - "/projects/" + - encodeURIComponent(scope.projectId) + - "/artifacts"; - const request = (path: string, body?: unknown) => - workflowHttpJson({ - capabilities: env.capabilities, - handle: HUB_CREDENTIAL_HANDLE, - path, - ...(body === undefined ? {} : { body }), - maxResponseBytes: MAX_RESULT_BYTES, - signal, - }); - if (call.name === "write_artifact") { - const input = DocumentWrite(call.arguments); - if (input instanceof type.errors) - return { - callId: call.id, - content: "invalid_arguments: " + input.summary, - isError: true, - }; - // The coordinator's writers own every kind but the brief, so the documents the person - // decides on come from them. - if (role === "coordinator" && input.kind !== "problem_brief") - return { - callId: call.id, - content: `brief_only: you write the problem brief; deploy_writer has a writer write the ${input.kind}`, - isError: true, - }; - const body = JSON.stringify(input); - const earlier = written.get(input.kind); - if (earlier?.body === body) - return { - callId: call.id, - content: `unchanged: version ${earlier.version} of this ${input.kind} already has exactly this content; the earlier write succeeded`, - }; - const receipt = HubArtifactReceipt.assert( - await request(base, input), - ); - written.set(input.kind, { - body, - version: receipt.artifact.version, - }); - return { - callId: call.id, - content: JSON.stringify(receipt.artifact), - }; - } - const input = ToolRead(call.arguments ?? {}); - if (input instanceof type.errors) - return { - callId: call.id, - content: "invalid_arguments: " + input.summary, - isError: true, - }; - target = input.kind ?? input.artifactId; - if (target === undefined) - return { - callId: call.id, - content: JSON.stringify( - HubArtifactList.assert(await request(base)), - ), - }; - const query = - input.version === undefined - ? "" - : "?version=" + String(input.version); - // The structured approaches repeat the document's prose for the person's side-by-side - // view and would push a part past the tool-output cap. - const { approaches: _, ...document } = HubArtifactContent.assert( - await request(base + "/" + encodeURIComponent(target) + query), - ); - const workspace = - "toolCwd" in env && typeof env.toolCwd === "string" - ? env.toolCwd - : undefined; - if (role === "coder" && document.kind === "design" && workspace) { - const files = await unpackDesign(document.content, workspace); - // DESIGN.md runs past the host's tool-output cap, and a cut copy sends the coder - // re-reading until the doom guard fails the run; read_file pages through it. - return { - callId: call.id, - content: JSON.stringify({ - ...document, - content: `Version ${document.version} of the design is unpacked, read-only, in .builder: ${Object.keys(files).join(", ")}. Read .builder/DESIGN.md with read_file.`, - }), - }; - } - const parts = Math.max( - 1, - Math.ceil(document.content.length / PART_CHARS), - ); - const part = Math.min(input.part ?? 1, parts); - if (parts === 1) - return { callId: call.id, content: JSON.stringify(document) }; - return { - callId: call.id, - content: JSON.stringify({ - ...document, - part, - parts, - content: document.content.slice( - (part - 1) * PART_CHARS, - part * PART_CHARS, - ), - }), - }; - } catch (error) { - return failure(call.id, error); - } - }, - }; - }, - }); -} diff --git a/apps/hub/src/workflows/coder-package.ts b/apps/hub/src/workflows/coder-package.ts index 40d9ab9a..dcbeb559 100644 --- a/apps/hub/src/workflows/coder-package.ts +++ b/apps/hub/src/workflows/coder-package.ts @@ -7,7 +7,9 @@ import { import { browserCatalog } from "../builds/browser-catalog.ts" with { type: "macro" }; import { mcpTools } from "@corbits/mcp/interchange/client"; import { mail, type MailToolEnv } from "@intx/tools-mail/sidecar-bundle"; -import { createArtifactTools } from "../artifacts/tools.ts"; +import { createArtifactTools } from "@corbits-builder/tools-artifacts/sidecar-bundle"; +import { failure } from "../artifacts/tools.ts"; +import { unpackDesign } from "../artifacts/design-bundle.ts"; import { createSkillTools } from "../skills/tools.ts"; import { roleInference } from "./role-inference.ts"; import { @@ -46,7 +48,7 @@ export async function coderPackage( inference: { ...source, parameters }, // The project role child supplies the sandboxed workspace and executor. tools: [ - createArtifactTools("coder"), + createArtifactTools("coder", { failure, unpackDesign }), mail, createSkillTools(["react-ui", "stack-docs"]), web, diff --git a/apps/hub/src/workflows/entries/coordinator.ts b/apps/hub/src/workflows/entries/coordinator.ts index d0077150..569b78d7 100644 --- a/apps/hub/src/workflows/entries/coordinator.ts +++ b/apps/hub/src/workflows/entries/coordinator.ts @@ -1,6 +1,7 @@ import { createCoordinatorAgent } from "@corbits-builder/coordinator/agent"; import { createCoordinatorWorkflow } from "@corbits-builder/coordinator"; -import { createArtifactTools } from "../../artifacts/tools.ts"; +import { createArtifactTools } from "@corbits-builder/tools-artifacts/sidecar-bundle"; +import { failure } from "../../artifacts/tools.ts"; import { createDeployTools } from "../../projects/deploy-tools.ts"; import { createSkillTools } from "../../skills/tools.ts"; import { roleInference } from "../role-inference.ts"; @@ -25,7 +26,7 @@ const agent = createCoordinatorAgent({ capabilities: [], inference: { ...source, parameters }, tools: [ - createArtifactTools("coordinator"), + createArtifactTools("coordinator", { failure }), createDeployTools(), createSkillTools(["problem-brief"]), ], diff --git a/apps/hub/src/workflows/entries/writer.ts b/apps/hub/src/workflows/entries/writer.ts index 9894de26..f5bec130 100644 --- a/apps/hub/src/workflows/entries/writer.ts +++ b/apps/hub/src/workflows/entries/writer.ts @@ -1,6 +1,7 @@ import { createWriterAgent } from "@corbits-builder/writer/agent"; import { createWriterWorkflow } from "@corbits-builder/writer"; -import { createArtifactTools } from "../../artifacts/tools.ts"; +import { createArtifactTools } from "@corbits-builder/tools-artifacts/sidecar-bundle"; +import { failure } from "../../artifacts/tools.ts"; import { createSkillTools } from "../../skills/tools.ts"; import { roleInference } from "../role-inference.ts"; import { @@ -23,7 +24,7 @@ const agent = createWriterAgent({ capabilities: [], inference: { ...source, parameters }, tools: [ - createArtifactTools("writer"), + createArtifactTools("writer", { failure }), createSkillTools([ "constraints", "approaches", diff --git a/bun.lock b/bun.lock index fbf3fd5a..95deac45 100644 --- a/bun.lock +++ b/bun.lock @@ -67,6 +67,7 @@ "@corbits-builder/designer": "workspace:*", "@corbits-builder/reviewer": "workspace:*", "@corbits-builder/schemas": "workspace:*", + "@corbits-builder/tools-artifacts": "workspace:*", "@corbits-builder/tools-design": "workspace:*", "@corbits-builder/tools-hub": "workspace:*", "@corbits-builder/writer": "workspace:*", @@ -160,6 +161,17 @@ "arktype": "catalog:", }, }, + "packages/tools-artifacts": { + "name": "@corbits-builder/tools-artifacts", + "version": "0.1.0-rc.2", + "dependencies": { + "@corbits-builder/schemas": "workspace:*", + "@corbits-builder/tools-hub": "workspace:*", + "@intx/agent": "workspace:*", + "@intx/types": "workspace:*", + "arktype": "catalog:", + }, + }, "packages/tools-design": { "name": "@corbits-builder/tools-design", "version": "0.1.0-rc.2", @@ -753,6 +765,8 @@ "@corbits-builder/schemas": ["@corbits-builder/schemas@workspace:packages/schemas"], + "@corbits-builder/tools-artifacts": ["@corbits-builder/tools-artifacts@workspace:packages/tools-artifacts"], + "@corbits-builder/tools-design": ["@corbits-builder/tools-design@workspace:packages/tools-design"], "@corbits-builder/tools-hub": ["@corbits-builder/tools-hub@workspace:packages/tools-hub"], diff --git a/docs/DEV.md b/docs/DEV.md index e4e42821..3a3d2e6b 100644 --- a/docs/DEV.md +++ b/docs/DEV.md @@ -7,18 +7,19 @@ 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. | -| `packages/tools-design` | The designer's tools: filing a design option and the design check (`@corbits-builder/tools-design`). | -| `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`). | -| `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-design` | The designer's tools: filing a design option and the design check (`@corbits-builder/tools-design`). | +| `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-artifacts` | The read and write tools agents use for project documents (`@corbits-builder/tools-artifacts`). | +| `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 86a0e49c..4e204da4 100644 --- a/docs/IMPLEMENTATION.md +++ b/docs/IMPLEMENTATION.md @@ -70,7 +70,7 @@ The tools agents call are tool packages that reach the hub's API with the run's own credential: - documents: `read_artifact`, `write_artifact` - ([apps/hub/src/artifacts/tools.ts](https://github.com/corbitsdev/corbits-builder/blob/main/apps/hub/src/artifacts/tools.ts)); + ([packages/tools-artifacts](https://github.com/corbitsdev/corbits-builder/tree/main/packages/tools-artifacts)); - hand-offs and decisions: `request_approval`, `ask_questions`, `deploy_writer`, `review_artifact`, `design_app`, `deploy_coder`, `instruct_coder` @@ -111,24 +111,25 @@ 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. | -| `packages/tools-design` | `@corbits-builder/tools-design` | The designer's design-option filing and design-check tools. | -| `packages/tools-hub` | `@corbits-builder/tools-hub` | The hub HTTP/JSON and project-scope helpers agent tools use. | -| `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-design` | `@corbits-builder/tools-design` | The designer's design-option filing and design-check tools. | +| `packages/tools-hub` | `@corbits-builder/tools-hub` | The hub HTTP/JSON and project-scope helpers agent tools use. | +| `packages/tools-artifacts` | `@corbits-builder/tools-artifacts` | The document read and write tools and their logic. | +| `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-artifacts/README.md b/packages/tools-artifacts/README.md new file mode 100644 index 00000000..1a7b4924 --- /dev/null +++ b/packages/tools-artifacts/README.md @@ -0,0 +1,33 @@ +# Tools artifacts + +`@corbits-builder/tools-artifacts`: the tools agents use to read and write +a project's documents. The main export holds the logic as plain functions +a workflow action can call: `projectArtifacts` and `documentPart`. +`./sidecar-bundle` exports `createArtifactTools`, the `defineTool` bundle +behind `read_artifact` and `write_artifact`. + +## Where it fits + +It reaches the hub through [`packages/tools-hub`][tools-hub] and checks +what it gets with [`packages/schemas`][schemas]. The [hub][hub]'s +workflow entries attach the bundle, passing in the hub's `failure`, which +logs a failed call and returns the code the model sees, and, for the +coder, `unpackDesign`. + +## Layout + +- `src/index.ts`: the logic exports. +- `src/artifacts.ts`: `projectArtifacts` (list, read and write against the + run's project) and `documentPart`, which splits a long document into + parts that fit a tool result. +- `src/definitions.ts`: the tool definitions and their input schema. +- `src/sidecar-bundle.ts`: `createArtifactTools`, the bundle per role. + +## Working on it + +The bundle id is part of every grant name (`tool:/`); keep it. +`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 +[tools-hub]: https://github.com/corbitsdev/corbits-builder/tree/main/packages/tools-hub diff --git a/packages/tools-artifacts/package.json b/packages/tools-artifacts/package.json new file mode 100644 index 00000000..2f28cf01 --- /dev/null +++ b/packages/tools-artifacts/package.json @@ -0,0 +1,19 @@ +{ + "name": "@corbits-builder/tools-artifacts", + "version": "0.1.0-rc.2", + "private": true, + "license": "SEE LICENSE IN ../../LICENSE.md", + "author": "Sawyer Cutler ", + "type": "module", + "exports": { + ".": "./src/index.ts", + "./sidecar-bundle": "./src/sidecar-bundle.ts" + }, + "dependencies": { + "@corbits-builder/schemas": "workspace:*", + "@corbits-builder/tools-hub": "workspace:*", + "@intx/agent": "workspace:*", + "@intx/types": "workspace:*", + "arktype": "catalog:" + } +} diff --git a/packages/tools-artifacts/src/artifacts.ts b/packages/tools-artifacts/src/artifacts.ts new file mode 100644 index 00000000..095d46ab --- /dev/null +++ b/packages/tools-artifacts/src/artifacts.ts @@ -0,0 +1,77 @@ +import { HUB_CREDENTIAL_HANDLE } from "@intx/types/credential-delivery"; +import type { RuntimeCapabilities } from "@intx/types/runtime-capabilities"; +import { + ArtifactContent, + ArtifactList, + type DocumentWrite, +} from "@corbits-builder/schemas/artifact"; +import type { WorkflowRole } from "@corbits-builder/schemas/installed-workflows"; +import { + HubArtifactReceipt, + resolveWorkflowProjectScope, + workflowHttpJson, +} from "@corbits-builder/tools-hub"; + +const MAX_RESULT_BYTES = 300 * 1024; + +// A deployed agent keeps the tool bundle it started with while the hub moves on; a field a newer +// hub adds must not fail every read of an older agent. +const HubArtifactList = ArtifactList.onDeepUndeclaredKey("ignore"); +const HubArtifactContent = ArtifactContent.onDeepUndeclaredKey("ignore"); + +// A tool result holds at most 10000 characters (inference size-cap), and a cut document sends agents +// re-reading until the doom guard fails the run, so a long document is returned in parts that fit, +// escaped JSON and metadata included. +const PART_CHARS = 7000; + +export async function projectArtifacts( + capabilities: RuntimeCapabilities, + role: WorkflowRole, + signal: AbortSignal, +) { + const scope = await resolveWorkflowProjectScope(capabilities, role, signal); + const base = + "/api/tenants/" + + encodeURIComponent(scope.tenantId) + + "/projects/" + + encodeURIComponent(scope.projectId) + + "/artifacts"; + const request = (path: string, body?: unknown) => + workflowHttpJson({ + capabilities, + handle: HUB_CREDENTIAL_HANDLE, + path, + ...(body === undefined ? {} : { body }), + maxResponseBytes: MAX_RESULT_BYTES, + signal, + }); + return { + list: async () => HubArtifactList.assert(await request(base)), + write: async (input: typeof DocumentWrite.infer) => + HubArtifactReceipt.assert(await request(base, input)), + read: async (target: string, version?: number) => { + const query = version === undefined ? "" : "?version=" + String(version); + // The structured approaches repeat the document's prose for the person's side-by-side + // view and would push a part past the tool-output cap. + const { approaches: _, ...document } = HubArtifactContent.assert( + await request(base + "/" + encodeURIComponent(target) + query), + ); + return document; + }, + }; +} + +export function documentPart( + document: T, + requested = 1, +) { + const parts = Math.max(1, Math.ceil(document.content.length / PART_CHARS)); + if (parts === 1) return document; + const part = Math.min(requested, parts); + return { + ...document, + part, + parts, + content: document.content.slice((part - 1) * PART_CHARS, part * PART_CHARS), + }; +} diff --git a/packages/tools-artifacts/src/definitions.ts b/packages/tools-artifacts/src/definitions.ts new file mode 100644 index 00000000..737aa164 --- /dev/null +++ b/packages/tools-artifacts/src/definitions.ts @@ -0,0 +1,16 @@ +import { ArtifactRead, DocumentWrite } from "@corbits-builder/schemas/artifact"; + +export const ToolRead = ArtifactRead.merge({ "part?": "number.integer > 0" }); + +export const readDefinition = { + name: "read_artifact", + description: + "See which project documents exist, or read one.\nWith no arguments it lists: each document's artifactId, kind, title, current version, sha256, approvedVersion, the version a person approved, and openQuestionCount, the questions its current version leaves open for the person; inFlight, the writer, reviewer, coder and designer runs still open, each with its status, deployed until its first assignment arrives and running after, and each working (mid-turn) or waiting (parked for your next mail), with progress once it has acted: its turns so far, its latest tool calls as tool and target, the files it has written, and lastActivityAt; failed, the latest runs that stopped on an error, with when and why, which deliver nothing more; awaitingApproval, the kinds of filed documents whose current version waits on the person's approval; and preferenceGuidance, how the person has said they want to work.\nWith a kind or an artifactId it returns that document's content at its current version, or at the given version, with the openQuestions that version leaves for the person; a long document comes in numbered parts, and part reads the next one.", + inputSchema: { ...ToolRead.toJsonSchema() }, +}; +export const writeDefinition = { + name: "write_artifact", + description: + "Write the next version of the project document of one kind. The hub assigns its id, version and sha256 and returns them. Writing a version is not approval.\nopenQuestions lists each question in the document's Open questions section that the person must answer: a short id, the question as they would read it, whyItBlocks, and suggestedAnswers, your recommendation first. A version written without openQuestions leaves none open.\nOn the approaches document, approaches carries the approaches it compares, for the person to see side by side: each with a short id, its name, a one-line summary, bestFor, its pros, cons and risks as short phrases, and its effort; recommended names the id of the one you recommend.", + inputSchema: { ...DocumentWrite.toJsonSchema() }, +}; diff --git a/packages/tools-artifacts/src/index.ts b/packages/tools-artifacts/src/index.ts new file mode 100644 index 00000000..bf828063 --- /dev/null +++ b/packages/tools-artifacts/src/index.ts @@ -0,0 +1 @@ +export { documentPart, projectArtifacts } from "./artifacts.ts"; diff --git a/packages/tools-artifacts/src/sidecar-bundle.ts b/packages/tools-artifacts/src/sidecar-bundle.ts new file mode 100644 index 00000000..1877618d --- /dev/null +++ b/packages/tools-artifacts/src/sidecar-bundle.ts @@ -0,0 +1,151 @@ +import { type } from "arktype"; +import { + defineTool, + type AnnotatedToolFactory, + type BaseEnv, +} from "@intx/agent/authoring"; +import { HUB_CREDENTIAL_HANDLE } from "@intx/types/credential-delivery"; +import type { ToolResult } from "@intx/types/runtime"; +import type { RuntimeCapabilities } from "@intx/types/runtime-capabilities"; +import { DocumentWrite } from "@corbits-builder/schemas/artifact"; +import type { WorkflowRole } from "@corbits-builder/schemas/installed-workflows"; +import { documentPart, projectArtifacts } from "./artifacts.ts"; +import { readDefinition, ToolRead, writeDefinition } from "./definitions.ts"; + +interface ArtifactToolEnv extends BaseEnv { + capabilities: RuntimeCapabilities; +} + +export interface ArtifactToolDeps { + failure: (callId: string, error: unknown) => ToolResult; + unpackDesign?: ( + content: string, + workspace: string, + ) => Promise>; +} + +export function createArtifactTools( + role: WorkflowRole, + { failure, unpackDesign }: ArtifactToolDeps, +): AnnotatedToolFactory { + if (role === "coder" && unpackDesign === undefined) + throw new Error("coder_artifact_tools_need_unpack_design"); + // A writer's or reviewer's task carries every document it needs in full; a read tool sends + // writers looking for documents that do not exist. + const definitions = { + coordinator: [readDefinition, writeDefinition], + writer: [writeDefinition], + reviewer: [readDefinition], + coder: [readDefinition], + designer: [readDefinition], + }[role]; + return defineTool({ + id: "@corbits-builder/artifacts/tools", + credentials: [{ handle: HUB_CREDENTIAL_HANDLE }], + // The coder's read of the design unpacks it into its workspace. + requires: role === "coder" ? ["capabilities", "toolCwd"] : ["capabilities"], + definitions: definitions.map(({ name }) => ({ name })), + factory: (env) => { + // Filing the same document again makes a new identical version each time until the doom + // guard fails the run; say the earlier write already landed. + const written = new Map(); + return { + definitions, + run: async (call, signal) => { + if (!definitions.some(({ name }) => name === call.name)) + return { + callId: call.id, + content: "unknown_artifact_tool", + isError: true, + }; + try { + const artifacts = await projectArtifacts( + env.capabilities, + role, + signal, + ); + if (call.name === "write_artifact") { + const input = DocumentWrite(call.arguments); + if (input instanceof type.errors) + return { + callId: call.id, + content: "invalid_arguments: " + input.summary, + isError: true, + }; + // The coordinator's writers own every kind but the brief, so the documents the person + // decides on come from them. + if (role === "coordinator" && input.kind !== "problem_brief") + return { + callId: call.id, + content: `brief_only: you write the problem brief; deploy_writer has a writer write the ${input.kind}`, + isError: true, + }; + const body = JSON.stringify(input); + const earlier = written.get(input.kind); + if (earlier?.body === body) + return { + callId: call.id, + content: `unchanged: version ${earlier.version} of this ${input.kind} already has exactly this content; the earlier write succeeded`, + }; + const receipt = await artifacts.write(input); + written.set(input.kind, { + body, + version: receipt.artifact.version, + }); + return { + callId: call.id, + content: JSON.stringify(receipt.artifact), + }; + } + if (call.arguments === undefined) + return { + callId: call.id, + content: JSON.stringify(await artifacts.list()), + }; + const input = ToolRead(call.arguments); + if (input instanceof type.errors) + return { + callId: call.id, + content: "invalid_arguments: " + input.summary, + isError: true, + }; + const target = input.kind ?? input.artifactId; + if (target === undefined) + return { + callId: call.id, + content: JSON.stringify(await artifacts.list()), + }; + const document = await artifacts.read(target, input.version); + const workspace = + "toolCwd" in env && typeof env.toolCwd === "string" + ? env.toolCwd + : undefined; + if ( + role === "coder" && + unpackDesign && + document.kind === "design" && + workspace + ) { + const files = await unpackDesign(document.content, workspace); + // DESIGN.md runs past the host's tool-output cap, and a cut copy sends the coder + // re-reading until the doom guard fails the run; read_file pages through it. + return { + callId: call.id, + content: JSON.stringify({ + ...document, + content: `Version ${document.version} of the design is unpacked, read-only, in .builder: ${Object.keys(files).join(", ")}. Read .builder/DESIGN.md with read_file.`, + }), + }; + } + return { + callId: call.id, + content: JSON.stringify(documentPart(document, input.part)), + }; + } catch (error) { + return failure(call.id, error); + } + }, + }; + }, + }); +} diff --git a/packages/tools-design/src/file-design-option.ts b/packages/tools-design/src/file-design-option.ts index 6684644a..ab5d1deb 100644 --- a/packages/tools-design/src/file-design-option.ts +++ b/packages/tools-design/src/file-design-option.ts @@ -2,20 +2,14 @@ import { type } from "arktype"; import { readFile, realpath } from "node:fs/promises"; import { join } from "node:path"; import type { ToolResult } from "@intx/types/runtime"; -import { - ArtifactReceipt, - DesignBundle, -} from "@corbits-builder/schemas/artifact"; +import { DesignBundle } from "@corbits-builder/schemas/artifact"; +import { HubArtifactReceipt } from "@corbits-builder/tools-hub"; import { designCheckFor } from "./design-check.ts"; import { withDesignTokens } from "./design-tokens.ts"; import { DESIGN_STYLES } from "./design-tweaks.ts"; export const MAX_DESIGN_BYTES = 256 * 1024; -// A deployed agent keeps the tool bundle it started with while the hub moves on; a field a newer -// hub adds must not fail every filing of an older agent. -const HubArtifactReceipt = ArtifactReceipt.onDeepUndeclaredKey("ignore"); - export const fileDesignOptionDefinition = { name: "file_design_option", description: diff --git a/packages/tools-hub/README.md b/packages/tools-hub/README.md index 698332e3..f025a2ab 100644 --- a/packages/tools-hub/README.md +++ b/packages/tools-hub/README.md @@ -5,6 +5,8 @@ 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. +`HubArtifactReceipt` checks a filing receipt while ignoring fields a newer +hub adds. ## Where it fits @@ -18,6 +20,7 @@ those tools. - `src/index.ts`: the package's exports. - `src/http-json.ts`: `workflowHttpJson`, `mediatedHttpJson` and `WorkflowHttpError`. +- `src/artifact-receipt.ts`: `HubArtifactReceipt`. - `src/project-scope.ts`: `resolveWorkflowProjectScope` and `projectScopeCode`. diff --git a/packages/tools-hub/src/artifact-receipt.ts b/packages/tools-hub/src/artifact-receipt.ts new file mode 100644 index 00000000..4f46925b --- /dev/null +++ b/packages/tools-hub/src/artifact-receipt.ts @@ -0,0 +1,5 @@ +import { ArtifactReceipt } from "@corbits-builder/schemas/artifact"; + +// A deployed agent keeps the tool bundle it started with while the hub moves on; a field a newer +// hub adds must not fail every write of an older agent. +export const HubArtifactReceipt = ArtifactReceipt.onDeepUndeclaredKey("ignore"); diff --git a/packages/tools-hub/src/index.ts b/packages/tools-hub/src/index.ts index ce9147e6..fb41ce00 100644 --- a/packages/tools-hub/src/index.ts +++ b/packages/tools-hub/src/index.ts @@ -1,3 +1,4 @@ +export { HubArtifactReceipt } from "./artifact-receipt.ts"; export { mediatedHttpJson, WorkflowHttpError,