Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions apps/hub/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*",
Expand Down
209 changes: 1 addition & 208 deletions apps/hub/src/artifacts/tools.ts
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -67,158 +15,3 @@ export function failure(callId: string, error: unknown) {
};
return projectScopeFailure(callId, error);
}

export function createArtifactTools(
role: WorkflowRole,
): AnnotatedToolFactory<ArtifactToolEnv> {
// 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<ArtifactToolEnv>({
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<string, { body: string; version: number }>();
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);
}
},
};
},
});
}
6 changes: 4 additions & 2 deletions apps/hub/src/workflows/coder-package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 3 additions & 2 deletions apps/hub/src/workflows/entries/coordinator.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -25,7 +26,7 @@ const agent = createCoordinatorAgent({
capabilities: [],
inference: { ...source, parameters },
tools: [
createArtifactTools("coordinator"),
createArtifactTools("coordinator", { failure }),
createDeployTools(),
createSkillTools(["problem-brief"]),
],
Expand Down
5 changes: 3 additions & 2 deletions apps/hub/src/workflows/entries/writer.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -23,7 +24,7 @@ const agent = createWriterAgent({
capabilities: [],
inference: { ...source, parameters },
tools: [
createArtifactTools("writer"),
createArtifactTools("writer", { failure }),
createSkillTools([
"constraints",
"approaches",
Expand Down
14 changes: 14 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading